Electron 可以讓你使用純 JavaScript 調用 Chrome 豐富的原生的介面來創造桌面應用。你可以把它看作一個專註於桌面應用的 Node.js 的變體,而不是 Web 伺服器。其基於瀏覽器的應用方式可以極方便的做各種響應式的交互,接下來介紹下關於 Electron 上衍生出的框架 N... ...
作者:William
本文為原創文章,轉載請註明作者及出處
Electron 可以讓你使用純 JavaScript 調用 Chrome 豐富的原生的介面來創造桌面應用。你可以把它看作一個專註於桌面應用的 Node.js 的變體,而不是 Web 伺服器。其基於瀏覽器的應用方式可以極方便的做各種響應式的交互,接下來介紹下關於 Electron 上衍生出的框架 Nightmare。
Nightmare 是一個基於 Electron 的框架,針對 Web 自動化測試和爬蟲(其實爬蟲這個是大家自己給這個框架加的功能XD),因為其具有跟 PlantomJS 一樣的自動化測試的功能可以在頁面上模擬用戶的行為觸發一些非同步數據載入,也可以跟 Request 庫一樣直接訪問 URL 來抓取數據,並且可以設置頁面的延遲時間,所以無論是手動觸發腳本還是行為觸發腳本都是輕而易舉的(這邊註意,如果事件具備 isTrusted 的檢查的話,就無法觸發了)。
使用 Nightmare
為了更快速使用 NPM 下載,可以使用淘寶的鏡像地址。直接 NPM 安裝Nightmare 就完成安裝了(二進位的 Electron 依賴有點大,安裝時間可能比較長)。
寫一個簡單的啟動 app.js;
const Nightmare = require('nightmare')
const nightmare = new Nightmare({
show: true,
openDevTools: {
mode: 'detach'
}
})
nightmare.goto('https://www.hujiang.com')
.evaluate(function() {
// 該環境中能使用瀏覽器中的任何對象window/document,並且返回一個promise
console.log('hello nightmare')
console.log('5 second close window')
})
.wait(5000)
.end()
.then(()=> {
console.log('close nightmare')
})
這個腳本會在打開的瀏覽器的調試控制臺中列印出 hello nightmare 並且在5秒後關閉,隨後在運行的該腳本的中輸出 close nightmare。
Nightmare原理
利用了 Electron 提供的 Browser 的環境,同時具備了 Node.js 的 I/O 能力,所以可以很方便實現一個爬蟲應用。Nightmare 的官網有更詳細的介紹:
大致操作:
- 瀏覽器事件: goto,back,forward,refresh,
- 用戶事件: click,mousedown,mouseup,mouseover,type,insert,select,check,uncheck,selectscrollTo
- 向網頁註入腳本: .js .css的文件類型原理是跟油猴差不多,可以編寫自己的js代碼註入十分方便
- wait 函數可以按照延遲時間或者一個 dom 元素的出現
- evaluate 以瀏覽器的環境運行的腳本函數,然後返回一個 promise 函數
一個完整的nightmare爬蟲應用
我們以抓取知乎上的話題的為應用場景,需要的數據是知乎的話題信息 包含以下欄位 話題名稱/話題的圖片/關註者數量/話題數量/精華話題數量,但是因為後三者只能在其父親話題中包含,所以必須先抓父話題才能抓取子話題,而且這些子話題是以 hover 的形式在父話題中非同步載入的,如果用Request/Superagent 需要 HTTP 傳遞其解析過的id才能獲取,但是用Nightmare 可以直接調用其 hover 事件觸發數據的載入。
第一步獲取需要抓取的話題深度,預設的根是現在知乎的根話題;
/**
* 抓取對應的話題頁面的url和對應的深度保存到指定的文件名中
* @param {string} rootUrl - 頂層的url
* @param {int} deep - 抓取頁面的深度
* @param {string} toFile - 保存的文件名
* @param {Function} cb - 完成後的回調
*/
async function crawlerTopicsFromRoot (rootUrl, deep, toFile, cb) {
rootUrl = rootUrl ||'https://www.zhihu.com/topic/19776749/hot'
toFile = toFile || './topicsTree.json'
console.time()
const result = await interactive
.iAllTopics(rootUrl, deep)
console.timeEnd()
util.writeJSONToFile(result['topics'], toFile, cb)
}
crawlerTopicsFromRoot('', 2, '', _ => {
console.log('完成抓取')
})
然後進行交互函數的核心函數,註意在開始抓取前,要去看看知乎的 robots.txt 文件看看哪些能抓和抓取的間隔不然很容易 timeout 的錯誤。
// 獲取對應的話題的信息
const cntObj = queue.shift()
const url = `https://www.zhihu.com/topic/${cntObj['id']}/hot`
const topicOriginalInfo = await nightmare
.goto(url)
.wait('.zu-main-sidebar') // 等待該元素的出現
.evaluate(function () {
// 獲取這塊數據
return document.querySelector('.zu-main-sidebar').innerHTML
})
// .....若幹步的操作後
// 獲取其子話題的數值信息
const hoverElement = `a.zm-item-tag[href$='${childTopics[i]['id']}']`
const waitElement = `.avatar-link[href$='${childTopics[i]['id']}']`
const topicAttached = await nightmare
.mouseover(hoverElement) // 觸發hover事件
.wait(waitElement)
.evaluate(function () {
return document.querySelector('.zh-profile-card').innerHTML
})
.then(val => {
return parseRule.crawlerTopicNumbericalAttr(val)
})
.catch(error => {
console.error(error)
})
cheerio 是一個 jQuery 的 selector 庫,可以應用於 HTML 片段並且獲得對應的DOM 元素,然後我們就可以進行對應的 DOM 操作->增刪改查都可以,這邊主要用來查詢 DOM 和獲取數據。
const $ = require('cheerio')
/** *抓取對應話題的問題數量/精華話題數量/關註者數量 */
const crawlerTopicNumbericalAttr = function (html) {
const $ = cheerio.load(html)
const keys = ['questions', 'top-answers', 'followers']
const obj = {}
obj['avatar'] = $('.Avatar.Avatar--xs').attr('src')
keys.forEach(key => {
obj[key] = ($(`div.meta a.item[href$=${key}] .value`).text() || '').trim()
})
return obj
}
/** * 抓取話題的信息 */
const crawlerTopics = function (html) {
const $ = cheerio.load(html)
const obj = {}
const childTopics = crawlerAttachTopic($, '.child-topic')
obj['desc'] = $('div.zm-editable-content').text() || ''
if (childTopics.length > 0) {
obj['childTopics'] = childTopics
}
return obj
}
/** * 抓取子話題的信息id/名稱 */
const crawlerAttachTopic = function ($, selector) {
const topicsSet = []
$(selector).find('.zm-item-tag').each((index, elm) => {
const self = $(elm)
const topic = {}
topic['id'] = self.attr('data-token')
topic['value'] = self.text().trim()
topicsSet.push(topic)
})
return topicsSet
}
然後一個簡單的爬蟲就完成了,最終獲得部分數據格式如何:
{
"value": "rootValue",
"id": "19776749",
"fatherId": "-1",
"desc": "知乎的全部話題通過父子關係構成一個有根無迴圈的有向圖。「根話題」即為所有話題的最上層的父話題。話題精華即為知乎的 Top1000 高票回答。請不要在問題上直接綁定「根話題」。這樣會使問題話題過於寬泛。",
"cids": [
"19778317",
"19776751",
"19778298",
"19618774",
"19778287",
"19560891"
]
},
{
"id": "19778317",
"value": "生活、藝術、文化與活動",
"avatar": "https://pic4.zhimg.com/6df49c633_xs.jpg",
"questions": "3.7M",
"top-answers": "1000",
"followers": "91K",
"fid": "19776749",
"desc": "以人類集體行為和人類社會文明為主體的話題,其內容主要包含生活、藝術、文化、活動四個方面。",
"cids": [
"19551147",
"19554825",
"19550453",
"19552706",
"19551077",
"19550434",
"19552266",
"19554791",
"19553622",
"19553632"
]
},
總結
Nightmare 作為爬蟲的最大優勢是只需要知道數據所在頁面的 URL 就可以獲取對應的同步/非同步數據,並不需要詳細的分析 HTTP 需要傳遞的參數。只需要知道進行哪些操作能使得網頁頁面數據更新,就能通過獲取更新後的 HTML 片段獲得對應的數據,在 Demo 中的 Nightmare 是打開了 chrome-dev 進行操作的,但是實際運行的時候是可以關閉的,關閉了之後其操作的速度會有一定的上升。下麵的項目中還包含了另外一個爬取的知乎的動態。
Demo源碼地址: https://github.com/williamstar/nightmare-demo
iKcamp原創新書《移動Web前端高效開發實戰》已在亞馬遜、京東、噹噹開售。
*報名地址:http://www.huodongxing.com/event/4404706591400*