本文主要目的是经过抓取「电影天堂」的最新电影名称和下载地址,展示如何抓取列表以后,继续抓取正文内容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是CommonJS提出来的这一种规范,有多个版本,在ES6当中已经归入规范,原生支持Promise 对象,非ES6环境能够用相似Bluebird、Q这类库来支持。函数
Promise能够将回调变成链式调用写法,流程更加清晰,代码更加优雅。fetch
简单概括下Promise:三个状态、两个过程、一个方法,3-2-1ui
固然还有其余概念,好比:catch
、Promise.all/race
这里就不展开了。this
了解了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-
@三水清
未经容许,请勿转载,不用打赏,喜欢请转发和关注
感受有用,欢迎关注个人公众号,每周一篇原创技术文章