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

抓取如果抓取的太快太频繁会被源站封IP,本文会介绍下通过限流、限速和使用代理的方式来防止被封

上篇文章,抓取「电影天堂」最新的170部电影,在抓取首页电影list之后,会同时发出170个请求抓取电影的详情页,这样在固定时间点集中爆发式的访问页面,很容易在日志中被找出来,而且并发请求大了,很可能会中网站的防火墙之类的策略,IP被加到黑名单就悲剧了

限流&限速

先说下限流的方法,将批量的并发请求,分成多次固定请求个数,等上一次抓取结束后,再开始下一次抓取,直到全部抓取结束。

这里我使用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显示时间,所以改完的代码如下:

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都是有流量限制的,不做控制,真金白银就打水漂了。。

用Node抓站(二):Promise使代码更优雅

本文主要目的是通过抓取「电影天堂」的最新电影名称和下载地址,展现如何抓取列表之后,继续抓取正文内容

使用《用Node抓站(一)》(没看过的可以翻看下本公众号的历史文章)当中写的spider.js 代码可以直接用下面的代码把列表抓出来:

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

spider({
  url: 'http://www.dytt8.net/index.htm',
  decoding: 'gb2312'
}, (err, data, body, req) => {
  if (!err) {
    console.log(data)
  }
}, {
  items: {
    selector: '.co_area2 .co_content2 ul a!attr:href'
  }
})

这里不同的是涉及到一个编码问题,「电影天堂」用的是gb2312编码,需要转成utf8,不然抓的内容会乱码。我扩展了request模块的参数增加了decoding:因为encoding被占用了,而且为了转码方便,我将encoding设为null,这样出来的数据就是Buffer,可以直接用iconv-lite之类的进行转码,涉及到编码问题不是本文讨论内容,就不多说了。

抓取列表后,发现title是被截断的,也要在正文页面抓取一下;继续写抓取下载地址和电影title的代码:

spider({
  url: 'http://www.dytt8.net/index.htm',
  decoding: 'gb2312'
}, (err, data, body, req) => {
  if (!err) {
    if (data && data.items) {
      var urls = data.items
      urls.forEach(function (url) {
        url = 'http://www.dytt8.net' + url
        spider({url: url, decoding: 'gb2312'}, (e, d) => {
          if (!e) {
            console.log(d)
          }
        }, {
          url: {
            selector: '#Zoom table td a!text'
          },
          title: {
            selector: '.title_all h1!text'
          }
        })
      })
    }
  }
}, {
  items: {
    selector: '.co_area2 .co_content2 ul a!attr:href'
  }
})

看上去挺简单的,但是回调好多啊。。。

处理这种异步回调可以使用Promise!