英國人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(我本人)在簡書平臺上,釋出過的所有文章資訊,包括每篇文章的:
- 釋出日期;
- 文章字數;
- 評論數;
- 瀏覽數、讚賞數;
等等。
最終爬取結果的輸出如下:
同時,以上結果,我們需要通過指令碼,自動傳送郵件到指定郵箱。收件內容如下:
全部操作只需要一鍵便可完成。
爬蟲設計
我們的程式一共依賴三個模組/類庫:
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!