Node.js 爬蟲

你可能會把 NodeJS 用作網(wǎng)絡服務器,但你知道它還可以用來做爬蟲嗎? 本教程中會介紹如何爬取靜態(tài)網(wǎng)頁——還有那些煩人的動態(tài)網(wǎng)頁——使用 NodeJS 和幾個有幫助的 NPM 模塊。

網(wǎng)絡爬蟲的一點知識

網(wǎng)絡爬蟲在網(wǎng)絡編程世界中總是被鄙視——說的也很有道理。在現(xiàn)代編程中,API 用于大多數(shù)流行的服務,應該用它們來獲取數(shù)據(jù),而不是用爬蟲。爬蟲有一個固有問題,就是它依賴于被爬取頁面的可視化結構。一旦 HTML 改變了——不管改變多么微小——都有可能完全破壞之前的代碼。

忽略這些瑕疵,學習一點關于網(wǎng)絡爬蟲的知識會很有幫助,一些工具可以幫我們完成這個任務。當一個網(wǎng)站沒有給 API 或任何聚合訂閱(RSS/Atom等)時,獲取內容只剩唯一的選項……爬蟲。

注意:如果無法通過 API 或訂閱獲得想要的信息,這很有可能表示擁有者不希望那些信息是可訪問的。但是,還有一些例外。

為什么用 NodeJS?

