nodeJS實(shí)現(xiàn)基于Promise爬蟲(chóng) 定時(shí)發(fā)送信息到指定郵件

英國(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í)候:

  1. PHP:對(duì)多線(xiàn)程扒袖、異步支持較差塞茅,不建議采用。
  2. NodeJS:對(duì)一些垂直網(wǎng)站爬取倒可以季率。但由于分布式爬取野瘦、消息通訊等支持較弱,根據(jù)自己情況判斷飒泻。
  3. 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)容如下:

郵件內(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!

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末育特,一起剝皮案震驚了整個(gè)濱河市丙号,隨后出現(xiàn)的幾起案子先朦,更是在濱河造成了極大的恐慌,老刑警劉巖犬缨,帶你破解...
    沈念sama閱讀 212,080評(píng)論 6 493
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件喳魏,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡怀薛,警方通過(guò)查閱死者的電腦和手機(jī)刺彩,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,422評(píng)論 3 385
  • 文/潘曉璐 我一進(jìn)店門(mén),熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)枝恋,“玉大人创倔,你說(shuō)我怎么就攤上這事》俾担” “怎么了畦攘?”我有些...
    開(kāi)封第一講書(shū)人閱讀 157,630評(píng)論 0 348
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)呐能。 經(jīng)常有香客問(wèn)我念搬,道長(zhǎng),這世上最難降的妖魔是什么摆出? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 56,554評(píng)論 1 284
  • 正文 為了忘掉前任,我火速辦了婚禮首妖,結(jié)果婚禮上偎漫,老公的妹妹穿的比我還像新娘。我一直安慰自己有缆,他們只是感情好象踊,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,662評(píng)論 6 386
  • 文/花漫 我一把揭開(kāi)白布。 她就那樣靜靜地躺著棚壁,像睡著了一般杯矩。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上袖外,一...
    開(kāi)封第一講書(shū)人閱讀 49,856評(píng)論 1 290
  • 那天史隆,我揣著相機(jī)與錄音,去河邊找鬼曼验。 笑死泌射,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的鬓照。 我是一名探鬼主播熔酷,決...
    沈念sama閱讀 39,014評(píng)論 3 408
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼豺裆!你這毒婦竟也來(lái)了拒秘?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書(shū)人閱讀 37,752評(píng)論 0 268
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎躺酒,沒(méi)想到半個(gè)月后咙轩,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 44,212評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡阴颖,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,541評(píng)論 2 327
  • 正文 我和宋清朗相戀三年活喊,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片量愧。...
    茶點(diǎn)故事閱讀 38,687評(píng)論 1 341
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡钾菊,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出偎肃,到底是詐尸還是另有隱情煞烫,我是刑警寧澤,帶...
    沈念sama閱讀 34,347評(píng)論 4 331
  • 正文 年R本政府宣布累颂,位于F島的核電站滞详,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏紊馏。R本人自食惡果不足惜料饥,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,973評(píng)論 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望朱监。 院中可真熱鬧岸啡,春花似錦、人聲如沸赫编。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 30,777評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)擂送。三九已至悦荒,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間嘹吨,已是汗流浹背搬味。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 32,006評(píng)論 1 266
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留躺苦,地道東北人身腻。 一個(gè)月前我還...
    沈念sama閱讀 46,406評(píng)論 2 360
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像匹厘,于是被迫代替她去往敵國(guó)和親嘀趟。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,576評(píng)論 2 349

推薦閱讀更多精彩內(nèi)容