英國(guó)人Robert Pitt曾在Github上公布了他的爬蟲(chóng)腳本茅信,導(dǎo)致任何人都可以容易地取得Google Plus的大量公開(kāi)用戶(hù)的ID信息主到。至今大概有2億2千5百萬(wàn)用戶(hù)ID遭曝光挡育。
亮點(diǎn)在于,這是個(gè)nodejs腳本,非常短禽篱,包括注釋只有71行囚企。
毫無(wú)疑問(wèn)丈咐,nodeJS改變了整個(gè)前端開(kāi)發(fā)生態(tài)。
本文一步步完成了一個(gè)基于promise的nodeJS爬蟲(chóng)程序龙宏,收集簡(jiǎn)書(shū)任意指定作者的文章信息棵逊。并最終把爬下來(lái)結(jié)果以郵件的形式,自動(dòng)發(fā)給目標(biāo)對(duì)象银酗。千萬(wàn)不要被nodeJS的外表嚇到辆影,即使你是初入前端的小菜鳥(niǎo),或是剛接觸nodeJS不久的新同學(xué)黍特,都不妨礙對(duì)這篇文章的閱讀和理解蛙讥。
爬蟲(chóng)的所有代碼可以在我的Github倉(cāng)庫(kù)找到,日后這個(gè)爬蟲(chóng)程序還會(huì)進(jìn)行不斷升級(jí)和更新灭衷,歡迎關(guān)注次慢。
nodeJS VS Python實(shí)現(xiàn)爬蟲(chóng)
我們先從爬蟲(chóng)說(shuō)起。對(duì)比一下翔曲,討論為什么nodeJS適合/不適合作為爬蟲(chóng)編寫(xiě)語(yǔ)言迫像。
首先,總結(jié)一下:
NodeJS單線(xiàn)程瞳遍、事件驅(qū)動(dòng)的特性可以在單臺(tái)機(jī)器上實(shí)現(xiàn)極大的吞吐量闻妓,非常適合寫(xiě)網(wǎng)絡(luò)爬蟲(chóng)這種資源密集型的程序。
但是傅蹂,對(duì)于一些復(fù)雜場(chǎng)景纷闺,需要更加全面的考慮。以下內(nèi)容總結(jié)自知乎相關(guān)問(wèn)題份蝴,感謝@知乎網(wǎng)友犁功,對(duì)答案的貢獻(xiàn)。
如果是定向爬取幾個(gè)頁(yè)面婚夫,做一些簡(jiǎn)單的頁(yè)面解析浸卦,爬取效率不是核心要求,那么用什么語(yǔ)言差異不大案糙。
如果是定向爬取限嫌,且主要目標(biāo)是解析js動(dòng)態(tài)生成的內(nèi)容 :
此時(shí)靴庆,頁(yè)面內(nèi)容是由js/ajax動(dòng)態(tài)生成的,用普通的請(qǐng)求頁(yè)面+解析的方法就不管用了怒医,需要借助一個(gè)類(lèi)似firefox侨颈、chrome瀏覽器的js引擎來(lái)對(duì)頁(yè)面的js代碼做動(dòng)態(tài)解析。如果爬蟲(chóng)是涉及大規(guī)模網(wǎng)站爬取庞瘸,效率槽卫、擴(kuò)展性、可維護(hù)性等是必須考慮的因素時(shí)候:
- PHP:對(duì)多線(xiàn)程扒袖、異步支持較差塞茅,不建議采用。
- NodeJS:對(duì)一些垂直網(wǎng)站爬取倒可以季率。但由于分布式爬取野瘦、消息通訊等支持較弱,根據(jù)自己情況判斷飒泻。
- Python:建議鞭光,對(duì)以上問(wèn)題都有較好支持。
當(dāng)然蠢络,我們今天所實(shí)現(xiàn)的是一個(gè)簡(jiǎn)易爬蟲(chóng)衰猛,不會(huì)對(duì)目標(biāo)網(wǎng)站帶來(lái)任何壓力,也不會(huì)對(duì)個(gè)人隱私造成不好影響刹孔。畢竟啡省,他的目的只是熟悉nodeJS環(huán)境。適用于新人入門(mén)和練手髓霞。
同樣卦睹,任何惡意的爬蟲(chóng)性質(zhì)是惡劣的,我們應(yīng)當(dāng)全力避免影響方库,共同維護(hù)網(wǎng)絡(luò)環(huán)境的健康结序。
爬蟲(chóng)實(shí)例
今天要編寫(xiě)的爬蟲(chóng)目的是爬取簡(jiǎn)書(shū)作者:LucasHC(我本人)在簡(jiǎn)書(shū)平臺(tái)上,發(fā)布過(guò)的所有文章信息纵潦,包括每篇文章的:
- 發(fā)布日期徐鹤;
- 文章字?jǐn)?shù);
- 評(píng)論數(shù)邀层;
- 瀏覽數(shù)返敬、贊賞數(shù);
等等寥院。
最終爬取結(jié)果的輸出如下:
同時(shí)劲赠,以上結(jié)果,我們需要通過(guò)腳本,自動(dòng)發(fā)送郵件到指定郵箱凛澎。收件內(nèi)容如下:
全部操作只需要一鍵便可完成霹肝。
爬蟲(chóng)設(shè)計(jì)
我們的程序一共依賴(lài)三個(gè)模塊/類(lèi)庫(kù):
const http = require("http");
const Promise = require("promise");
const cheerio = require("cheerio");
發(fā)送請(qǐng)求
http是nodeJS的原生模塊,自身就可以用來(lái)構(gòu)建服務(wù)器塑煎,而且http模塊是由C++實(shí)現(xiàn)的沫换,性能可靠。
我們使用Get轧叽,來(lái)請(qǐng)求簡(jiǎn)書(shū)作者相關(guān)文章的對(duì)應(yīng)頁(yè)面:
http.get(url, function(res) {
var html = "";
res.on("data", function(data) {
html += data;
});
res.on("end", function() {
...
});
}).on("error", function(e) {
reject(e);
console.log("獲取信息出錯(cuò)!");
});
因?yàn)槲野l(fā)現(xiàn)苗沧,簡(jiǎn)書(shū)中每一篇文章的鏈接形式如下:
完整形式:“http://www.reibang.com/p/ab2741f78858”刊棕,
即 “http://www.reibang.com/p/” + “文章id”炭晒。
所以,上述代碼中相關(guān)作者的每篇文章url:由baseUrl和相關(guān)文章id拼接組成:
articleIds.forEach(function(item) {
url = baseUrl + item;
});
articleIds自然是存儲(chǔ)作者每篇文章id的數(shù)組甥角。
最終网严,我們把每篇文章的html內(nèi)容存儲(chǔ)在html這個(gè)變量中。
異步promise封裝
由于作者可能存在多篇文章嗤无,所以對(duì)于每篇文章的獲取和解析我們應(yīng)該異步進(jìn)行震束。這里我使用了promise封裝上述代碼:
function getPageAsync (url) {
return new Promise(function(resolve, reject){
http.get(url, function(res) {
...
}).on("error", function(e) {
reject(e);
console.log("獲取信息出錯(cuò)!");
});
});
};
這樣一來(lái),比如我寫(xiě)過(guò)14篇原創(chuàng)文章当犯。那么對(duì)每一片文章的請(qǐng)求和處理全都是一個(gè)promise對(duì)象垢村。我們存儲(chǔ)在預(yù)先定義好的數(shù)組當(dāng)中:
const articlePromiseArray = [];
接下來(lái),我使用了Promise.all方法進(jìn)行處理嚎卫。
Promise.all方法用于將多個(gè)Promise實(shí)例嘉栓,包裝成一個(gè)新的Promise實(shí)例。
該方法接受一個(gè)promise實(shí)例數(shù)組作為參數(shù)拓诸,實(shí)例數(shù)組中所有實(shí)例的狀態(tài)都變成Resolved侵佃,Promise.all返回的實(shí)例才會(huì)變成Resolved,并將Promise實(shí)例數(shù)組的所有返回值組成一個(gè)數(shù)組奠支,傳遞給回調(diào)函數(shù)馋辈。
也就是說(shuō),我的14篇文章的請(qǐng)求對(duì)應(yīng)14個(gè)promise實(shí)例倍谜,這些實(shí)例都請(qǐng)求完畢后迈螟,執(zhí)行以下邏輯:
Promise.all(articlePromiseArray).then(function onFulfilled (pages) {
pages.forEach(function(html) {
let info = filterArticles(html);
printInfo(info);
});
}, function onRejected (e) {
console.log(e);
});
他的目的在于:對(duì)每一個(gè)返回值(這個(gè)返回值為單篇文章的html內(nèi)容),進(jìn)行filterArticles方法處理尔崔。處理所得結(jié)果進(jìn)行printInfo方法輸出答毫。
接下來(lái),我們看看filterArticles方法做了什么您旁。
html解析
其實(shí)很明顯烙常,如果您理解了上文的話(huà)。filterArticles方法就是對(duì)單篇文章的html內(nèi)容進(jìn)行有價(jià)值的信息提取。這里有價(jià)值的信息包括:
1)文章標(biāo)題蚕脏;
2)文章發(fā)表時(shí)間侦副;
3)文章字?jǐn)?shù);
4)文章瀏覽量驼鞭;
5)文章評(píng)論數(shù)秦驯;
6)文章贊賞數(shù)。
function filterArticles (html) {
let $ = cheerio.load(html);
let title = $(".article .title").text();
let publishTime = $('.publish-time').text();
let textNum = $('.wordage').text().split(' ')[1];
let views = $('.views-count').text().split('閱讀')[1];
let commentsNum = $('.comments-count').text();
let likeNum = $('.likes-count').text();
let articleData = {
title: title,
publishTime: publishTime,
textNum: textNum
views: views,
commentsNum: commentsNum,
likeNum: likeNum
};
return articleData;
};
你也許會(huì)奇怪挣棕,為什么我能使用類(lèi)似jQuery中的$對(duì)html信息進(jìn)行操作译隘。其實(shí)這歸功于cheerio類(lèi)庫(kù)。
filterArticles方法返回了每篇文章我們感興趣的內(nèi)容洛心。這些內(nèi)容存儲(chǔ)在articleData對(duì)象當(dāng)中固耘,最終由printInfo進(jìn)行輸出。
郵件自動(dòng)發(fā)送
到此词身,爬蟲(chóng)的設(shè)計(jì)與實(shí)現(xiàn)到了一段落厅目。接下來(lái),就是把我們爬取的內(nèi)容以郵件方式進(jìn)行發(fā)送法严。
這里我使用了nodemailer模塊進(jìn)行發(fā)送郵件损敷。相關(guān)邏輯放在Promise.all當(dāng)中:
Promise.all(articlePromiseArray).then(function onFulfilled (pages) {
let mailContent = '';
var transporter = nodemailer.createTransport({
host : 'smtp.sina.com',
secureConnection: true, // 使用SSL方式(安全方式,防止被竊取信息)
auth : {
user : '**@sina.com',
pass : ***
},
});
var mailOptions = {
// ...
};
transporter.sendMail(mailOptions, function(error, info){
if (error) {
console.log(error);
}
else {
console.log('Message sent: ' + info.response);
}
});
}, function onRejected (e) {
console.log(e);
});
郵件服務(wù)的相關(guān)配置內(nèi)容我已經(jīng)進(jìn)行了適當(dāng)隱藏深啤。讀者可以自行配置拗馒。
總結(jié)
本文,我們一步一步實(shí)現(xiàn)了一個(gè)爬蟲(chóng)程序溯街。涉及到的知識(shí)點(diǎn)主要有:nodeJS基本模塊用法诱桂、promise概念等。如果拓展下去苫幢,我們還可以做nodeJS連接數(shù)據(jù)庫(kù)访诱,把爬取內(nèi)容存在數(shù)據(jù)庫(kù)當(dāng)中。當(dāng)然也可以使用node-schedule進(jìn)行定時(shí)腳本控制韩肝。當(dāng)然触菜,目前這個(gè)爬蟲(chóng)目的在于入門(mén),實(shí)現(xiàn)還相對(duì)簡(jiǎn)易哀峻,目標(biāo)源并不是大型數(shù)據(jù)涡相。
全部?jī)?nèi)容只涉及nodeJS的冰山一角,希望大家一起探索剩蟀。如果你對(duì)完整代碼感興趣催蝗,請(qǐng)點(diǎn)擊這里。
Happy Coding!