用所有語言都可以寫爬蟲,真的。我喜歡用 Node 的原因是因為它的異步特性,表示在進程中我的代碼任何時候都不會被阻塞。還有一個額外的優(yōu)勢,就是我很熟悉 JavaScript。最后,有一些為 NodeJS 寫的新模塊可以幫助輕松爬取網(wǎng)頁,用一種可靠的方式(好吧,其實就是爬蟲的可靠性極限?。?。開始吧。

用 YQL 實現(xiàn)簡單爬蟲

從簡單的使用場景開始:靜態(tài)網(wǎng)頁。這些是標準的工場網(wǎng)頁。對于這些,Yahoo! Query Language(YQL)可以很好的完成。對于不熟悉 YQL 的人,它就是一個類似 SQL 的語法,可以用來以一致的方式使用不同的API。

YQL 有一些很棒的表來幫助開發(fā)者獲取網(wǎng)頁的 HTML。我想強調的是:

挨個看一下,看如何用 NodeJS 實現(xiàn)。

html/ table

html 表是從 URL 爬取 HTML 最基本的方式。用這個表實現(xiàn)的常規(guī)查詢如下:

select * from html where url="http://finance.yahoo.com/q?s=yhoo" and xpath='//div[@id="yfi_headlines"]/div[2]/ul/li/a'

這個查詢由兩個參數(shù)組成:“url” 和 “xpath”。網(wǎng)址大家都知道。XPath 包含一個 XPath 字符串,告訴 YQL 應該返回 HTML 的哪一部分。在這里查詢一下試試。

還有一些可用的參數(shù)包括 browser (布爾型),charset(字符串)和 compat(字符串)。我沒有使用這些參數(shù),但如果你有特別需要的話可以參考文檔。

XPath 感覺不舒服?

很不幸,XPath 不是一個獲取 HTML 屬性結構的常用方式。對于新手讀和寫都可能很復雜。
看看下一個表,可以完成同樣的事,但使用 CSS 做替代

data.html.cssselect

data.html.cssselect 表是我推薦的爬取頁面 HTML 方式。和 html 表用相同的方式工作,但可以用 CSS 替代 XPath。實際上,這個表默默把 CSS 轉換為 XPath,然后調用 html 表,所以會有一點慢。對于爬取網(wǎng)頁來說,區(qū)別可以忽略不計。

使用這個表的通常方式是:

select * from data.html.cssselect where url="www.yahoo.com" and css="#news a"

可以看到,整潔許多。我建議在嘗試用 YQL 爬取網(wǎng)頁的時候優(yōu)先嘗試這個方法。 在這里查詢一下試試。

* htmlstring* 表

htmlstring 表在嘗試從網(wǎng)頁爬取大量格式化文本的時候用。
用這個表可以用一個單獨的字符串抓取網(wǎng)頁的全部 HTML 內容,而不是基于 DOM 結構切分的 JSON。
例如,一個爬取 <a> 標簽的常規(guī) JSON 返回:

"results": {
   "a": {
     "href": "...",
     "target": "_blank",
     "content": "Apple Chief Executive Cook To Climb on a New Stage"
    }
 }

看到 attribute 如何定義為 property 了吧?相反,htmlstring 表的返回看起來會像這樣:

"results": {
  "result": {
    "<a href=\"…\" target="_blank">Apple Chief Executive Cook To Climb on a New Stage</a>
   }
}

所以,為什么要這么用呢?從我的經(jī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 表,可以把這個 HTML 獲取為字符串,然后用正則移除 HTML 標簽,留下的就只有文本了。這比 JSON 根據(jù)頁面的 DOM 結構分為屬性和子對象的迭代更容易。

在 NodeJS 里用 YQL

現(xiàn)在我們了解了一些 YQL 中可用的表,讓我們用 YQL 和 NodeJS 實現(xiàn)一個網(wǎng)絡爬蟲。幸運的是,相當簡單,感謝 Derek Gathright 寫的 node-yql 模塊。

可以用 npm 安裝它:

npm install yql

這個模塊極為簡單,只包括一個方法:YQL.exec() 方法。定義如下:

function exec (string query [, function callback] [, object params] [, object httpOptions])

我們 require 它然后調用 YQL.exec() 就可以用了。例如,假設要抓取 Nettuts 主頁所有文章的標題:

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 最棒的就是能夠實時測試查詢然后確定會返回的 JSON。去 console 用一下試試,或者點擊這里查看原生 JSON。

paramshttpOptions 對象是可選的。參數(shù)可以包括像 env(是否為表使用特定的環(huán)境) 和 format (xml 或 json)這樣的屬性。所有傳給 params 的屬性都是 URI 編碼然后附到查詢字符串的尾端。httpOptions 對象被傳遞到請求頭中。例如這里你可以指定是否想啟用 SSL。

叫做 yqlServer.js 的 JavaScript 文件,包含使用 YQL 爬取所需的最少代碼??梢栽诮K端里用以下命令來運行它:

node yqlServer.js

例外情況和其它知名工具

YQL 是我推薦的爬取靜態(tài)網(wǎng)頁內容的選擇,因為讀起來簡單、用起來也簡單。然而,如果網(wǎng)頁有 robots.txt 文件來拒絕響應,YQL 就會失敗。在這種情況下,可以看看下面提到的工具,或者用下一節(jié)會講的 PhantomJS。

Node.io 是一個實用的 Node 工具,為數(shù)據(jù)爬取而特別設計??梢詣?chuàng)建接受輸入,處理并返回某些輸出的作業(yè)。Node.io 在 GitHub 上關注量很高,有一些實用的例子幫你上手。

JSDOM 是一個很流行的項目,用 JavaScript 實現(xiàn)了 W3C DOM。當提供 HTML 時,它可以構造一個能夠與之交互的 DOM。查看文檔,了解如何使用 JSDOM 和任意 JS 庫(如 jQuery )一起從網(wǎng)頁抓取數(shù)據(jù)。

從頁面抓取動態(tài)內容

到目前為止,我們已經(jīng)看過一些工具,可以幫助我們抓取靜態(tài)內容的網(wǎng)頁。有了YQL,相當簡單。不幸的是,我們經(jīng)??吹揭恍﹥热菔怯肑avaScript動態(tài)加載的頁面。在這些情況下,頁面最初通常為空,然后隨后附加內容。如何處理這個問題呢?

例子

我提供了一個例子;我上傳了一個簡單的 HTML 文件到我自己的網(wǎng)站,document.ready() 函數(shù)被調用后兩秒通過 JavaScript 附加了一些內容??梢栽?a target="_blank" rel="nofollow">這里查看這個頁面。源文件如下:

<!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);
 
});

你會發(fā)現(xiàn) YQL 返回了 undefined,因為頁面被加載后,<div id=“content”> 是空的。內容還沒有被附加上去??梢栽?a target="_blank" rel="nofollow">這里自己嘗試一下。

來看看如何解決這個問題!

PhantomJS

PhantomJS 可以加載網(wǎng)頁,并模仿基于 Webkit 的瀏覽器,然而并沒有 GUI。

