手把手教你如何利用nodejs+es6+co写一个爬虫章

注意事项:

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

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

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

  4. 本文涉及到的es6语法这里只会简单的说明一下.如果看不懂...来打我啊(笑)

涉及框架

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

本文已经同步到

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 162,158评论 4 370
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 68,600评论 1 307
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 111,785评论 0 254
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,655评论 0 220
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 53,075评论 3 295
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 41,002评论 1 225
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 32,146评论 2 318
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,918评论 0 211
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,671评论 1 250
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,838评论 2 254
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,318评论 1 265
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,636评论 3 263
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,343评论 3 244
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,187评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,982评论 0 201
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 36,126评论 2 285
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,934评论 2 279

推荐阅读更多精彩内容