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 控制并发
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();