如何使用nodejs做爬蟲程序?
https://github.com/alsotang/node-lessons/tree/master/lesson3
之前研究數據,零零散散的寫過一些數據抓取的爬蟲,不過寫的比較隨意。有很多地方現在看起來並不是很合理 這段時間比較閑,本來是想給之前的項目做重構的。
後來 利用這個周末,索性重新寫了一個項目,就是本項目 guwen-spider。目前這個爬蟲還是比較簡單的類型的, 直接抓取頁面,然後在頁面中提取數據,保存數據到資料庫。
通過與之前寫的對比,我覺得難點在於整個程序的健壯性,以及相應的容錯機制。在昨天寫代碼的過程中其實也有反映, 真正的主體代碼其實很快就寫完了 ,花了大部分時間是在
做穩定性的調試, 以及尋求一種更合理的方式來處理數據與流程式控制制的關係。
背景
項目的背景是抓取一個一級頁面是目錄列表 ,點擊一個目錄進去 是一個章節 及篇幅列表 ,點擊章節或篇幅進入具體的內容頁面。
概述
本項目github地址 : [guwen-spider](yangfan0095/guwen-spider) (PS:最後面還有彩蛋 ~~逃
項目技術細節
項目大量用到了 ES7 的async 函數, 更直觀的反應程序了的流程。為了方便,在對數據遍歷的過程中直接使用了著名的async這個庫,所以不可避免的還是用到了回調promise ,因為數據的處理髮生在回調函數中,不可避免的會遇到一些數據傳遞的問題,其實也可以直接用ES7的async await 寫一個方法來實現相同的功能。這裡其實最贊的一個地方是使用了 Class 的 static 方法封裝對資料庫的操作, static 顧名思義 靜態方法 就跟 prototype 一樣 ,不會佔用額外空間。
項目主要用到了
* 1 ES7的 async await 協程做非同步有關的邏輯處理。
* 2 使用 npm的 async庫 來做循環遍歷,以及並發請求操作。
* 3 使用 log4js 來做日誌處理
* 4 使用 cheerio 來處理dom的操作。
* 5 使用 mongoose 來連接mongoDB 做數據的保存以及操作。
目錄結構
├── bin // 入口
│ ├── booklist.js // 抓取書籍邏輯
│ ├── chapterlist.js // 抓取章節邏輯
│ ├── content.js // 抓取內容邏輯
│ └── index.js // 程序入口
├── config // 配置文件
├── dbhelper // 資料庫操作方法目錄
├── logs // 項目日誌目錄
├── model // mongoDB 集合操作實例
├── node_modules
├── utils // 工具函數
├── package.json
項目實現方案分析
項目是一個典型的多級抓取案例,目前只有三級,即 書籍列表, 書籍項對應的 章節列表,一個章節鏈接對應的內容。 抓取這樣的結構可以採用兩種方式, 一是 直接從外層到內層 內層抓取完以後再執行下一個外層的抓取, 還有一種就是先把外層抓取完成保存到資料庫,然後根據外層抓取到所有內層章節的鏈接,再次保存,然後從資料庫查詢到對應的鏈接單元 對之進行內容抓取。這兩種方案各有利弊,其實兩種方式我都試過, 後者有一個好處,因為對三個層級是分開抓取的, 這樣就能夠更方便,儘可能多的保存到對應章節的相關數據。 可以試想一下 ,如果採用前者 按照正常的邏輯
對一級目錄進行遍歷抓取到對應的二級章節目錄, 再對章節列表進行遍歷 抓取內容,到第三級 內容單元抓取完成 需要保存時,如果需要很多的一級目錄信息,就需要 這些分層的數據之間進行數據傳遞 ,想想其實應該是比較複雜的一件事情。所以分開保存數據 一定程度上避開了不必要的複雜的數據傳遞。
目前我們考慮到 其實我們要抓取到的古文書籍數量並不多,古文書籍大概只有180本囊括了各種經史。其和章節內容本身是一個很小的數據 ,即一個集合裡面有180個文檔記錄。 這180本書所有章節抓取下來一共有一萬六千個章節,對應需要訪問一萬六千個頁面爬取到對應的內容。所以選擇第二種應該是合理的。
項目實現
主程有三個方法 bookListInit ,chapterListInit,contentListInit, 分別是抓取書籍目錄,章節列表,書籍內容的方法對外公開暴露的初始化方法。通過async 可以實現對這三個方法的運行流程進行控制,書籍目錄抓取完成將數據保存到資料庫,然後執行結果返回到主程序,如果運行成功 主程序則執行根據書籍列表對章節列表的抓取,同理對書籍內容進行抓取。
項目主入口
/**
* 爬蟲抓取主入口
*/
const start = async() =&> {
let booklistRes = await bookListInit();
if (!booklistRes) {
logger.warn("書籍列表抓取出錯,程序終止...");
return;
}
logger.info("書籍列表抓取成功,現在進行書籍章節抓取...");
let chapterlistRes = await chapterListInit();
if (!chapterlistRes) {
logger.warn("書籍章節列表抓取出錯,程序終止...");
return;
}
logger.info("書籍章節列表抓取成功,現在進行書籍內容抓取...");
let contentListRes = await contentListInit();
if (!contentListRes) {
logger.warn("書籍章節內容抓取出錯,程序終止...");
return;
}
logger.info("書籍內容抓取成功");
}
// 開始入口
if (typeof bookListInit === "function" typeof chapterListInit === "function") {
// 開始抓取
start();
}
引入的 bookListInit ,chapterListInit,contentListInit, 三個方法
booklist.js
/**
* 初始化方法 返回抓取結果 true 抓取成果 false 抓取失敗
*/
const bookListInit = async() =&> {
logger.info("抓取書籍列表開始...");
const pageUrlList = getPageUrlList(totalListPage, baseUrl);
let res = await getBookList(pageUrlList);
return res;
}
chapterlist.js
/**
* 初始化入口
*/
const chapterListInit = async() =&> {
const list = await bookHelper.getBookList(bookListModel);
if (!list) {
logger.error("初始化查詢書籍目錄失敗");
}
logger.info("開始抓取書籍章節列表,書籍目錄共:" + list.length + "條");
let res = await asyncGetChapter(list);
return res;
};
content.js
/**
* 初始化入口
*/
const contentListInit = async() =&> {
//獲取書籍列表
const list = await bookHelper.getBookLi(bookListModel);
if (!list) {
logger.error("初始化查詢書籍目錄失敗");
return;
}
const res = await mapBookList(list);
if (!res) {
logger.error("抓取章節信息,調用 getCurBookSectionList() 進行串列遍歷操作,執行完成回調出錯,錯誤信息已列印,請查看日誌!");
return;
}
return res;
}
內容抓取的思考
書籍目錄抓取其實邏輯非常簡單,只需要使用async.mapLimit做一個遍歷就可以保存數據了,但是我們在保存內容的時候 簡化的邏輯其實就是 遍歷章節列表 抓取鏈接里的內容。但是實際的情況是鏈接數量多達幾萬 我們從內存佔用角度也不能全部保存到一個數組中,然後對其遍歷,所以我們需要對內容抓取進行單元化。
普遍的遍歷方式 是每次查詢一定的數量,來做抓取,這樣缺點是只是以一定數量做分類,數據之間沒有關聯,以批量方式進行插入,如果出錯 則容錯會有一些小問題,而且我們想一本書作為一個集合單獨保存會遇到問題。因此我們採用第二種就是以一個書籍單元進行內容抓取和保存。
這裡使用了 `async.mapLimit(list, 1, (series, callback) =&> {})`這個方法來進行遍歷,不可避免的用到了回調,感覺很噁心。async.mapLimit()的第二個參數可以設置同時請求數量。
/*
* 內容抓取步驟:
* 第一步得到書籍列表, 通過書籍列表查到一條書籍記錄下 對應的所有章節列表,
* 第二步 對章節列表進行遍歷獲取內容保存到資料庫中
* 第三步 保存完數據後 回到第一步 進行下一步書籍的內容抓取和保存
*/
/**
* 初始化入口
*/
const contentListInit = async() =&> {
//獲取書籍列表
const list = await bookHelper.getBookList(bookListModel);
if (!list) {
logger.error("初始化查詢書籍目錄失敗");
return;
}
const res = await mapBookList(list);
if (!res) {
logger.error("抓取章節信息,調用 getCurBookSectionList() 進行串列遍歷操作,執行完成回調出錯,錯誤信息已列印,請查看日誌!");
return;
}
return res;
}
/**
* 遍曆書籍目錄下的章節列表
* @param {*} list
*/
const mapBookList = (list) =&> {
return new Promise((resolve, reject) =&> {
async.mapLimit(list, 1, (series, callback) =&> {
let doc = series._doc;
getCurBookSectionList(doc, callback);
}, (err, result) =&> {
if (err) {
logger.error("書籍目錄抓取非同步執行出錯!");
logger.error(err);
reject(false); return;
}
resolve(true);
})
})
}
/**
* 獲取單本書籍下章節列表 調用章節列表遍歷進行抓取內容
* @param {*} series
* @param {*} callback
*/
const getCurBookSectionList = async(series, callback) =&> {
let num = Math.random() * 1000 + 1000;
await sleep(num);
let key = series.key;
const res = await bookHelper.querySectionList(chapterListModel, {
key: key
});
if (!res) {
logger.error("獲取當前書籍: " + series.bookName + " 章節內容失敗,進入下一部書籍內容抓取!");
callback(null, null);
return;
}
//判斷當前數據是否已經存在
const bookItemModel = getModel(key);
const contentLength = await bookHelper.getCollectionLength(bookItemModel, {});
if (contentLength === res.length) {
logger.info("當前書籍:" + series.bookName + "資料庫已經抓取完成,進入下一條數據任務");
callback(null, null);
return;
}
await mapSectionList(res);
callback(null, null);
}
數據抓取完了 怎麼保存是個問題
這裡我們通過key 來給數據做分類,每次按照key來獲取鏈接,進行遍歷,這樣的好處是保存的數據是一個整體,現在思考數據保存的問題
1 可以以整體的方式進行插入
優點 : 速度快 資料庫操作不浪費時間。
缺點 : 有的書籍可能有幾百個章節 也就意味著要先保存幾百個頁面的內容再進行插入,這樣做同樣很消耗內存,有可能造成程序運行不穩定。
2可以以每一篇文章的形式插入資料庫。
優點 : 頁面抓取即保存的方式 使得數據能夠及時保存,即使後續出錯也不需要重新保存前面的章節,
缺點 : 也很明顯 就是慢 ,仔細想想如果要爬幾萬個頁面 做 幾萬次*N 資料庫的操作 這裡還可以做一個緩存器一次性保存一定條數 當條數達到再做保存這樣也是一個不錯的選擇。
/**
* 遍歷單條書籍下所有章節 調用內容抓取方法
* @param {*} list
*/
const mapSectionList = (list) =&> {
return new Promise((resolve, reject) =&> {
async.mapLimit(list, 1, (series, callback) =&> {
let doc = series._doc;
getContent(doc, callback)
}, (err, result) =&> {
if (err) {
logger.error("書籍目錄抓取非同步執行出錯!");
logger.error(err);
reject(false);
return;
}
const bookName = list[0].bookName;
const key = list[0].key;
// 以整體為單元進行保存
saveAllContentToDB(result, bookName, key, resolve);
//以每篇文章作為單元進行保存
// logger.info(bookName + "數據抓取完成,進入下一部書籍抓取函數...");
// resolve(true);
})
})
}
兩者各有利弊,這裡我們都做了嘗試。 準備了兩個錯誤保存的集合,errContentModel, errorCollectionModel,在插入出錯時 分別保存信息到對應的集合中,二者任選其一即可。增加集合來保存數據的原因是 便於一次性查看以及後續操作, 不用看日誌。
(PS ,其實完全用 errorCollectionModel 這個集合就可以了 ,errContentModel這個集合可以完整保存章節信息)
//保存出錯的數據名稱
const errorSpider = mongoose.Schema({
chapter: String,
section: String,
url: String,
key: String,
bookName: String,
author: String,
})
// 保存出錯的數據名稱 只保留key 和 bookName信息
const errorCollection = mongoose.Schema({
key: String,
bookName: String,
})
我們將每一條書籍信息的內容 放到一個新的集合中,集合以key來進行命名。
總結
寫這個項目 其實主要的難點在於程序穩定性的控制,容錯機制的設置,以及錯誤的記錄,目前這個項目基本能夠實現直接運行 一次性跑通整個流程。 但是程序設計也肯定還存在許多問題 ,歡迎指正和交流。
彩蛋
寫完這個項目 做了一個基於React開的前端網站用於頁面瀏覽 和一個基於koa2.x開發的服務端, 整體技術棧相當於是 React + Redux + Koa2 ,前後端服務是分開部署的,各自獨立可以更好的去除前後端服務的耦合性,比如同一套服務端代碼,不僅可以給web端 還可以給 移動端 ,app 提供支持。目前整個一套還很簡陋,但是可以滿足基本的查詢瀏覽功能。希望後期有時間可以把項目變得更加豐富。
本項目地址 地址 : [guwen-spider](yangfan0095/guwen-spider)
對應前端 React + Redux + semantic-ui 地址 : [guwen-react](yangfan0095/guwen-react)
對應Node端 Koa2.2 + mongoose 地址 : [guwen-node](yangfan0095/guwen-node)
項目挺簡單的 ,但是多了一個學習和研究 從前端到服務端的開發的環境。
感謝閱讀!
以上です
最近寫了個嘀哩嘀哩的爬蟲
GitHub - Relsoul/dilidiliNode
代碼都很簡單 歡迎star
你可能會把 NodeJS 用作網路伺服器,但你知道它還可以用來做爬蟲嗎? 本教程中會介紹如何爬取靜態網頁——還有那些煩人的動態網頁——使用 NodeJS 和幾個有幫助的 NPM 模塊。
網路爬蟲的一點知識
網路爬蟲在網路編程世界中總是被鄙視——說的也很有道理。在現代編程中,API 用於大多數流行的服務,應該用它們來獲取數據,而不是用爬蟲。爬蟲有一個固有問題,就是它依賴於被爬取頁面的可視化結構。一旦 HTML 改變了——不管改變多麼微小——都有可能完全破壞之前的代碼。
忽略這些瑕疵,學習一點關於網路爬蟲的知識會很有幫助,一些工具可以幫我們完成這個任務。當一個網站沒有給 API 或任何聚合訂閱(RSS/Atom等)時,獲取內容只剩唯一的選項……爬蟲。
注意:如果無法通過 API 或訂閱獲得想要的信息,這很有可能表示擁有者不希望那些信息是可訪問的。但是,還有一些例外。
為什麼用 NodeJS?
用所有語言都可以寫爬蟲,真的。我喜歡用 Node 的原因是因為它的非同步特性,表示在進程中我的代碼任何時候都不會被阻塞。還有一個額外的優勢,就是我很熟悉 JavaScript。最後,有一些為 NodeJS 寫的新模塊可以幫助輕鬆爬取網頁,用一種可靠的方式(好吧,其實就是爬蟲的可靠性極限!)。開始吧。
用 YQL 實現簡單爬蟲
從簡單的使用場景開始:靜態網頁。這些是標準的工場網頁。對於這些,Yahoo! Query Language(YQL)可以很好的完成。對於不熟悉 YQL 的人,它就是一個類似 SQL 的語法,可以用來以一致的方式使用不同的API。
YQL 有一些很棒的表來幫助開發者獲取網頁的 HTML。我想強調的是:
- html
- data.html.cssselect
- htmlstring
挨個看一下,看如何用 NodeJS 實現。
html/ table
html 表是從 URL 爬取 HTML 最基本的方式。用這個表實現的常規查詢如下:
select * from html where url="http://finance.yahoo.com/q?s=yhoo" and xpath="//div[@id="yfi_headlines"]/div[2]/ul/li/a"
這個查詢由兩個參數組成:「url」 和 「xpath」。網址大家都知道。XPath 包含一個 XPath 字元串,告訴 YQL 應該返回 HTML 的哪一部分。在這裡查詢一下試試。
還有一些可用的參數包括 browser (布爾型),charset(字元串)和 compat(字元串)。我沒有使用這些參數,但如果你有特別需要的話可以參考文檔。
XPath 感覺不舒服?
很不幸,XPath 不是一個獲取 HTML 屬性結構的常用方式。對於新手讀和寫都可能很複雜。
看看下一個表,可以完成同樣的事,但使用 CSS 做替代
data.html.cssselect 表
data.html.cssselect 表是我推薦的爬取頁面 HTML 方式。和 html 表用相同的方式工作,但可以用 CSS 替代 XPath。實際上,這個表默默把 CSS 轉換為 XPath,然後調用 html 表,所以會有一點慢。對於爬取網頁來說,區別可以忽略不計。
使用這個表的通常方式是:
select * from data.html.cssselect where url="www.yahoo.com" and css="#news a"
可以看到,整潔許多。我建議在嘗試用 YQL 爬取網頁的時候優先嘗試這個方法。 在這裡查詢一下試試。
htmlstring 表
htmlstring 表在嘗試從網頁爬取大量格式化文本的時候用。
用這個表可以用一個單獨的字元串抓取網頁的全部 HTML 內容,而不是基於 DOM 結構切分的 JSON。
例如,一個爬取 & 標籤的常規 JSON 返回:
"results": {
"a": {
"href": "...",
"target": "_blank",
"content": "Apple Chief Executive Cook To Climb on a New Stage"
}
}
看到 attribute 如何定義為 property 了吧?相反,htmlstring 表的返回看起來會像這樣:
"results": {
"result": {
"&Apple Chief Executive Cook To Climb on a New Stage&
}
}
所以,為什麼要這麼用呢?從我的經驗來看,嘗試爬取大量格式化文本的時候會相當有用。例如下面的片段:
Lorem ipsum &dolor sit amet&, consectetur adipiscing elit.& Proin nec diam magna. Sed non lorem a nisi porttitor pharetra et non arcu.&&
使用 htmlstring 表,可以把這個 HTML 獲取為字元串,然後用正則移除 HTML 標籤,留下的就只有文本了。這比 JSON 根據頁面的 DOM 結構分為屬性和子對象的迭代更容易。
在 NodeJS 里用 YQL
現在我們了解了一些 YQL 中可用的表,讓我們用 YQL 和 NodeJS 實現一個網路爬蟲。幸運的是,相當簡單,感謝 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。
params 和 httpOptions 對象是可選的。參數可以包括像 env(是否為表使用特定的環境) 和 format (xml 或 json)這樣的屬性。所有傳給 params 的屬性都是 URI 編碼然後附到查詢字元串的尾端。httpOptions 對象被傳遞到請求頭中。例如這裡你可以指定是否想啟用 SSL。
叫做 yqlServer.js 的 JavaScript 文件,包含使用 YQL 爬取所需的最少代碼。可以在終端里用以下命令來運行它:
node yqlServer.js
例外情況和其它知名工具
YQL 是我推薦的爬取靜態網頁內容的選擇,因為讀起來簡單、用起來也簡單。然而,如果網頁有 robots.txt 文件來拒絕響應,YQL 就會失敗。在這種情況下,可以看看下面提到的工具,或者用下一節會講的 PhantomJS。
Node.io 是一個實用的 Node 工具,為數據爬取而特別設計。可以創建接受輸入,處理並返回某些輸出的作業。Node.io 在 GitHub 上關注量很高,有一些實用的例子幫你上手。
JSDOM 是一個很流行的項目,用 JavaScript 實現了 W3C DOM。當提供 HTML 時,它可以構造一個能夠與之交互的 DOM。查看文檔,了解如何使用 JSDOM 和任意 JS 庫(如 jQuery )一起從網頁抓取數據。
從頁面抓取動態內容
到目前為止,我們已經看過一些工具,可以幫助我們抓取靜態內容的網頁。有了YQL,相當簡單。不幸的是,我們經常看到一些內容是用JavaScript動態載入的頁面。在這些情況下,頁面最初通常為空,然後隨後附加內容。如何處理這個問題呢?
例子
我提供了一個例子;我上傳了一個簡單的 HTML 文件到我自己的網站,document.ready() 函數被調用後兩秒通過 JavaScript 附加了一些內容。可以在這裡查看這個頁面。源文件如下:
&
&
&
&
&
&
Content on this page is appended to the DOM after the page is loaded.
&
&
&
&
&
&
現在嘗試用 YQL 從 & 中抓取文本。
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);
});
你會發現 YQL 返回了 undefined,因為頁面被載入後,& 是空的。內容還沒有被附加上去。可以在這裡自己嘗試一下。
來看看如何解決這個問題!
PhantomJS
PhantomJS 可以載入網頁,並模仿基於 Webkit 的瀏覽器,然而並沒有 GUI。
從這類站點爬取信息我建議的方式是使用 PhantomJS 。PhantomJS 形容自己是「用 JavaScript API 的無用戶界面 Webkit。「簡單來說,表示 PhantomJS 可以載入網頁然後模仿基於 Webkit 的瀏覽器,然而並沒有GUI。作為一個開發者,可以調用 PhantomJS 提供的特定方法在頁面上執行代碼。由於它的行為像瀏覽器,網頁上的腳本就像在一個普通的瀏覽器中運行。
為了從我們的頁面獲取數據,要使用 PhantomJS-Node,這是一個很小的開源項目,它將 PhantomJS 與NodeJS 橋接起來。此模塊默默把 PhantomJS 作為一個子進程運行。
安裝 PhantomJS
在安裝 PhantomJS-Node NPM 模塊之前,必須安裝 PhantomJS。但安裝和構建 PhantomJS 可能有點棘手。
首先,去 http://PhantomJS.org 並為操作系統下載相應的版本。我是Mac OSX。
下載後,將其解壓到某個位置,例如/ Applications /。接下來,您要將其添加到PATH:
sudo ln -s /Applications/phantomjs-1.5.0/bin/phantomjs /usr/local/bin/
把 1.5.0 替換為你下載的 PhantomJS 版本。請注意,並非所有系統都具有/ usr / local / bin /。一些系統將有:/ usr / bin /,/ bin /或usr / X11 / bin。
對於 Windows 用戶,看這裡的 短篇 教程。如果你打開終端,輸入 phantomjs 並且沒有任何錯誤,就安裝完成了。
如果你不想編輯 PATH,記下你解壓 PhantomJS 的地方,我會在下一節中展示另一種設置方法,雖然我建議你編輯 PATH。
安裝 PhantomJS-Node
設置 PhantomJS-Node 就簡單多了。如果已經安裝了 NodeJS,可以通過 npm 來安裝它:
npm install phantom
如果你在前一節安裝 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
如果正確得到了,就已經設置完成。如果沒有,在現在評論一下我會試著幫你解決!
使用 PhantomJS-Node
為了讓你更容易,我已經在下載中包含了一個名為 phantomServer.js 的 JS 文件,使用了一些 PhantomJS 的 API 來載入網頁。等待 5 秒後執行 JavaScript 來爬取頁面。你可以通過導航到該目錄並在終端中使用以下命令來運行它:
node phantomServer.js
我將概述一下它在這裡是如何工作的。首先,我們需要 PhantomJS:
var phantom = require("phantom』);
接下來,利用 API 實現一些方法。也就是說,我們創建一個實例頁面,然後調用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 現在載入好了,但我們不知道頁面上的動態內容是否載入完畢。為了解決這個問題,我通常會把我的爬蟲代碼放在一個 setTimeout() 函數中,在特定時間間隔後執行。如果你想要一個更靈活的方案,PhantomJS API 允許監聽和模仿指定事件。看一下簡單的例子:
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);
});
});
});
});
這個實現有一些粗糙、無組織性,但重點找到了。使用 PhantomJS,能夠抓取具有動態內容的頁面!控制台應輸出以下內容:
→ 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." ] }
總結
在本教程中講了實現網路爬蟲的兩種不同方式。抓取靜態網頁可以用 YQL,很容易設置和使用。另一方面,對於動態站點可以用 PhantomJS。設置起來更麻煩,但提供更多功能。記住:也可以使用PhantomJS 抓取靜態網站!
如果你對這個話題有任何疑問,可以在下面隨時詢問,我會盡我所能幫助你。
翻了這麼多問題終於看到一個nodejs相關的,看到有回答說事件驅動型的nodejs不適合開發爬蟲,我真是哭笑不得。
首先樓主要使用nodejs作為語言或者平台來開發爬蟲是沒有任何問題的,反而nodejs的event io能夠讓你感覺到網路編程可以如此酣暢淋漓,一如http://nodejs.org 官網給出的http server 的例子,幾行代碼就能實現web服務,沒有apache 沒有tomcat也沒有iis,只要設置一個回調,監聽一個埠,你就可以使用瀏覽器訪問了。
其次,爬蟲是最明顯的IO密集型應用場景,顯然和擅長處理IO的nodejs是絕配,但是把它用好是需要一些功底的。如果你想要從頭開發,不藉助第三方框架,那麼你需要對nodejs中的event emitter 及 http 模塊有深入的了解,加上你的HTTP協議的知識,再到node平台中找到相應的實現方案,從而實現你要的功能。舉個例子,使用http模塊發送一個GET請求到百度,你需要弄清楚對req,也就是http.get的返回值,監聽什麼樣的事件,http的頭部信息和正文信息分別應該在什麼階段獲取。在python中方法很直白,簡簡單單的用urllib來open一個鏈接,接著read就算完事兒,寫到這裡突然感覺可以理解之前那位仁兄認為的"不適合",也許是覺得node 不如python的直白,又或許只是對node的理解不夠到位,不好拿捏?
最後,如果樓主想要把node作為平台來開發爬蟲,我建議直接從開源框架入手,立刻做出一些可用的程序來給自己一些正反饋,然後再慢慢學習體會。以nodejs編寫的開源爬蟲最有名的莫過於node-crawler,送上地址,準備好可樂。https://github.com/bda-research/node-crawler
最近剛用Nodejs寫了一個爬取西貝的小爬蟲,供參考 Nodejs爬蟲實踐小記
後端渲染的頁面用cheerio這個模塊爬就可以,具體如何爬可以去谷歌。如果是js渲染的,比如百度圖片,就用phantomjs去爬,用phantomjs爬的話需要在phantomjs這個看不見的瀏覽器里進行瀏覽器操作,獲得相應數據後,再通過node的介面傳給node
request即可~
百度node爬蟲下載圖片,有個爬美女圖片的那個就是一個不錯的例子
簡書的簡單例子 touch Me!
可以先玩下這個,百度貼吧爬蟲
tyaqing/baidu_tieba_crawler
可以多進程爬取,用了mongo,socket。
前端用了vue。
用TypeScript開發爬蟲程序
如今計算機都這麼便宜了,普通的爬蟲用 greasemonkey 敲敲就搞定了。
採集來的數據扔到 nodejs 後台裡面去煉化
試試神箭手雲爬蟲
Atitit 爬蟲 node版 attilax
1.1. 貌似不跟python壓實,,java的webmagic壓實,,什麼爬蟲框架也沒有,只好自己寫了。
查了百度三爺資料也沒有。都是自己寫。。
1.2. 爬蟲演算法 調用http模塊獲取內容。
算去時間戳作為文件名tag部分。。
調用Io模塊,保存為文件
1.3. C: workspaceatiplat_spidercom.attilaxspiderorgSpd.js
/**
* Created by Administrator on 2017/1/14.
*/
var sys = require("util");
var fs = require("fs");
sys.log("Hello world");
var https = require("http");
function getUrls()
{
var urls=new Array();
for(var i=1;i&<100;i++)
{
var reqdata_str="orgName=corporateType=1managerDeptCode=?istrationNo=unifiedCode=order=registrationDatelegalName=page_flag=truepagesize_key=resultgoto_page=next¤t_page=@page@total_count=1938to_page=";
reqdata_str=reqdata_str.replace("@page@",i);
urls.push(reqdata_str );
}
return urls;
}
//main
var urls=getUrls();
var cnt=0;
for(idx in urls)
{
var url=urls[idx];
console.log("url:"+url);
cnt++;
req(url,cnt);
}
function req(reqdata_str,page)
{
var reqData={
order:"registrationDate",
corporateType:"1",
current_page:"4",
page_flag:true,
pagesize_key:"result",
goto_page:"next",
total_count:1938,
orgName:"",
managerDeptCode:"",
registrationNo:"",
unifiedCode:"",
legalName:"",
to_page:""
};
var data_str= JSON.stringify (reqData);
sys.log("--reqData.length:"+data_str.length);
//var reqdata_str="orgName=corporateType=1managerDeptCode=?istrationNo=unifiedCode=order=registrationDatelegalName=page_flag=truepagesize_key=resultgoto_page=next¤t_page=3total_count=1938to_page=";
var post_options = {
host: "http://www.chinanpo.gov.cn",
port: "80",
path: "/search/searchOrgList.do?action=searchOrgList",
method: "POST",
headers: {
"Content-Type": "application/x-www-form-urlencoded",
"Content-Length":reqdata_str.length,
"Origin":"http://www.chinanpo.gov.cn",
"Referer":"http://www.chinanpo.gov.cn/search/searchOrgList.do?action=searchOrgList",
"User-Agent":"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/47.0.2526.106 Safari/537.36",
"Upgrade-Insecure-Requests":1
// "Cookie":"chinanpojsessionid=C5B432F4A8100CFA5803EA018A8B4AEC; _gscu_815174165=84382222mstxh112; _gscs_815174165=t84402470rmeenn15|pv:2; _gscbrs_815174165=1; Hm_lvt_3adce665674fbfb5552846b40f1c3cbc=1484382201; Hm_lpvt_3adce665674fbfb5552846b40f1c3cbc=1484403650"
}
}; //poost opt end
var post_req_ClientRequest=https.request(post_options, function(res) {
console.log("statusCode: ", res.statusCode);
// console.log("headers: ", res.headers);
var html = "";
res.on("data", function(d) {
// process.stdout.write(d);
html += d;
});
res.on("end", function(data) {
save(html,page);
})
}).on("error", function(e) {
console.error(e);
});
// post the data
post_req_ClientRequest.write(reqdata_str);
//https.write(reqData);
post_req_ClientRequest.end();
}
function getTimestamp()
{
var timestamp = (new Date()).valueOf();
return timestamp;
}
function save(html,page)
{
console.log("start save!")
// ????????????浽?????????
fs.writeFile("c:\00orgSave\index_P"+page+"_"+getTimestamp()+".html", html, function(err) {
if (err) {
console.log("???????!")
}
console.log("???????index.html??")
})
}
作者:: 綽號:老哇的爪子claw of Eagle 偶像破壞者Iconoclast image-smasher
捕鳥王"Bird Catcher 王中之王King of Kings 虔誠者Pious 宗教信仰捍衛者 Defender Of the Faith. 卡拉卡拉紅斗篷 Caracalla red cloak
簡稱:: Emir Attilax Akbar 埃米爾 阿提拉克斯 阿克巴
全名::Emir Attilax Akbar bin Mahmud bin attila bin Solomon bin adam Al Rapanui 埃米爾 阿提拉克斯 阿克巴 本 馬哈茂德 本 阿提拉 本 所羅門 本亞當 阿爾 拉帕努伊
常用名:艾提拉(艾龍), EMAIL:1466519819@qq.com
頭銜:uke總部o2o負責人,全球網格化項目創始人,
uke宗教與文化融合事務部部長, uke宗教改革委員會副主席
,Uke部落首席大酋長,
uke制度與重大會議委員會委員長,uke保安部首席大隊長,uke制度檢查委員會副會長,
uke 首席cto 奶牛科技首席cto , 軟體部門總監 技術部副總監 研發部門總監主管 產品部副經理 項目部副經理
uke波利尼西亞區大區連鎖負責人 湯加王國區域負責人。
uke克爾格倫群島區連鎖負責人,萊恩群島區連鎖負責人,
uke布維島和南喬治亞和南桑威奇群島大區連鎖負責人
Uke軟體標準化協會理事長理事長 uke終身教育學校副校長
Uke 資料庫與存儲標準化協會副會長 uke出版社編輯總編
Uke醫院 與醫學院方面的創始人
Uke 戶外運動協會理事長 uke交友協會會長
轉載請註明來源:attilax的專欄
--Atiend
推薦閱讀:
※Websocket為什麼在客戶端向服務端發送報文的時候需要掩碼加密,而服務端向客戶端不需要呢?
※SVG 具體的應用場景是怎樣的?為什麼代替 Flash 的是 Canvas 而不是 SVG 呢?
※如何評價浙江大學三名教授及一名研究生申請利用 defineProperty 做數據雙向綁定的專利?
※餓了么H5站那個佔位的base64圖片是如何生成的啊?
※WebStorm 有哪些過人之處?
TAG:前端開發 | JavaScript | Nodejs | 爬蟲計算機網路 | 網頁爬蟲 |