從這類站點爬取信息我建議的方式是使用 PhantomJS 。PhantomJS 形容自己是“用 JavaScript API 的無用戶界面 Webkit?!昂唵蝸碚f,表示 PhantomJS 可以加載網(wǎng)頁然后模仿基于 Webkit 的瀏覽器,然而并沒有GUI。作為一個開發(fā)者,可以調用 PhantomJS 提供的特定方法在頁面上執(zhí)行代碼。由于它的行為像瀏覽器,網(wǎng)頁上的腳本就像在一個普通的瀏覽器中運行。

為了從我們的頁面獲取數(shù)據(jù),要使用 PhantomJS-Node,這是一個很小的開源項目,它將 PhantomJS 與NodeJS 橋接起來。此模塊默默把 PhantomJS 作為一個子進程運行。

安裝 PhantomJS
在安裝 PhantomJS-Node NPM 模塊之前,必須安裝 PhantomJS。但安裝和構建 PhantomJS 可能有點棘手。

首先,去 PhantomJS.org 并為操作系統(tǒng)下載相應的版本。我是Mac OSX。
下載后,將其解壓到某個位置,例如/ Applications /。接下來,您要將其添加到PATH

sudo ln -s /Applications/phantomjs-1.5.0/bin/phantomjs /usr/local/bin/

1.5.0 替換為你下載的 PhantomJS 版本。請注意,并非所有系統(tǒng)都具有/ usr / local / bin /。一些系統(tǒng)將有:/ usr / bin /,/ bin /usr / X11 / bin

對于 Windows 用戶,看這里的 短篇 教程。如果你打開終端,輸入 phantomjs 并且沒有任何錯誤,就安裝完成了。

如果你不想編輯 PATH,記下你解壓 PhantomJS 的地方,我會在下一節(jié)中展示另一種設置方法,雖然我建議你編輯 PATH。

安裝 PhantomJS-Node

設置 PhantomJS-Node 就簡單多了。如果已經(jīng)安裝了 NodeJS,可以通過 npm 來安裝它:

npm install phantom

如果你在前一節(jié)安裝 PhantomJS 的時候沒有編輯 PATH,可以去 npm pull 下來的 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]));

完成后,可以運行這段代碼進行測試:

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();
      });
    });
  });
});

在命令行運行它應該會有如下輸出:


opened google?  success
Page title is Google

如果正確得到了,就已經(jīng)設置完成。如果沒有,在現(xiàn)在評論一下我會試著幫你解決!

使用 PhantomJS-Node

為了讓你更容易,我已經(jīng)在下載中包含了一個名為 phantomServer.js 的 JS 文件,使用了一些 PhantomJS 的 API 來加載網(wǎng)頁。等待 5 秒后執(zhí)行 JavaScript 來爬取頁面。你可以通過導航到該目錄并在終端中使用以下命令來運行它:

node phantomServer.js

我將概述一下它在這里是如何工作的。首先,我們需要 PhantomJS:

var phantom = require('phantom’);

接下來,利用 API 實現(xiàn)一些方法。也就是說,我們創(chuàng)建一個實例頁面,然后調用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);
 
        });
    });
});

頁面打開后,我們可以注入一些 JavaScript 到頁面上。通過 page.injectJS() 方法來注入 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)在加載好了,但我們不知道頁面上的動態(tài)內容是否加載完畢。為了解決這個問題,我通常會把我的爬蟲代碼放在一個 setTimeout() 函數(shù)中,在特定時間間隔后執(zhí)行。如果你想要一個更靈活的方案,PhantomJS API 允許監(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 看起來會像這樣:

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);
 
            });
    });
    });
});

這個實現(xiàn)有一些粗糙、無組織性,但重點找到了。使用 PhantomJS,能夠抓取具有動態(tà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.' ] }

總結

在本教程中講了實現(xiàn)網(wǎng)絡爬蟲的兩種不同方式。抓取靜態(tài)網(wǎng)頁可以用 YQL,很容易設置和使用。另一方面,對于動態(tài)站點可以用 PhantomJS。設置起來更麻煩,但提供更多功能。記?。阂部梢允褂肞hantomJS 抓取靜態(tài)網(wǎng)站!

如果你對這個話題有任何疑問,可以在下面隨時詢問,我會盡我所能幫助你。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
【社區(qū)內容提示】社區(qū)部分內容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內容

友情鏈接更多精彩內容