用Node抓站(三):防止被封

抓取若是抓取的太快太頻繁會被源站封IP,本文會介紹下經過限流、限速和使用代理的方式來防止被封html

上篇文章,抓取「電影天堂」最新的170部電影,在抓取首頁電影list以後,會同時發出170個請求抓取電影的詳情頁,這樣在固定時間點集中爆發式的訪問頁面,很容易在日誌中被找出來,並且併發請求大了,極可能會中網站的防火牆之類的策略,IP被加到黑名單就悲劇了數據庫

限流&限速

先說下限流的方法,將批量的併發請求,分紅屢次固定請求個數,等上一次抓取結束後,再開始下一次抓取,直到所有抓取結束。json

這裏我使用async模塊限制併發次數,async主要有:集合、流程和工具三大類方法,這裏我使用eachLimit(arr, limit, iterator, [callback]),全部修改是上篇文章的fetchContents方法,該方法接受抓取到的170個文章的url list,此次經過eachLimit將170個url按照3個一組併發,依次執行,具體代碼以下:數組

function fetchContents (urls) {
  return new Promise((resolve, reject) => {
    var results = []
    async.eachLimit(urls, 3, (url, callback) => {
      spider({url: url, decoding: 'gb2312'}, {
        url: {
          selector: '#Zoom table td a!text'
        },
        title: {
          selector: '.title_all h1!text'
        }
      }).then((d) => {
        results.push(d)
        callback()
      }, () => {
        callback()
      })
    }, () => {
      resolve(results)
    })
  })
}複製代碼

限流只是控制了一次併發的請求數,並無讓抓取程序慢下來,因此還須要限速,在限流的基礎上限速就變得很簡單,只須要在執行eachLimitcallback的時候,加上個Timer就行了,爲了方便查看限速的效果,每次抓取成功以後,都console.log顯示時間,因此改完的代碼以下:markdown

