Node Crawler 使用说明

image

node-crawler Doc
Crawler DOC 翻译

在使用这个框架一段时间之后,发现这个doc 有些乱,并且缺少完整的案例。 面对不同的情况,想让crawler正常运作的话,需要查看依赖的库,如request , cheerio等。在此我记录一下我个人对这个框架的使用说明,附案例。本人非科班出身,非javaScript专业用户,文内如有偏颇,欢迎指正!
pre:如果你对javaScript、Nodejs没有基础,建议先阅读我的学习笔记:(目前没整理出来,比较懒!23333)

目录:
一、框架机制 :
二、常用参数:
三、案例:豆瓣图书
四、How to Debug


一、框架机制

nodejs 作用机制是“单线程异步”“非阻塞式IO模型”,赘述一下,就是主线程是单线程,而处理主线程“分发”的事件是交由ChromeV8异步处理的。


机制.jpg

1.1 crawler

所以针对这个机制,crawler 维护一个任务队列/请求队列queue, 主线程遇到加入queue的请求,会把新请求丢入队列,如果这个请求中有callback,则callback会被交给异步线程处理,主线程继续向下执行,直到程序done。随后,主线程会不断从queue头部取新任务处理,形成闭环,直到队列为空。

类似上图中右侧图,在额外操作一个任务队列。

1.2 other spider

当然,nodejs 的单线程异步,可能会让其他语言“转职”过来的人迷惑。由于我没了解过其他语言爬虫机制,说一下我对其机制的猜测(阻塞式IO):

爬虫程序由function1请求入口页面。假设入口页面有100个list url,在function1中循环100次,请求fucntion2进入list页面。假设每个list页面有10个detail url, 则在function2中循环10次请求detail函数写入database,程序完成交回爬虫程序done。这样完成了一个单线程阻塞式模型,清楚知道爬虫程序运行到哪里,该在哪debug。在这种情况下要开多线程,则可以先准备好10个线程的线程池,在入口页面函数function1中将100个list request交给10个线程处理,每条线程依照上面的步骤跑到底,空闲则回到线程池,爬虫主线程会再从剩下的90个list req中分给线程任务,直到threadPool 为空。

上述过程相对来说更符合自然人操作逻辑,更好理解。具体不同框架肯定对线程池的调度有着不同的优化,例如开启的额外线程可能会每完成一个请求函数,就回到线程池, 在总爬虫程序程序构造方法处进行线程池设置。也可能添加callback函数优化翻页逻辑,这些我不得而知。

1.3 总结

在理解了框架工作机制后,不难发现尽管crawler只有一个主线程,但工作效率并不低,可以用于生产环境。唯一不足是因为框架本身轻量,欠缺了一些鲁棒性。

其实,爬虫无非就是请求request和响应response,下文简写req与res。如果你的req与浏览器一致,那么你的到的res也必然相同,剩下的事情就是解析res得到自己想要的数据。至于所有的爬虫框架就是在这最本质的内核上锦上添花、方便使用,crawler 的分布式版本 floodesh ,即,将crawler维护的queue 改为分布式DB MongoDB,增加了主机index与客户端worker,分别负责任务调度与爬取工作。
floodesh DOC文档


二、常用参数

2.1 依赖包

java 习惯称之为包, 也可叫模块、轮子……whatever!源码如下:

var path = require('path')//解决一些path 问题,如不同系统\ /,绝对、相对路径
    , util = require('util')//node核心模块,解决一些回调继承的问题
    , EventEmitter = require('events').EventEmitter//nodejs异步io事件队列
    , request = require('request')//发送请求
    , _ = require('lodash')//优化一些js对象操作,提供方便使用的接口
    , cheerio = require('cheerio')//jquery选择器
    , fs = require('fs')//file 的io操作
    , Bottleneck = require('bottleneckp')//任务调度以及限制速率
    , seenreq = require('seenreq')//req url 去重
    , iconvLite = require('iconv-lite')//编码转换
    , typeis = require('type-is').is;//js 类型检查器

日常使用的话,不需要了解所有包的全部功能, 需要的话可以查阅文档:
https://www.npmjs.com/
最常用的的如request 、cheerio还是建议了解一下 DOC。

2.2 参数

对于crawler维护的任务队列, 其实是一个包含options对象的json数组,源码:

Crawler.prototype.queue = function queue (options) {
    var self = this;

    // Did you get a single object or string? Make it compatible.
    options = _.isArray(options) ? options : [options];

    options = _.flattenDeep(options);

    for(var i = 0; i < options.length; ++i) {
        if(self.isIllegal(options[i])) {
            log('warn','Illegal queue option: ', JSON.stringify(options[i]));
            continue;
        }
        self._pushToQueue(
            _.isString(options[i]) ? {uri: options[i]} : options[i]
        );
    }
};

option可以全局传给crawler,这样会对每一次请求生效, 也可以给把独立的option传给queue,关于这点doc写的很清楚。option常用参数和默认值见源码:

var defaultOptions = {
        autoWindowClose:        true,
        forceUTF8:              true,
        gzip:                   true,
        incomingEncoding:       null,
        jQuery:                 true,//res 是否注入 cheerio,doc有详细说明
        maxConnections:         10,//只有在rateLimit == 0时起作用,限制并发数
        method:                 'GET',
        priority:               5,//queue请求优先级,模拟用户行为
        priorityRange:          10,
        rateLimit:             0,//请求最小间隔
        referer:                false,
        retries:                3,//重试次数,请求不成功会重试3次
        retryTimeout:           10000,//重试间隔
        timeout:                15000,//15s req无响应,req失败
        skipDuplicates:         false,//url去重,建议框架外单读使用seenreq
        rotateUA:               false,//数组多组UA
        homogeneous:            false
    };

