NodeJS 也可以开发爬虫服务,使用 cheerio ,可以像使用 Jquery 一样方便的把网页内容提取分析出来。

1.1.1. 第三方库

  • express
  • superagent http库,可以发起 get 或 post 请求。
  • cheerio https://github.com/cheeriojs/cheerio 可以理解成一个Node.js版的jquery,用来从网页中以css selector取数据,使用方式跟jquery一样。

下面的例子将扒取 https://cnodejs.org/ 网站

'use strict';

var express = require('express');
var superagent = require('superagent');
var cheerio = require('cheerio');

var app = express();
app.get('/', function (req, resp, next) {
    superagent.get('https://cnodejs.org/')
    .end(function(err, sres){
        if(err){
            next(err);
        }
        // sres.text 里面存储着网页的 html 内容,将它传给 cheerio.load 之后
        // 就可以得到一个实现了 jquery 接口的变量,我们习惯性地将它命名为 `$`
        // 剩下就都是 jquery 的内容了
        var $ = cheerio.load(sres.text);
        var items = [];
        $('#topic_list .topic_title').each(function (idx, element) {
            var element = $(element);
            items.push({
                title: element.attr('title'),
                href: element.attr('href')
            });
        });
        resp.send(items);
    });
});
var server = app.listen(8081, function() {
    var host = server.address().address;
    var port = server.address().port;
    console.log(host + ":" + port);
});

1.1.2. eventProxy 控制并发

eventproxy

Node.js 的并发模型跟多线程不同,如果你要并发异步获取两三个地址的数据,并且要在获取到数据之后,对这些数据一起进行利用的话,常规的写法是自己维护一个计数器。

eventproxy 就起到了这个计数器的作用,它来帮你管理到底这些异步操作是否完成,完成之后,它会自动调用你提供的处理函数,并将抓取到的数据当参数传过来。

如果利用 eventproxy,则是这样的

var ep = new eventproxy();
ep.all('data1_event', 'data2_event', 'data3_event', function (data1, data2, data3) {
    var html = func(data1, data2, data3);
    render(html);
});

$.get('http://data1_source', function (data) {
    ep.emit('data1_event', data);
});

$.get('http://data2_source', function (data) {
    ep.emit('data2_event', data);
});

$.get('http://data3_source', function (data) {
    ep.emit('data3_event', data);
});

其实就是相当于一个高等计数器

ep.all('data1_event', 'data2_event', 'data3_event', function (data1, data2, data3) {});

上面的代码监听了三个事件,分别是data1_event, data2_event, data3_event,每次当一个源的数据抓取完成时,就通过ep.emit()来告诉ep,某某事件已经完成了。 当三个事件未同时完成时,ep.emit()调用之后不会做任何事;当三个事件都完成的时候,就会调用末尾的回调函数,来对它们进行统一处理。

eventproxy 提供了不少其他场景所需的 API,但最最常用的用法就是以上的这种,即:

  • var ep = new eventproxy(); 得到一个 eventproxy 实例。
  • 告诉它你要监听哪些事件,并给它一个回调函数。ep.all('event1', 'event2', function (result1, result2) {})
  • 在适当的时候 ep.emit('event_name', eventData)

更多

使用 eventproxy 并发扒取

'use strict';

var express = require('express');
var superagent = require('superagent');
var cheerio = require('cheerio');
var eventproxy = require('eventproxy');
var url = require('url');

var app = express();
superagent.get('https://cnodejs.org/')
.end(function(err, sres){
    if(err){
        next(err);
    }
    // sres.text 里面存储着网页的 html 内容,将它传给 cheerio.load 之后
    // 就可以得到一个实现了 jquery 接口的变量,我们习惯性地将它命名为 `$`
    // 剩下就都是 jquery 的内容了
    var $ = cheerio.load(sres.text);
    var items = [];
    $('#topic_list .topic_title').each(function (idx, element) {
        var element = $(element);
        var href = url.resolve('https://cnodejs.org/', element.attr('href'));
        items.push(href);
    });
    // 获取topic
    getTopic(items);
});

