目標(biāo)
- 使用 eventproxy(https://github.com/JacksonTian/eventproxy ) 控制并發(fā)
新建文件 app.js,當(dāng)調(diào)用 node app.js 時(shí),輸出 CNode(https://cnodejs.org/ ) 社區(qū)首頁的所有帖子標(biāo)題和鏈接以及第一條評論,以 json 的格式返回。
輸出示例:
[
{
title: '微信應(yīng)用號(hào)在前端開發(fā)圈火了,而Docker其實(shí)早已火遍后端',
href: 'https: //cnodejs.org/topic/57e45421015b4f570e0d02df',
comment1: 'weapp跟Docker。。。這兩者有關(guān)系嗎。。。。'
},
{
title: '開發(fā)過微信簽名算法的大大看過來。。。',
href: 'https: //cnodejs.org/topic/57e287a43af3942a3aa3b959',
comment1: '這個(gè)參數(shù)是微信加的用于跟蹤之類而且前端wx.config注冊一次以后就用不到簽名了還是看你的業(yè)務(wù)邏輯吧'
}
]
在上一篇文章(使用 superagent 與 cheerio 完成簡單爬蟲)中我們介紹了如何使用 superagent 和 cheerio 來獲取主頁內(nèi)容,那只需要發(fā)起一次 http get 請求就能辦到。但這次,我們需要取出每個(gè)帖子的第一條評論,這就要求我們對每個(gè)帖子的鏈接發(fā)起請求,并用 cheerio 去取出其中的第一條評論。
CNode 目前每一頁有 40 個(gè)帖子,于是我們就需要發(fā)起 1 + 40 個(gè)請求,來達(dá)到我們的目標(biāo)。對于后面的 40 個(gè)請求,我們并發(fā)地發(fā)起,而且不會(huì)遇到多線程啊鎖什么的,Node.js 的并發(fā)模型跟多線程不同,拋卻那些觀念。
初識(shí) eventproxy
用 js 寫過異步的同學(xué)應(yīng)該都知道,如果你要并發(fā)異步獲取兩三個(gè)地址的數(shù)據(jù),并且要在獲取到數(shù)據(jù)之后,對這些數(shù)據(jù)一起進(jìn)行利用的話,常規(guī)的寫法是自己維護(hù)一個(gè)計(jì)數(shù)器。
先定義一個(gè) var count = 0,然后每次抓取成功以后,就 count++。如果你是要抓取三個(gè)源的數(shù)據(jù),由于你根本不知道這些異步操作到底誰先完成,那么每次當(dāng)抓取成功的時(shí)候,就判斷一下 count === 3 。當(dāng)值為真時(shí),使用另一個(gè)函數(shù)繼續(xù)完成操作。
而 eventproxy 就起到了這個(gè)計(jì)數(shù)器的作用,它來幫你管理到底這些異步操作是否完成,完成之后,它會(huì)自動(dòng)調(diào)用你提供的處理函數(shù),并將抓取到的數(shù)據(jù)當(dāng)參數(shù)傳過來。
假設(shè)我們不使用 eventproxy 也不使用計(jì)數(shù)器時(shí),抓取三個(gè)源的寫法是這樣的:
// 參考 jquery 的 $.get 方法
$.get("http://data1_source", function(data1) {
// something
$.get("http://data2_source", function(data2) {
// something
$.get("http://data3_source", function(data3) {
// something
var html = fuck(data1, data2, data3);
render(html);
});
});
});
上述的代碼大家都寫過吧。先獲取 data1,獲取完成之后獲取 data2,然后再獲取 data3,然后 fuck 它們,進(jìn)行輸出。
但大家應(yīng)該也想到了,其實(shí)這三個(gè)源的數(shù)據(jù),是可以并行去獲取的,data2 的獲取并不依賴 data1 的完成,data3 同理也不依賴 data2。
于是我們用計(jì)數(shù)器來寫,會(huì)寫成這樣:
(function() {
var count = 0;
var result = {};
$.get('http://data1_source', function(data) {
result.data1 = data;
count++;
handle();
});
$.get('http://data2_source', function(data) {
result.data2 = data;
count++;
handle();
});
$.get('http://data3_source', function(data) {
result.data3 = data;
count++;
handle();
});
function handle() {
if (count === 3) {
var html = fuck(result.data1, result.data2, result.data3);
render(html);
}
}
})();
如果用 eventproxy,寫出來是這樣的:
var ep = new eventproxy();
ep.all('data1_event', 'data2_event', 'data3_event', function(data1, data2, data3) {
var html = fuck(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);
});
說白了,也就是個(gè)高等計(jì)數(shù)器嘛。
ep.all('data1_event', 'data2_event', 'data3_event', function (data1, data2, data3) {});
這一句,監(jiān)聽了三個(gè)事件,分別是 data1_event, data2_event, data3_event,每當(dāng)一個(gè)源的數(shù)據(jù)抓取完成時(shí),就通過 ep.emit() 來告訴 ep 自己,某某事件已經(jīng)完成了。當(dāng)三個(gè)事件未同時(shí)完成時(shí),ep.emit()調(diào)用之后不會(huì)做任何事;當(dāng)三個(gè)事件都完成時(shí),就會(huì)調(diào)用末尾的那個(gè)回調(diào)函數(shù),來對它們進(jìn)行統(tǒng)一處理。
最終版代碼:
var superagent = require('superagent');
var cheerio = require('cheerio');
// url 模塊是 Node.js 標(biāo)準(zhǔn)庫里面的,http://nodejs.org/api/url.html
var url = require('url');
var eventproxy = require('eventproxy');
var cnodeUrl = 'https://cnodejs.org/'
superagent.get(cnodeUrl)
.end(function(err, res) {
var topicUrls = [];
var $ = cheerio.load(res.text);
// 獲取首頁所有的鏈接
$('#topic_list .topic_title').each(function(index, element) {
var $element = $(element);
// $element.attr('href') 本來的樣子是 /topic/542acd7d5d28233425538b04
// 我們用 url.resolve 來自動(dòng)推斷出完整 url,變成 https://cnodejs.org/topic/542acd7d5d28233425538b04 的形式
// 具體請看 http://nodejs.org/api/url.html#url_url_resolve_from_to 的示例
var href = url.resolve(cnodeUrl, $element.attr('href'));
topicUrls.push(href);
});
// 得到一個(gè) eventproxy 的實(shí)例
var ep = new eventproxy();
// 命令 ep 重復(fù)監(jiān)聽 topicUrls.length 次(在這里也就是 40 次) 再執(zhí)行后面的回調(diào)函數(shù)
ep.after('topic_html', topicUrls.length, function(topics) {
// 數(shù)組的 map 方法返回一個(gè)由原數(shù)組中的每個(gè)元素調(diào)用一個(gè)指定方法后的返回值組成的新數(shù)組
var data = 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(data);
});
topicUrls.forEach(function(topicUrl) {
superagent.get(topicUrl)
.end(function(err, res) {
ep.emit('topic_html', [topicUrl, res.text]);
});
});
});
在上面的代碼中我們使用到了 eventproxy的重復(fù)異步協(xié)作。參照(https://github.com/JacksonTian/eventproxy#重復(fù)異步協(xié)作 )。