你可能會(huì)把 NodeJS 用作網(wǎng)絡(luò)服務(wù)器边篮,但你知道它還可以用來(lái)做爬蟲嗎瘤载? 本教程中會(huì)介紹如何爬取靜態(tài)網(wǎng)頁(yè)——還有那些煩人的動(dòng)態(tài)網(wǎng)頁(yè)——使用 NodeJS 和幾個(gè)有幫助的 NPM 模塊世落。
網(wǎng)絡(luò)爬蟲的一點(diǎn)知識(shí)
網(wǎng)絡(luò)爬蟲在網(wǎng)絡(luò)編程世界中總是被鄙視——說(shuō)的也很有道理潭辈。在現(xiàn)代編程中线罕,API 用于大多數(shù)流行的服務(wù)父泳,應(yīng)該用它們來(lái)獲取數(shù)據(jù)般哼,而不是用爬蟲。爬蟲有一個(gè)固有問(wèn)題尘吗,就是它依賴于被爬取頁(yè)面的可視化結(jié)構(gòu)逝她。一旦 HTML 改變了——不管改變多么微小——都有可能完全破壞之前的代碼。
忽略這些瑕疵睬捶,學(xué)習(xí)一點(diǎn)關(guān)于網(wǎng)絡(luò)爬蟲的知識(shí)會(huì)很有幫助黔宛,一些工具可以幫我們完成這個(gè)任務(wù)。當(dāng)一個(gè)網(wǎng)站沒(méi)有給 API 或任何聚合訂閱(RSS/Atom等)時(shí)擒贸,獲取內(nèi)容只剩唯一的選項(xiàng)……爬蟲臀晃。
注意:如果無(wú)法通過(guò) API 或訂閱獲得想要的信息,這很有可能表示擁有者不希望那些信息是可訪問(wèn)的介劫。但是徽惋,還有一些例外。
為什么用 NodeJS座韵?
用所有語(yǔ)言都可以寫爬蟲险绘,真的踢京。我喜歡用 Node 的原因是因?yàn)樗漠惒教匦裕硎驹谶M(jìn)程中我的代碼任何時(shí)候都不會(huì)被阻塞宦棺。還有一個(gè)額外的優(yōu)勢(shì)瓣距,就是我很熟悉 JavaScript。最后代咸,有一些為 NodeJS 寫的新模塊可以幫助輕松爬取網(wǎng)頁(yè)蹈丸,用一種可靠的方式(好吧,其實(shí)就是爬蟲的可靠性極限D沤妗)逻杖。開始吧。
用 YQL 實(shí)現(xiàn)簡(jiǎn)單爬蟲
從簡(jiǎn)單的使用場(chǎng)景開始:靜態(tài)網(wǎng)頁(yè)思瘟。這些是標(biāo)準(zhǔn)的工場(chǎng)網(wǎng)頁(yè)荸百。對(duì)于這些,Yahoo! Query Language(YQL)可以很好的完成潮太。對(duì)于不熟悉 YQL 的人管搪,它就是一個(gè)類似 SQL 的語(yǔ)法,可以用來(lái)以一致的方式使用不同的API铡买。
YQL 有一些很棒的表來(lái)幫助開發(fā)者獲取網(wǎng)頁(yè)的 HTML。我想強(qiáng)調(diào)的是:
挨個(gè)看一下霎箍,看如何用 NodeJS 實(shí)現(xiàn)奇钞。
html/ table
html 表是從 URL 爬取 HTML 最基本的方式。用這個(gè)表實(shí)現(xiàn)的常規(guī)查詢?nèi)缦拢?/p>
select * from html where url="http://finance.yahoo.com/q?s=yhoo" and xpath='//div[@id="yfi_headlines"]/div[2]/ul/li/a'
這個(gè)查詢由兩個(gè)參數(shù)組成:“url” 和 “xpath”漂坏。網(wǎng)址大家都知道景埃。XPath 包含一個(gè) XPath 字符串,告訴 YQL 應(yīng)該返回 HTML 的哪一部分顶别。在這里查詢一下試試谷徙。
還有一些可用的參數(shù)包括 browser
(布爾型),charset
(字符串)和 compat
(字符串)驯绎。我沒(méi)有使用這些參數(shù)完慧,但如果你有特別需要的話可以參考文檔。
XPath 感覺(jué)不舒服剩失?
很不幸屈尼,XPath 不是一個(gè)獲取 HTML 屬性結(jié)構(gòu)的常用方式。對(duì)于新手讀和寫都可能很復(fù)雜拴孤。
看看下一個(gè)表脾歧,可以完成同樣的事,但使用 CSS 做替代
data.html.cssselect 表
data.html.cssselect 表是我推薦的爬取頁(yè)面 HTML 方式演熟。和 html 表用相同的方式工作鞭执,但可以用 CSS 替代 XPath。實(shí)際上,這個(gè)表默默把 CSS 轉(zhuǎn)換為 XPath兄纺,然后調(diào)用 html 表大溜,所以會(huì)有一點(diǎn)慢。對(duì)于爬取網(wǎng)頁(yè)來(lái)說(shuō)囤热,區(qū)別可以忽略不計(jì)猎提。
使用這個(gè)表的通常方式是:
select * from data.html.cssselect where url="www.yahoo.com" and css="#news a"
可以看到,整潔許多旁蔼。我建議在嘗試用 YQL 爬取網(wǎng)頁(yè)的時(shí)候優(yōu)先嘗試這個(gè)方法锨苏。 在這里查詢一下試試。
* htmlstring* 表
htmlstring 表在嘗試從網(wǎng)頁(yè)爬取大量格式化文本的時(shí)候用棺聊。
用這個(gè)表可以用一個(gè)單獨(dú)的字符串抓取網(wǎng)頁(yè)的全部 HTML 內(nèi)容伞租,而不是基于 DOM 結(jié)構(gòu)切分的 JSON。
例如限佩,一個(gè)爬取 <a>
標(biāo)簽的常規(guī) JSON 返回:
"results": {
"a": {
"href": "...",
"target": "_blank",
"content": "Apple Chief Executive Cook To Climb on a New Stage"
}
}
看到 attribute 如何定義為 property 了吧葵诈?相反,htmlstring 表的返回看起來(lái)會(huì)像這樣:
"results": {
"result": {
"<a href=\"…\" target="_blank">Apple Chief Executive Cook To Climb on a New Stage</a>
}
}
所以祟同,為什么要這么用呢作喘?從我的經(jīng)驗(yàn)來(lái)看,嘗試爬取大量格式化文本的時(shí)候會(huì)相當(dāng)有用晕城。例如下面的片段:
<p>Lorem ipsum <strong>dolor sit amet</strong>, consectetur adipiscing elit.</p>
<p>Proin nec diam magna. Sed non lorem a nisi porttitor pharetra et non arcu.</p>
使用 htmlstring 表泞坦,可以把這個(gè) HTML 獲取為字符串,然后用正則移除 HTML 標(biāo)簽砖顷,留下的就只有文本了贰锁。這比 JSON 根據(jù)頁(yè)面的 DOM 結(jié)構(gòu)分為屬性和子對(duì)象的迭代更容易。
在 NodeJS 里用 YQL
現(xiàn)在我們了解了一些 YQL 中可用的表滤蝠,讓我們用 YQL 和 NodeJS 實(shí)現(xiàn)一個(gè)網(wǎng)絡(luò)爬蟲豌熄。幸運(yùn)的是,相當(dāng)簡(jiǎn)單物咳,感謝 Derek Gathright 寫的 node-yql 模塊锣险。
可以用 npm
安裝它:
npm install yql
這個(gè)模塊極為簡(jiǎn)單,只包括一個(gè)方法:YQL.exec() 方法所森。定義如下:
function exec (string query [, function callback] [, object params] [, object httpOptions])
我們 require 它然后調(diào)用 YQL.exec()
就可以用了囱持。例如,假設(shè)要抓取 Nettuts 主頁(yè)所有文章的標(biāo)題:
var YQL = require("yql");
new YQL.exec('select * from data.html.cssselect where url="http://net.tutsplus.com/" and css=".post_title a"', function(response) {
//response consists of JSON that you can parse
});
YQL 最棒的就是能夠?qū)崟r(shí)測(cè)試查詢?nèi)缓蟠_定會(huì)返回的 JSON焕济。去 console 用一下試試纷妆,或者點(diǎn)擊這里查看原生 JSON。
params
和 httpOptions
對(duì)象是可選的晴弃。參數(shù)可以包括像 env
(是否為表使用特定的環(huán)境) 和 format
(xml 或 json)這樣的屬性掩幢。所有傳給 params
的屬性都是 URI 編碼然后附到查詢字符串的尾端逊拍。httpOptions
對(duì)象被傳遞到請(qǐng)求頭中。例如這里你可以指定是否想啟用 SSL际邻。
叫做 yqlServer.js
的 JavaScript 文件芯丧,包含使用 YQL 爬取所需的最少代碼∈涝可以在終端里用以下命令來(lái)運(yùn)行它:
node yqlServer.js
例外情況和其它知名工具
YQL 是我推薦的爬取靜態(tài)網(wǎng)頁(yè)內(nèi)容的選擇缨恒,因?yàn)樽x起來(lái)簡(jiǎn)單、用起來(lái)也簡(jiǎn)單轮听。然而骗露,如果網(wǎng)頁(yè)有 robots.txt 文件來(lái)拒絕響應(yīng),YQL 就會(huì)失敗血巍。在這種情況下萧锉,可以看看下面提到的工具,或者用下一節(jié)會(huì)講的 PhantomJS述寡。
Node.io 是一個(gè)實(shí)用的 Node 工具柿隙,為數(shù)據(jù)爬取而特別設(shè)計(jì)■晷祝可以創(chuàng)建接受輸入禀崖,處理并返回某些輸出的作業(yè)。Node.io 在 GitHub 上關(guān)注量很高螟炫,有一些實(shí)用的例子幫你上手帆焕。
JSDOM 是一個(gè)很流行的項(xiàng)目,用 JavaScript 實(shí)現(xiàn)了 W3C DOM不恭。當(dāng)提供 HTML 時(shí),它可以構(gòu)造一個(gè)能夠與之交互的 DOM财饥。查看文檔换吧,了解如何使用 JSDOM 和任意 JS 庫(kù)(如 jQuery )一起從網(wǎng)頁(yè)抓取數(shù)據(jù)。
從頁(yè)面抓取動(dòng)態(tài)內(nèi)容
到目前為止钥星,我們已經(jīng)看過(guò)一些工具沾瓦,可以幫助我們抓取靜態(tài)內(nèi)容的網(wǎng)頁(yè)。有了YQL谦炒,相當(dāng)簡(jiǎn)單贯莺。不幸的是,我們經(jīng)衬模看到一些內(nèi)容是用JavaScript動(dòng)態(tài)加載的頁(yè)面缕探。在這些情況下,頁(yè)面最初通常為空还蹲,然后隨后附加內(nèi)容爹耗。如何處理這個(gè)問(wèn)題呢耙考?
例子
我提供了一個(gè)例子;我上傳了一個(gè)簡(jiǎn)單的 HTML 文件到我自己的網(wǎng)站潭兽,document.ready()
函數(shù)被調(diào)用后兩秒通過(guò) JavaScript 附加了一些內(nèi)容倦始。可以在這里查看這個(gè)頁(yè)面山卦。源文件如下:
<!DOCTYPE html>
<html>
<head>
<title>Test Page with content appended after page load</title>
</head>
<body>
Content on this page is appended to the DOM after the page is loaded.
<div id="content">
</div>
<script src="http://ajax.googleapis.com/ajax/libs/jquery/1.7.2/jquery.min.js"></script>
<script>
$(document).ready(function() {
setTimeout(function() {
$('#content').append("<h2>Article 1</h2><p>Lorem ipsum dolor sit amet, consectetur adipiscing elit.</p><h2>Article 2</h2><p>Ut sed nulla turpis, in faucibus ante. Vivamus ut malesuada est. Curabitur vel enim eget purus pharetra tempor id in tellus.</p><h2>Article 3</h2><p>Curabitur euismod hendrerit quam ut euismod. Ut leo sem, viverra nec gravida nec, tristique nec arcu.</p>");
}, 2000);
});
</script>
</body>
</html>
現(xiàn)在嘗試用 YQL 從 <div id=“content”>
中抓取文本鞋邑。
var YQL = require("yql");
new YQL.exec('select * from data.html.cssselect where url="http://tilomitra.com/repository/screenscrape/ajax.html" and css="#content"', function(response) {
//This will return undefined! The scraping was unsuccessful!
console.log(response.results);
});
你會(huì)發(fā)現(xiàn) YQL 返回了 undefined
,因?yàn)轫?yè)面被加載后账蓉,<div id=“content”>
是空的枚碗。內(nèi)容還沒(méi)有被附加上去√拊常可以在這里自己嘗試一下视译。
來(lái)看看如何解決這個(gè)問(wèn)題!
PhantomJS
PhantomJS 可以加載網(wǎng)頁(yè)归敬,并模仿基于 Webkit 的瀏覽器酷含,然而并沒(méi)有 GUI。
從這類站點(diǎn)爬取信息我建議的方式是使用 PhantomJS 汪茧。PhantomJS 形容自己是“用 JavaScript API 的無(wú)用戶界面 Webkit椅亚。“簡(jiǎn)單來(lái)說(shuō)舱污,表示 PhantomJS 可以加載網(wǎng)頁(yè)然后模仿基于 Webkit 的瀏覽器呀舔,然而并沒(méi)有GUI。作為一個(gè)開發(fā)者扩灯,可以調(diào)用 PhantomJS 提供的特定方法在頁(yè)面上執(zhí)行代碼媚赖。由于它的行為像瀏覽器,網(wǎng)頁(yè)上的腳本就像在一個(gè)普通的瀏覽器中運(yùn)行珠插。
為了從我們的頁(yè)面獲取數(shù)據(jù)惧磺,要使用 PhantomJS-Node,這是一個(gè)很小的開源項(xiàng)目捻撑,它將 PhantomJS 與NodeJS 橋接起來(lái)磨隘。此模塊默默把 PhantomJS 作為一個(gè)子進(jìn)程運(yùn)行。
安裝 PhantomJS
在安裝 PhantomJS-Node NPM 模塊之前顾患,必須安裝 PhantomJS番捂。但安裝和構(gòu)建 PhantomJS 可能有點(diǎn)棘手。
首先江解,去 PhantomJS.org 并為操作系統(tǒng)下載相應(yīng)的版本设预。我是Mac OSX。
下載后膘流,將其解壓到某個(gè)位置絮缅,例如/ Applications /
鲁沥。接下來(lái),您要將其添加到PATH
:
sudo ln -s /Applications/phantomjs-1.5.0/bin/phantomjs /usr/local/bin/
把 1.5.0
替換為你下載的 PhantomJS 版本耕魄。請(qǐng)注意画恰,并非所有系統(tǒng)都具有/ usr / local / bin /
。一些系統(tǒng)將有:/ usr / bin /
吸奴,/ bin /
或usr / X11 / bin
允扇。
對(duì)于 Windows 用戶,看這里的 短篇 教程则奥。如果你打開終端考润,輸入 phantomjs
并且沒(méi)有任何錯(cuò)誤,就安裝完成了读处。
如果你不想編輯 PATH
糊治,記下你解壓 PhantomJS 的地方,我會(huì)在下一節(jié)中展示另一種設(shè)置方法罚舱,雖然我建議你編輯 PATH
井辜。
安裝 PhantomJS-Node
設(shè)置 PhantomJS-Node 就簡(jiǎn)單多了。如果已經(jīng)安裝了 NodeJS管闷,可以通過(guò) npm 來(lái)安裝它:
npm install phantom
如果你在前一節(jié)安裝 PhantomJS 的時(shí)候沒(méi)有編輯 PATH
粥脚,可以去 npm pull 下來(lái)的 phantom/
目錄,在 phantom.js
里編輯這一行包个。
ps = child.spawn('phantomjs', args.concat([__dirname + '/shim.js', port]));
把路徑改為:
ps = child.spawn('/path/to/phantomjs-1.5.0/bin/phantomjs', args.concat([__dirname + '/shim.js', port]));
完成后刷允,可以運(yùn)行這段代碼進(jìn)行測(cè)試:
var phantom = require('phantom');
phantom.create(function(ph) {
return ph.createPage(function(page) {
return page.open("http://www.google.com", function(status) {
console.log("opened google? ", status);
return page.evaluate((function() {
return document.title;
}), function(result) {
console.log('Page title is ' + result);
return ph.exit();
});
});
});
});
在命令行運(yùn)行它應(yīng)該會(huì)有如下輸出:
opened google? success
Page title is Google
如果正確得到了,就已經(jīng)設(shè)置完成碧囊。如果沒(méi)有树灶,在現(xiàn)在評(píng)論一下我會(huì)試著幫你解決!
使用 PhantomJS-Node
為了讓你更容易糯而,我已經(jīng)在下載中包含了一個(gè)名為 phantomServer.js
的 JS 文件破托,使用了一些 PhantomJS 的 API 來(lái)加載網(wǎng)頁(yè)。等待 5 秒后執(zhí)行 JavaScript 來(lái)爬取頁(yè)面歧蒋。你可以通過(guò)導(dǎo)航到該目錄并在終端中使用以下命令來(lái)運(yùn)行它:
node phantomServer.js
我將概述一下它在這里是如何工作的。首先州既,我們需要 PhantomJS:
var phantom = require('phantom’);
接下來(lái)谜洽,利用 API 實(shí)現(xiàn)一些方法。也就是說(shuō)吴叶,我們創(chuàng)建一個(gè)實(shí)例頁(yè)面阐虚,然后調(diào)用open()
方法:
phantom.create(function(ph) {
return ph.createPage(function(page) {
//From here on in, we can use PhantomJS' API methods
return page.open("http://tilomitra.com/repository/screenscrape/ajax.html", function(status) {
//The page is now open
console.log("opened site? ", status);
});
});
});
頁(yè)面打開后,我們可以注入一些 JavaScript 到頁(yè)面上蚌卤。通過(guò) page.injectJS()
方法來(lái)注入 jQuery:
phantom.create(function(ph) {
return ph.createPage(function(page) {
return page.open("http://tilomitra.com/repository/screenscrape/ajax.html", function(status) {
console.log("opened site? ", status);
page.injectJs('http://ajax.googleapis.com/ajax/libs/jquery/1.7.2/jquery.min.js', function() {
//jQuery Loaded
//We can use things like $("body").html() in here.
});
});
});
});
jQuery 現(xiàn)在加載好了实束,但我們不知道頁(yè)面上的動(dòng)態(tài)內(nèi)容是否加載完畢奥秆。為了解決這個(gè)問(wèn)題,我通常會(huì)把我的爬蟲代碼放在一個(gè) setTimeout()
函數(shù)中咸灿,在特定時(shí)間間隔后執(zhí)行构订。如果你想要一個(gè)更靈活的方案,PhantomJS API 允許監(jiān)聽和模仿指定事件避矢〉狂看一下簡(jiǎn)單的例子:
setTimeout(function() {
return page.evaluate(function() {
//Get what you want from the page using jQuery.
//A good way is to populate an object with all the jQuery commands that you need and then return the object.
var h2Arr = [], //array that holds all html for h2 elements
pArr = []; //array that holds all html for p elements
//Populate the two arrays
$('h2').each(function() {
h2Arr.push($(this).html());
});
$('p').each(function() {
pArr.push($(this).html());
});
//Return this data
return {
h2: h2Arr,
p: pArr
}
}, function(result) {
console.log(result); //Log out the data.
ph.exit();
});
}, 5000);
全部放在一起后,我們的 phantomServer.js
看起來(lái)會(huì)像這樣:
var phantom = require('phantom');
phantom.create(function(ph) {
return ph.createPage(function(page) {
return page.open("http://tilomitra.com/repository/screenscrape/ajax.html", function(status) {
console.log("opened site? ", status);
page.injectJs('http://ajax.googleapis.com/ajax/libs/jquery/1.7.2/jquery.min.js', function() {
//jQuery Loaded.
//Wait for a bit for AJAX content to load on the page. Here, we are waiting 5 seconds.
setTimeout(function() {
return page.evaluate(function() {
//Get what you want from the page using jQuery. A good way is to populate an object with all the jQuery commands that you need and then return the object.
var h2Arr = [],
pArr = [];
$('h2').each(function() {
h2Arr.push($(this).html());
});
$('p').each(function() {
pArr.push($(this).html());
});
return {
h2: h2Arr,
p: pArr
};
}, function(result) {
console.log(result);
ph.exit();
});
}, 5000);
});
});
});
});
這個(gè)實(shí)現(xiàn)有一些粗糙审胸、無(wú)組織性亥宿,但重點(diǎn)找到了。使用 PhantomJS砂沛,能夠抓取具有動(dòng)態(tài)內(nèi)容的頁(yè)面烫扼!控制臺(tái)應(yīng)輸出以下內(nèi)容:
→ node phantomServer.js
opened site? success
{ h2: [ 'Article 1', 'Article 2', 'Article 3' ],
p:
[ 'Lorem ipsum dolor sit amet, consectetur adipiscing elit.',
'Ut sed nulla turpis, in faucibus ante. Vivamus ut malesuada est. Curabitur vel enim eget purus pharetra tempor id in tellus.',
'Curabitur euismod hendrerit quam ut euismod. Ut leo sem, viverra nec gravida nec, tristique nec arcu.' ] }
總結(jié)
在本教程中講了實(shí)現(xiàn)網(wǎng)絡(luò)爬蟲的兩種不同方式。抓取靜態(tài)網(wǎng)頁(yè)可以用 YQL碍庵,很容易設(shè)置和使用映企。另一方面,對(duì)于動(dòng)態(tài)站點(diǎn)可以用 PhantomJS怎抛。設(shè)置起來(lái)更麻煩卑吭,但提供更多功能。記茁砭:也可以使用PhantomJS 抓取靜態(tài)網(wǎng)站豆赏!
如果你對(duì)這個(gè)話題有任何疑問(wèn),可以在下面隨時(shí)詢問(wèn)富稻,我會(huì)盡我所能幫助你掷邦。