function getTopic(items) {
    var ep = new eventproxy();
    // topic_getted 被触发了 items.length 次之后,执行回调方法
    ep.after('topic_getted', items.length, function(topics) {
        //
        console.log('topic_getted:' + items.length);
        topics = topics.map(function(topicPair){
            var topicUrl = topicPair[0];
            var topicHtml = topicPair[1];
            var $ = cheerio.load(topicHtml);
            return ({
                title: $('.topic_full_title').text().trim(),
                href: topicUrl,
                comment1: $('.reply_content').eq(0).text().trim(),
            });
        });
        console.log(topics);
    });

    items.forEach(function (topicUrl) {
        // 发起请求
        superagent.get(topicUrl)
        .end(function (err, res) {
            if (err) {
                console.log('error:_');
            }
            // 触发事件并传入参数 [url, 请求获取到的html]
            ep.emit('topic_getted', [topicUrl, res.text]);
        });
    });
}

要点:

var ep = new eventproxy();
ep.all('event1', 'event2', function(e1, e1){}); // 全部事件被触发之后执行回调
ep.after('event_', 20, function(data){}); // 事件被触发多少次之后执行回调

1.1.3. 添加 header

var cheerio = require('cheerio');
var request = require('request');
var fs = require('fs');
var http = require('http');

var options = {
    host: "www.mi.com",
    path: "http://www.mi.com/",
    headers: {
        'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/33.0.1750.117 Safari/537.36'
    }
};
http.get(options, function (res) {
    var html = '';
    res.on('data', function (data) {
        html += data;
    });
    res.on('end', function () {
        var $ = cheerio.load(html);
    });
});

1.1.4. demo2

articlelist.js

// title, url, date, id
var request = require('request');
var cheerio = require('cheerio');

var articleList = [];

function getArcList (url, callback) {
    request(url, function (err, res){
        if(err) {
            return console.error(err);
        }
        var $ = cheerio.load(res.body.toString());
        $('.articleList .articleCell').each(function(index, ele, list) {
            var $me = $(this);
            var article = $me.find('.atc_title a');
            var title = article.text();
            var url = article.attr('href');
            var date = $me.find('.atc_info span.atc_tm').text();
            // console.log(title, url, date);
            var s = url.match(/blog_(\w+).html/);
            var item = {
                title: title,
                url: url,
                date: date,
            };
            if(Array.isArray(s)) {
                item.id = s[1];
            }
            articleList.push(item);
        });
        var nextUrl =  $('li.SG_pgnext a').attr('href');
        console.log('下一页:' + nextUrl);
        if (nextUrl) { // -> 下一页
            getArcList(nextUrl, callback);
        } else { // 完成
            callback(null, articleList);
        }
    });
}

module.exports = getArcList;
// getArcList('http://blog.sina.com.cn/s/articlelist_1776757314_0_1.html');

main.js

var request = require('request');
var cheerio = require('cheerio');
var fs = require('fs');
var events = require('events');
var getArcList = require('./articlelist');

var classList = [];

var em = new events.EventEmitter();

em.on('classListGetted', function(err) {
    if(!err){
        console.log('classListGetted');
        // classList.each(function(index, ele, list){
        //     console.log(index, ele);
        // });
        classList.forEach(function cb (ele, index) {
            // body...
            console.log(ele);
        });
        // 获取文章列表
        getArcList(classList[0], function(err, list){
            if(list){
                console.log(list.length);
            }
        });
        return;
    }
});

function getClassList() {
    fs.readFile('classList.txt', 'utf-8', function(err, data) {
        if(err){
            console.log('get classList from net...');
            getClassListFromNet(function(err, list){
                if(!err){
                    em.emit('classListGetted', null);
                }
            });
        }else{
            console.log('get classList from disk...');
            classList = JSON.parse(data);
            em.emit('classListGetted', null);
        }
    });
}

function getClassListFromNet(callback){
    request('http://blog.sina.com.cn/u/1776757314', function (err, res) {
        if(err) {
            callback(err, null);
            return console.error(err);
        }
        var html = res.body.toString();
        var $ = cheerio.load(html);

        $('.classList a').each(function(){
            var $me = $(this);
            var item = {
                name : $me.text().trim(),
                url : $me.attr('href'),
            };
            var s = item.url.match(/list_\d+_(\d+)_\d\.html/);
            if(Array.isArray(s)){
                item.id = s[1];
            }
            classList.push(item);
        });
        // save file
        var classListText = JSON.stringify(classList);
        fs.writeFile('classList.txt', classListText, function(err) {
            // console.log(err);
        });
        //
        callback(null, classList);
    });
}

// 获取分类列表
getClassList();

results matching ""

    No results matching ""