第一章有提到,爬虫最重要的是req和res , crawler在req部分使用的是 request.js API :https://github.com/request/request#requestoptions-callback
可以在options中使用request.js ,诸如body、form 、header…具体可以见第三章的实例代码。

2.3 常识

在简介 crawler event 之前,要提到一些爬虫的常识,因为我有看到github上有人对crawler提问,提问的原因是自己常识不足!
爬虫实际情况大概分两种: 一、针对异步API接口 ,二、针对url返回的html页面。一般来讲,前者返回可解析的json数据,而后者返回的是html文本,你需要用正则regex匹配自己想要的,也可以cheerio注入jquery得到自己想要的。
如果已经想清楚自己是那种情况,仍然得不到res的话,排除服务器端加密情况,多半就是你req没有发对,建议chrome 多按按F12、request.js doc、cheerio doc。

2.4 事件

crawler doc中这部份表述非常清晰,提一下常用情况:

queue : 推任务到queue
schedule : 任务被推queue时候触发,多用于添加代理
drain : queue为空时触发 , 多用于关闭数据库、关闭写入流

如果你在queue为空后,异步重新把任务推入queue,会频繁触发drain。


三、案例:豆瓣图书

爬取豆瓣图书TOP250总榜,这是一个返回html页面的案例,算是爬虫届的HelloWorld !

"use strict";
process.env.NODE_TLS_REJECT_UNAUTHORIZED = "0";
const fs = require('fs');
const moment = require('moment');
const Crawler = require('crawler');

const _prgname = 'doubanTop250';
class Douban{
    constructor() {
        this.writeStream = fs.createWriteStream('../result/' + _prgname + '_book_' + moment().format('YYYY-MM-DD') + '.csv');
        this.header = ['排名','标题','信息','评分','url','抓取时间'];
        this.rank = 1;
        this.crawler = new Crawler({
            maxConnection: 1,
            forceUTF8: true,
            rateLimit: 2000,
            jar: true,
            time: true,
            headers: {
                'User-Agent':`Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/79.0.3945.88 Safari/537.36`//,
            }
        });
        this.crawler.on('drain', () => {
            console.log('Job done');
            //end stream
            this.writeStream.end();
        }).on('schedule', options => {
            //options.proxy = 'http://xxx.xxx.xx.xxx:xxxx';
            options.limiter = Math.floor(Math.random() * 10);//并发10
        });
    }

    start() {
        let self = this;
        self.writeStream.write(`\ufeff${self.header}\n`);
        console.log(`start`);
        this.crawler.queue({
            uri: 'https://book.douban.com/top250?icn=index-book250-all' ,
            method:'GET',
            gene:{
                page : 1
            },
            callback: this.pageList.bind(this)
        });
    }
    
    pageList(err, res, done) {
        let self = this;
        if (err) {
            console.log(`pageList got erro : ${err.stack}`);
            return done();
        }
        const gene = res.options.gene;
        const $ = res.$;
        $('#content > div > div.article > div.indent >table').map(function (){
            const title = $('tr > td:nth-child(2) > div.pl2 a ',this).text().trim().replace(/[,\r\n]/g, '');
            const src = $('tr > td:nth-child(2) > div.pl2 a',this).attr("href");
            const info = $('tr > td:nth-child(2) p.pl',this).text();
            const rate = $('tr > td:nth-child(2) span.rating_nums',this).text();
            const time = moment().format('YYYY-MM-DD HH:mm:ss');
            
            const result = [self.rank++, title, info, rate, src, time];
            console.log(`${result}\n`);
            self.writeStream.write(`${result}\n`);
        });

        if(gene.page <= 10){
            console.log(`currentPage : ${gene.page}`);
            this.crawler.queue({
                uri: 'https://book.douban.com/top250?start=' + gene.page*25,
                method:'GET',
                gene : {
                    page : gene.page + 1
                },
                callback: self.pageList.bind(self)
            });
        }
        return done();
    }
}
const douban = new Douban();
douban.start();

install 相关的包,在上级目录建好result文件夹,脚本可以直接跑。
注:
1、 gene 为自定义通过option传入回调的json对象。
2、 使用jquery 时,作用域this覆盖问题,可以用self指向本类this。


四、How to Debug

crawler 可以使用docker debug 稍微复杂有空单起一篇文章。
但是一般比较简单的脚本使用log在关键节点记录一下就可以查出问题。
见案例代码console.log()多为debug服务。

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

推荐阅读更多精彩内容

  • 今日闲来无事在洗池里刷了一双球鞋。洗的过程中很自然地想起了数年前在杭州遇到的一位洗鞋女士,她洗鞋的价格好像是10、...
    化浊阅读 395评论 4 4
  • 亚青寺距离甘孜县城100公里左右,一路向西翻越卓达拉山(海拔4600米)和海子山(海拔4410米)两座山峰,围绕寺...
    Yn大漠沙如雪阅读 579评论 0 3
  • 我从来不是一个很会生活的人,没有远大的人生大计,过好每一日每一时。我会从街首到街尾,只为吃一碗羊肉面,我也会精心准...
    花若兮阅读 1,113评论 2 8
  • 一滴阳光阅读 343评论 0 0