IT博客汇
  • 首页
  • 精华
  • 技术
  • 设计
  • 资讯
  • 扯淡
  • 权利声明
  • 登录 注册

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

    三水清 (ksky521@gmail.com)发表于 2018-06-25 03:33:19
    love 0

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

    使用《用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!

    Promise

    Promise是CommonJS提出来的这一种规范,有多个版本,在ES6当中已经纳入规范,原生支持Promise 对象,非ES6环境可以用类似Bluebird、Q这类库来支持。

    Promise可以将回调变成链式调用写法,流程更加清晰,代码更加优雅。

    简单归纳下Promise:三个状态、两个过程、一个方法,3-2-1

    • 三个状态:pending、fulfilled、rejected
    • 两个过程:
      • pending→fulfilled(resolve)
      • pending→rejected(reject)
    • 一个方法:then

    当然还有其他概念,比如:catch、Promise.all/race这里就不展开了。

    代码的Promise改造

    了解了Promise之后,先把spider.js改成Promise的

    return new Promise((resolve, reject) => {
      opts.callback = function (error, response, body) {
        if (!error) {
          body = iconv.decode(body, opts.decoding || 'utf8')
          // 处理json
          try {
            body = JSON.parse(body)
          } catch (e) {
          }
          var data = parser(body, handlerMap)
          callback(error, data, response)
          resolve(data, response)
        } else {
          callback(error, body, response)
          reject(error)
        }
      }
      request(opts)
    })
    

    这里Promise是个类,接受一个函数,函数参数是两个函数:resolve和reject,当成功的时候resolve(结果),当失败的时候reject(原因)

    完成spider.js改造之后,使用spider抓取代码变成了下面这样:

    spider({
      url: 'http://www.dytt8.net/index.htm',
      decoding: 'gb2312'
    }, {
      items: {
        selector: '.co_area2 .co_content2 ul a!attr:href'
      }
    }).then(function (data) {
      // 第一页成功
      if (data && data.items) {
        var urls = data.items
        urls.forEach(function (url) {
          url = 'http://www.dytt8.net' + url
          // 遍历开始抓取第二页面
          spider({url: url, decoding: 'gb2312'}, {
            url: {
              selector: '#Zoom table td a!text'
            },
            title: {
              selector: '.title_all h1!text'
            }
          }).then((d) => {
            console.log(d)
          })
        })
      }
    })
    

    上面的代码能够实现需求,但是没有充分利用Promise的链式写法,还是出现了回调,没有专注程序流程,看上去还是乱糟糟的。

    Promise的链式调用

    提到链式调用,最多的是jQuery的写法:$(document).click(handler).addClass()….。

    这里简单代码实现一个可以链式调用的类,方便大家举一反三:

    
    class M {
      constructor (number) {
        this.number = number
      }
      add (n) {
        this.number += n
        return this
      }
      sub (n) {
        this.number -= n
        return this
      }
      result () {
        return this.number
      }
    }
    
    var m = new M(1)
    m.add(2).sub(3).result()
    

    在Promise中,每个then或者catch 返回的都是一个Promise对象,所以可以继续用then/catch,而且每次then都是上一次then的return结果,如果没有return那么就是undefined,例如下面:

    var resolve = Promise.resolve(1)
    
    resolve.then((d) => {
      console.log(`第1个:${d}`) // 1
    }).then((d) => {
      console.log(`第2个:${d}`) // undefined
    })
    

    而如果return 则是return后的结果:

    var resolve = Promise.resolve(1)
    
    resolve.then((d) => {
      console.log(`第1个:${d}`) // 1
      return 2 // 2
    }).then((d) => {
      console.log(`第2个:${d}`) //2
    })
    

    上面的代码和下面的代码实现一样,建议每个then都返回一个Promise对象

    var resolve = Promise.resolve(1)
    
    resolve.then((d) => {
      console.log(`第1个:${d}`)
      return Promise.resolve(2)
    }).then((d) => {
      console.log(`第2个:${d}`)
    })
    

    了解了上面的知识之后,我将整个流程划分为三部分:获取列表fetchList,处理列表数据dealListData和获取正文内容fetchContents

    然后将三个相互关联串行的流程,通过then串联起来:

    fetchList().then(dealListData).then(fetchContents).then((d) => {
      console.log(d, d.length)
    }).catch((e) => {
      console.log(e)
    })
    

    再来看下特殊处理的fetchContents,因为传进来的是一堆需要抓取的正文页面的url,如果我们使用Promise.all这个方法,其中一个正文页面抓取失败,就会导致Promise都rejected,则后续then都失败,Promise状态只会改变一次,而且回调只会执行一次。我们的需求是正文页面一个抓取失败不要紧,其他的页面继续抓取。所以特殊处理下:

    function fetchContents (urls) {
      return new Promise((resolve, reject) => {
        var count = 0
        var len = urls.length
        var results = []
        while (len--) {
          var url = urls[len]
          count++
          spider({url: url, decoding: 'gb2312'}, {
            url: {
              selector: '#Zoom table td a!text'
            },
            title: {
              selector: '.title_all h1!text'
            }
          }).then((d) => {
            results.push(d)
          }).finally(() => {
            count--
            if (count === 0) {
              resolve(results)
            }
          })
        }
      })
    }
    

    总结

    本文通过抓取「电影天堂」下载地址的实例,粗略的讲解了Promise的使用方法。后面抓取系列文章还会介绍怎么避免封IP等知识,敬请关注本公众号后续文章。

    本文的完整代码,在github/ksky521/mpdemo/ 对应文章名文件夹下可以找到



沪ICP备19023445号-2号
友情链接