注意事项:
这里的爬虫不做太复杂的处理..
考虑到并发问题.这里的爬虫仅仅是爬完上一个后再爬下一个. 爬完当页后再去爬取下一页,效率虽然低..但是胜在不用同一时间发请大量请求避免被ban
本文以admin5.com为案例来爬取200页的文章title和content
本文涉及到的es6语法这里只会简单的说明一下.如果看不懂...来打我啊(笑)
涉及框架
- crawler
- co
- cheerio
crawler:为一个封装好的nodejs爬虫库,免去你用request框架发请请求然后处理一大堆的返回代码问题.本文只把crawler当做请求工具用.内容的处理将会用cheerio框架来完成
co:能够把异步代码写成跟同步一样,号称es6的async.
cheerio:nodejs版的jQuery
分析目标网站url
目标网站的url都是
http://www.admin5.com/browse/19/list_${i}.shtml
${i}<=965
那么这就好办了.生成965个链接然后每次去爬一个链接
分享目标网站DOM结构
目标网站的每篇文字的链接都在一个class为sherry_title的a标签里
我是如何通过论坛推广产品的?
那么每次爬的时候获取当页的所有文章链接然后再去爬取
文章内容DOM结构
标题放在一个class为sherry_title的div下的h1标签中
我是如何通过论坛推广产品的?
内容则放在一个class为content的div标签中
那么内容中的图片如何爬取呢?
这个也简单...不过这篇文章暂时不说..哈哈哈哈哈
爬取分析
分析完目标网站后.那么就开始分析如何去爬.
- 封装一个获取html的Promise函数
- 封装一个获取目录的Promise函数
- 获取一个获取文章内容的Promise函数
- 开始爬取函数
关于promise与co模块
首先我们知道关于最初的解决异步方案是callback(回调),当异步请求完毕后再去通知你的callback然后我们只能在callback里去做数据处理.
这样很容易引起回调地狱.
a(function(){
b(function(){
c(function(){
d(function(){
})
})
})
})
后来出现了promise.实际上也是改善了写法而已,promise会返回两种状态,成功(resolve)和失败(reject).就像你做事情一样,只有成功或者失败
function a(id){
return new Promise(function(resolve,reject){
setTimeout(function(){
if(id>10){
reject(id)
}
resolve(id)
},1000)
})
}
a(8).then((id)=>{
id+=10;
return a(id)
})
.then((id)=>{
})
.catch((id)=>{
})
上述封装了一个a函数,这个a函数执行的时候不可能立即返回一个id给你,因为有个定时器,等一秒后才会返回. 这个就是很明显的异步.然后我们把他封装成promise
当你调用a(id)的时候,实际上就已经开始执行这个函数了,不过因为我们a函数返回的是一个promise,这个promise会有个then方法.那么我们可以在then方法里面拿到1秒以后的id
promise有个特性是,你可以返回无限的promise,然后一直then,then,then下去.这算是改善了一种写法.不过重点不在于此.因为后面的co模块和Generator函数都是基于promise来完成的
Generator函数
这个说起来太长...篇幅问题.下次再谈
Co模块
其实简单点,我们并不需要知道内部调用.我们最终想要的效果仅仅是 让异步的写法变得优雅最好能够变成同步函数.ok.co函数和未来的async可以满足你这个需求
拿上述的a函数来说,在co中是这样处理的
co(function*(){
let id=yield a(10)
let id1=yield a(id);
})
爽吧.只需要包裹在co里面,就可以达到同步写法的效果,那么这个yield后面的函数满足什么条件呢?
很简单,yield后面的函数只要是promise函数即可. 上述我们说过,promise有两种状态,一种是成功,一种是拒绝
成功当然你就可以直接拿到let id=yield a(10);这个id值咯,假如失败如何监听呢? 也很简单
try{
let id=yield a(10);
}catch(e){
}
用try,catch即可. 那么我没用try,catch 但是又返回了一个失败的状态.那错误在哪里?
说实在话..你如果不去捕捉的话..你这个错误会消失..对..就会消失掉. 如果你某一天发现你的程序无论如何也run不起来.但是莫名其妙又没报错.
相信我兄弟..这锅promise绝对要背..
那我这种懒癌晚期的患者怎么办?不可能每次都要写try,catch吧?
在nodejs中有两个事件,可以监听到未捕捉的报错信息 那就是
process.on('unhandledRejection', function (err) {
console.error(err.stack);
});
process.on(`uncaughtException`, console.error);
其实不用管这个事件是啥意思.你每次加上就行了..程序运行起来的时候有很多问题都是我们考虑不到的..但是错误又被吞了.我们又不能进一步处理.
这时候我们可以监听这两个事件.就算没写try catch 你都可以找到错误的源头.
说多了.咋们继续爬虫
获取html的Promise函数
let c=new Crawler({
retries:1, //超时重试次数
retryTimeout:3000 //超时时间
});
let contentJson=[];
const getHtml=co.wrap(function*(html){
return new Promise((resolve,reject)=>{
setTimeout(()=>{
c.queue({
url:html,
forceUTF8:true,
callback:function (error,result,$) {
if(error||!result.body){
errorCount++;
return resolve({result:false});
}
result=result.body;
resolve({error,result,$})
}
})
},2000)
})
});
这里的let c=new Crawler 为初始化爬虫引擎,返回的是这个爬虫引擎的实例.
c.queue为爬取函数.
{
url:html, //爬取目标网站的url
forceUTF8:true, // 强制转码为UTF-8
callback:function (error,result,$) { //error为如果爬取超时或者返回错误HTTP代码时会出现
if(error||!result.body){
return resolve({result:false});
}
result=result.body;
resolve({error,result,$})
}
}
这里的$是框架已经调用了cheerio.不过我们这里不用框架封装好的cheerio.
获取目录的Promise函数
const getSubHtml=co.wrap(function*(body){
let $=cheerio.load(body); //字符串转为DOM
let UrlElems=$("a.sherry_title"); //获取到目录中所有文章的url
let subUrlList=[]; //链接存储数组
UrlElems.each((i,e)=>{ //循环获取链接并且存储起来
let url=$(e).attr('href');
let href=`${url}`;
subUrlList.push(href);
});
for(let item of subUrlList){
let {result}=yield getHtml(item); //获取每篇文章的body内容
if(!result){
continue;
}
let {title,content}=yield getContent(result); //获取标题和内容
console.log(`${title}获取完毕`);
contentJson.push({ //最终存储到JSON数组中
title,
content
})
}
});
获取每篇文章内容的Promise函数
嗯..实际上这里并不是异步的.只是从DOM中去获取内容.但是为了保持好看一致..这里也就用co来封装了一下
const getContent=co.wrap(function*(body){
let $=cheerio.load(body); //字符串转DOM
let title=$(".sherry_title>h1").text(); //获取标题
let content=$(".content").text(); //获取内容
return Promise.resolve({title,content})
});
start函数
let urlList=[];
for(let i=1;i<=250;i++){
urlList.push(`http://www.admin5.com/browse/19/list_${i}.shtml`)
}
co(function*(){
for (let url of urlList){
let {result}=yield getHtml(url); //获取目录body
if(!result){
continue;
}
//console.log("result",result);
//获取当页所有SUB
yield getSubHtml(result);
}
console.info(`全部爬取完毕`,contentJson);
});
添加全局错误监听函数
process.on('unhandledRejection', function (err) {
console.error(err.stack);
});
process.on(`uncaughtException`, console.error);
最终代码
down
本文已经同步到
- igeekbar
- soulBlog