英國人Robert Pitt曾在Github上公佈了他的爬蟲腳本,致使任何人均可以容易地取得Google Plus的大量公開用戶的ID信息。至今大概有2億2千5百萬用戶ID遭曝光。html
亮點在於,這是個nodejs腳本,很是短,包括註釋只有71行。前端
毫無疑問,nodeJS改變了整個前端開發生態。本文一步步完成了一個基於promise的nodeJS爬蟲程序,收集簡書任意指定做者的文章信息。並最終把爬下來結果以郵件的形式,自動化發給目標對象。千萬不要被nodeJS的外表嚇到,既是你是初入前端的小菜鳥,或是剛接觸nodeJS不久的新同窗,都不妨礙對這篇文章的閱讀和理解。node
爬蟲的全部代碼能夠在個人Github倉庫找到,往後這個爬蟲程序還會進行不斷升級和更新,歡迎關注。git
咱們先從爬蟲提及。對比一下,討論爲何nodeJS適合/不適合做爲爬蟲編寫語言。
首先,總結一下:github
NodeJS單線程、事件驅動的特性能夠在單臺機器上實現極大的吞吐量,很是適合寫網絡爬蟲這種資源密集型的程序。ajax
可是,對於一些複雜場景,須要更加全面的考慮。如下內容總結自知乎相關問題,感謝@知乎網友,對答案的貢獻。chrome
若是是定向爬取幾個頁面,作一些簡單的頁面解析,爬取效率不是核心要求,那麼用什麼語言差別不大。數據庫
若是是定向爬取,且主要目標是解析js動態生成的內容 :
此時,頁面內容是由js/ajax動態生成的,用普通的請求頁面+解析的方法就無論用了,須要藉助一個相似firefox、chrome瀏覽器的js引擎來對頁面的js代碼作動態解析。npm
若是爬蟲是涉及大規模網站爬取,效率、擴展性、可維護性等是必須考慮的因素時候:
大規模爬蟲爬取涉及諸多問題:多線程併發、I/O機制、分佈式爬取、消息通信、判重機制、任務調度等等,此時候語言和所用框架的選取就具備極大意義了。具體來看:數組
PHP:對多線程、異步支持較差,不建議採用。
NodeJS:對一些垂直網站爬取倒能夠。但因爲分佈式爬取、消息通信等支持較弱,根據本身狀況判斷。
Python:建議,對以上問題都有較好支持。
固然,咱們今天所實現的是一個簡易爬蟲,不會對目標網站帶來任何壓力,也不會對我的隱私形成很差影響。畢竟,他的目的只是熟悉nodeJS環境。適用於新人入門和練手。
固然,任何惡意的爬蟲性質是惡劣的,咱們應當全力避免影響,共同維護網絡環境的健康。
今天要編寫的爬蟲目的是爬取簡書做者:LucasHC(我本人)在簡書平臺上,發佈過的全部文章信息,包括:每篇文章的:
發佈日期;
文章字數;
評論數;
瀏覽數、讚揚數;
等等。
最終爬取結果的輸出以下:
如下結果,咱們須要經過腳本,自動地發送郵件到指定郵箱。收件內容以下:
以上結果只須要一鍵操做即可完成。
咱們的程序一共依賴三個模塊/類庫:
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("獲取信息出錯!"); });
由於我發現,簡書中每一篇文章的連接形式以下:
完整形式:「http://www.jianshu.com/p/ab27...」,
即 「http://www.jianshu.com/p/」 + 「文章id」。
因此,上述代碼中相關做者的每篇文章url:由baseUrl和相關文章id拼接組成:
articleIds.forEach(function(item) { url = baseUrl + item; });
articleIds天然是存儲做者每篇文章id的數組。
最終,咱們把每篇文章的html內容存儲在html這個變量中。
因爲做者可能存在多篇文章,因此對於每篇文章的獲取和解析咱們應該異步進行。這裏我使用了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方法作了什麼。
其實很明顯,若是您理解了上文的話。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!