nodeJS實現基於Promise爬蟲 定時傳送資訊到指定郵件

LucasHC發表於2017-03-29

英國人Robert Pitt曾在Github上公佈了他的爬蟲指令碼,導致任何人都可以容易地取得Google Plus的大量公開使用者的ID資訊。至今大概有2億2千5百萬使用者ID遭曝光。

亮點在於,這是個nodejs指令碼,非常短,包括註釋只有71行。

毫無疑問,nodeJS改變了整個前端開發生態。
本文一步步完成了一個基於promise的nodeJS爬蟲程式,收集簡書任意指定作者的文章資訊。並最終把爬下來結果以郵件的形式,自動發給目標物件。千萬不要被nodeJS的外表嚇到,即使你是初入前端的小菜鳥,或是剛接觸nodeJS不久的新同學,都不妨礙對這篇文章的閱讀和理解。

爬蟲的所有程式碼可以在我的Github倉庫找到,日後這個爬蟲程式還會進行不斷升級和更新,歡迎關注。

nodeJS VS Python實現爬蟲

我們先從爬蟲說起。對比一下,討論為什麼nodeJS適合/不適合作為爬蟲編寫語言。
首先,總結一下:

NodeJS單執行緒、事件驅動的特性可以在單臺機器上實現極大的吞吐量,非常適合寫網路爬蟲這種資源密集型的程式。

但是,對於一些複雜場景,需要更加全面的考慮。以下內容總結自知乎相關問題,感謝@知乎網友,對答案的貢獻。

  • 如果是定向爬取幾個頁面,做一些簡單的頁面解析,爬取效率不是核心要求,那麼用什麼語言差異不大。

  • 如果是定向爬取,且主要目標是解析js動態生成的內容 :
    此時,頁面內容是由js/ajax動態生成的,用普通的請求頁面+解析的方法就不管用了,需要藉助一個類似firefox、chrome瀏覽器的js引擎來對頁面的js程式碼做動態解析。

  • 如果爬蟲是涉及大規模網站爬取,效率、擴充套件性、可維護性等是必須考慮的因素時候:
    1) PHP:對多執行緒、非同步支援較差,不建議採用。
    2) NodeJS:對一些垂直網站爬取倒可以。但由於分散式爬取、訊息通訊等支援較弱,根據自己情況判斷。
    3) Python:建議,對以上問題都有較好支援。

當然,我們今天所實現的是一個簡易爬蟲,不會對目標網站帶來任何壓力,也不會對個人隱私造成不好影響。畢竟,他的目的只是熟悉nodeJS環境。適用於新人入門和練手。

同樣,任何惡意的爬蟲性質是惡劣的,我們應當全力避免影響,共同維護網路環境的健康。

爬蟲例項

今天要編寫的爬蟲目的是爬取簡書作者:LucasHC(我本人)在簡書平臺上,釋出過的所有文章資訊,包括每篇文章的:

  • 釋出日期;
  • 文章字數;
  • 評論數;
  • 瀏覽數、讚賞數;
    等等。

最終爬取結果的輸出如下:

nodeJS實現基於Promise爬蟲 定時傳送資訊到指定郵件
爬取輸出

同時,以上結果,我們需要通過指令碼,自動傳送郵件到指定郵箱。收件內容如下:

nodeJS實現基於Promise爬蟲 定時傳送資訊到指定郵件
郵件內容

全部操作只需要一鍵便可完成。

爬蟲設計

我們的程式一共依賴三個模組/類庫:

const http = require("http");
const Promise = require("promise");
const cheerio = require("cheerio");複製程式碼

傳送請求

http是nodeJS的原生模組,自身就可以用來構建伺服器,而且http模組是由C++實現的,效能可靠。
我們使用Get,來請求簡書作者相關文章的對應頁面:

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("獲取資訊出錯!");
});複製程式碼

因為我發現,簡書中每一篇文章的連結形式如下:
完整形式:“www.jianshu.com/p/ab2741f78…
即 “www.jianshu.com/p/” + “文章id”。

所以,上述程式碼中相關作者的每篇文章url:由baseUrl和相關文章id拼接組成:

articleIds.forEach(function(item) {
    url = baseUrl + item;
});複製程式碼

articleIds自然是儲存作者每篇文章id的陣列。

最終,我們把每篇文章的html內容儲存在html這個變數中。

非同步promise封裝

由於作者可能存在多篇文章,所以對於每篇文章的獲取和解析我們應該非同步進行。這裡我使用了promise封裝上述程式碼:

function getPageAsync (url) {
    return new Promise(function(resolve, reject){
        http.get(url, function(res) {
            ...
        }).on("error", function(e) {
            reject(e);
            console.log("獲取資訊出錯!");
        });
    });
};複製程式碼

這樣一來,比如我寫過14篇原創文章。那麼對每一片文章的請求和處理全都是一個promise物件。我們儲存在預先定義好的陣列當中:

const articlePromiseArray = [];複製程式碼

接下來,我使用了Promise.all方法進行處理。

Promise.all方法用於將多個Promise例項,包裝成一個新的Promise例項。

該方法接受一個promise例項陣列作為引數,例項陣列中所有例項的狀態都變成Resolved,Promise.all返回的例項才會變成Resolved,並將Promise例項陣列的所有返回值組成一個陣列,傳遞給回撥函式。

也就是說,我的14篇文章的請求對應14個promise例項,這些例項都請求完畢後,執行以下邏輯:

Promise.all(articlePromiseArray).then(function onFulfilled (pages) {
    pages.forEach(function(html) {
        let info = filterArticles(html);
        printInfo(info);        
    });
}, function onRejected (e) {
    console.log(e);
});複製程式碼

他的目的在於:對每一個返回值(這個返回值為單篇文章的html內容),進行filterArticles方法處理。處理所得結果進行printInfo方法輸出。
接下來,我們看看filterArticles方法做了什麼。

html解析

其實很明顯,如果您理解了上文的話。filterArticles方法就是對單篇文章的html內容進行有價值的資訊提取。這裡有價值的資訊包括:
1)文章標題;
2)文章發表時間;
3)文章字數;
4)文章瀏覽量;
5)文章評論數;
6)文章讚賞數。

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;
};複製程式碼

你也許會奇怪,為什麼我能使用類似jQuery中的$對html資訊進行操作。其實這歸功於cheerio類庫。

filterArticles方法返回了每篇文章我們感興趣的內容。這些內容儲存在articleData物件當中,最終由printInfo進行輸出。

郵件自動傳送

到此,爬蟲的設計與實現到了一段落。接下來,就是把我們爬取的內容以郵件方式進行傳送。
這裡我使用了nodemailer模組進行傳送郵件。相關邏輯放在Promise.all當中:

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);
});複製程式碼

郵件服務的相關配置內容我已經進行了適當隱藏。讀者可以自行配置。

總結

本文,我們一步一步實現了一個爬蟲程式。涉及到的知識點主要有:nodeJS基本模組用法、promise概念等。如果擴充下去,我們還可以做nodeJS連線資料庫,把爬取內容存在資料庫當中。當然也可以使用node-schedule進行定時指令碼控制。當然,目前這個爬蟲目的在於入門,實現還相對簡易,目標源並不是大型資料。

全部內容只涉及nodeJS的冰山一角,希望大家一起探索。如果你對完整程式碼感興趣,請點選這裡。

Happy Coding!

相關文章