本文主要目的是通過抓取「電影天堂」的最新電影名稱和下載地址,展現如何抓取列表之後,繼續抓取正文內容
使用《用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/ 對應文章名資料夾下可以找到
-eof-
@三水清
未經允許,請勿轉載,不用打賞,喜歡請轉發和關注
感覺有用,歡迎關注我的公眾號,每週一篇原創技術文章