用 Node 抓站(二):Promise 使代碼更優雅

本文主要目的是經過抓取「電影天堂」的最新電影名稱和下載地址,展示如何抓取列表以後,繼續抓取正文內容git

使用《用Node抓站(一)》(沒看過的能夠翻看下本公衆號的歷史文章)當中寫的spider.js 代碼能夠直接用下面的代碼把列表抓出來:github

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之類的進行轉碼,涉及到編碼問題不是本文討論內容,就很少說了。json

抓取列表後,發現title是被截斷的,也要在正文頁面抓取一下;繼續寫抓取下載地址和電影title的代碼:promise

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!ide

Promise

Promise是CommonJS提出來的這一種規範,有多個版本,在ES6當中已經歸入規範,原生支持Promise 對象,非ES6環境能夠用相似Bluebird、Q這類庫來支持。函數

Promise能夠將回調變成鏈式調用寫法,流程更加清晰,代碼更加優雅。fetch

簡單概括下Promise:三個狀態、兩個過程、一個方法,3-2-1ui

  • 三個狀態:pending、fulfilled、rejected
  • 兩個過程:
    • pending→fulfilled(resolve)
    • pending→rejected(reject)
  • 一個方法:then

固然還有其餘概念,好比:catchPromise.all/race這裏就不展開了。this

代碼的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是個類,接受一個函數,函數參數是兩個函數:resolvereject,當成功的時候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都是上一次thenreturn結果,若是沒有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/ 對應文章名文件夾下能夠找到

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

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

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