function fetchContents (urls) {
  return new Promise((resolve, reject) => {
    var results = []
    async.eachLimit(urls, 3, (url, callback) => {
      spider({url: url, decoding: 'gb2312'}, {
        url: {
          selector: '#Zoom table td a!text'
        },
        title: {
          selector: '.title_all h1!text'
        }
      }).then((d) => {
        var time = moment().format(‘HH:MM:ss')
        console.log(`${url}===>success, ${time}`)
        results.push(d)
        setTimeout(callback, 2e3)
      }, () => {
        callback()
      })
    }, () => {
      resolve(results)
    })
  })
}複製代碼

效果以下:併發

避免重複抓取

由於一些網站更新比較慢,咱們寫的抓取程序在定時腳本任務(crontab)跑的時候,可能網站尚未更新,若是不作處理會形成資源的浪費,尤爲國內很多VPS都是有流量限制的,不作控制,真金白銀就打水漂了。。async

繼續拿「電影天堂」最新更新的內容進行抓取,若是假設每五分鐘執行一次抓取腳本,那麼須要記錄下已經抓取過的文章(電影),這裏我簡單處理一下,經過一個_fetchedList.json的文件,記錄抓取完的文章(電影)。具體思路以下:ide

  1. 抓取每一個電影詳情頁成功後,將抓取到的url放入一個數組Array
  2. 等所有抓取結束,將這個數組Array,寫到文件_fetchedList.json
  3. 下次抓取的時候,require這個_fetchedList.json,獲得數組Array,抓取以前判斷要抓取的url是否在這個數組內
  4. 數組保持長度是300(170個電影夠用了),保證先入先出,即超過300長度將最先的移出

具體代碼講解以下:函數

引入抓取的記錄文件
var fs = require('fs-extra')
var path = require('path')
var uniqueArray = []
const UNIQUE_ARRAY_URL = './_fetchedList.json'
try {
  uniqueArray = require(UNIQUE_ARRAY_URL)
} catch (e) {
}複製代碼
改造url處理函數,過濾下url數組,已經抓取過的就不要抓取了
function dealListData (data) {
  return new Promise((resolve, reject) => {
    var urls = _.get(data, 'items')
    if (urls) {
      urls = urls.map(url => {
        return 'http://www.dytt8.net' + url
      }).filter(url => {
        return uniqueArray.indexOf(url) === -1
      })
      // 若是爲空就reject
      urls.length ? resolve(urls) : reject('empty urls')
    } else {
      reject(urls)
    }
  })
}複製代碼
增長一個處理方法,保持uniqueArray長度是300,不要無限增長
function addUniqueArray (url) {
  uniqueArray.push(url)
  if (uniqueArray.length > 300) {
    // 超長就刪掉多餘的
    uniqueArray.shift()
  }
}複製代碼
在抓取完以後,記錄新的uniqueArray數組內容到json文件:
fetchList().then(dealListData).then(fetchContents).then((d) => {
  console.log(d, d.length)
  // json落地
  fs.writeJson(path.join(__dirname, UNIQUE_ARRAY_URL), uniqueArray)
}).catch((e) => {
  console.log(e)
})複製代碼

代理

爲了迷惑被抓取的網站,除了假裝User-Agent等方法,最重要的是使用代理服務,若是有錢的主能夠買代理,而後用,對於咱們作demo,那就直接抓取代理吧!下面代碼是抓取快代理網站的代理代碼:工具

var spider = require('../lib/spider')

function fetchProxy () {
  return spider({
    url: 'http://www.kuaidaili.com/proxylist/1/'
  }, {
    selector: '#index_free_list tbody tr',
    handler: function ($tr, $) {
      var proxy = []
      $tr.find('td').each(function (i) {
        proxy[i] = $(this).html().trim()
      })
      if (proxy[0] && proxy[1] && /\d{1,3}.\d{1,3}.\d{1,3}.\d{1,3}/.test(proxy[0]) && /\d{2,6}/.test(proxy[1])) {
      } else {
        // console.log(proxy);
      }
      return proxy
    }
  })
}

fetchProxy().then(data => {
  console.log(data.map(p => p.join(',')))
})複製代碼

抓取以後的代理不必定直接就能夠用,還須要測試下代理是否能夠訪問成功咱們要抓取的網站,先寫個checkProxy(proxy)的方法,用於檢測使用傳入的proxy是否抓取成功:

function checkProxy (proxy) {
  return spider({
    url: 'http://www.dytt8.net/index.htm',
    proxy: proxy,
    timeout: 5e3,
    decoding: 'gb2312'
  }, {
    items: {
      selector: '.co_area2 .co_content2 ul a!attr:href'
    }
  })
}複製代碼

而後將從快代理網站抓取到的代理一次傳入進去:

fetchProxy().then(data => {
  var len = data.length
  var succArray = []
  data.forEach(p => {
    checkProxy(`http://${p[0]}:${p[1]}`).then(() => {
      succArray.push(p)
    }).finally(done).catch(e => void (e))
  })

  function done () {
    len--
    if (len === 0) {
      console.log(succArray)
    }
  }
})複製代碼

這裏最後console.log出來的就是經過代理抓取成功的代理,能夠存入到數據庫,之後抓取使用。

代理的維護

最後在簡單說下代理的維護,抓取到了代理,由於是免費的,通常過一段時間就會不能用了,因此在使用的時候,能夠將代理放到一個數據庫中維護,數據庫中有字段:succCountfailCount 用於記錄每次使用該代理成功和失敗的次數。每次使用代理抓取的時候,要有個反饋機制,若是成功就succCount +1 ,失敗就failCount +1。當失敗次數過多的時候,這個代理就不要再使用了。

後語

本系列寫到第三篇了,後面還會有一些常見問題解答。週末看到一篇用Python抓知乎導出txt或者markdown的文章,之後手癢就會放出番外篇 😁

敬請你們關注同時穿插更新的「Hybrid開發」專題!

-eof-
@三水清
未經容許,請勿轉載,不用打賞,喜歡請轉發和關注

感受有用,歡迎關注個人公衆號,每週一篇原創技術文章

關注三水清
相關文章
相關標籤/搜索