注意事项:

  1. 这里的爬虫不做太复杂的处理..

  2. 考虑到并发问题.这里的爬虫仅仅是爬完上一个后再爬下一个. 爬完当页后再去爬取下一页,效率虽然低..但是胜在不用同一时间发请大量请求避免被ban

  3. 本文以admin5.com为案例来爬取200页的文章title和content

  4. 本文涉及到的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标签里

<a href="http://www.admin5.com/article/20161209/700550.shtml" class="sherry_title" target="_blank">我是如何通过论坛推广产品的?</a> 

那么每次爬的时候获取当页的所有文章链接然后再去爬取

文章内容DOM结构

标题放在一个class为sherry_title的div下的h1标签中

<div class="sherry_title"> 
   <h1>我是如何通过论坛推广产品的?</h1> 
</div> 

内容则放在一个class为content的div标签中

<div class='content'> 
</div> 

那么内容中的图片如何爬取呢?这个也简单…不过这篇文章暂时不说..哈哈哈哈哈

爬取分析

分析完目标网站后.那么就开始分析如何去爬.

  1. 封装一个获取html的Promise函数
  2. 封装一个获取目录的Promise函数
  3. 获取一个获取文章内容的Promise函数
  4. 开始爬取函数

关于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
  • 简书