基於 Electron 的爬蟲框架 Nightmare

做者:William
本文爲原創文章,轉載請註明做者及出處javascript

Electron 可讓你使用純 JavaScript 調用 Chrome 豐富的原生的接口來創造桌面應用。你能夠把它看做一個專一於桌面應用的 Node.js 的變體,而不是 Web 服務器。其基於瀏覽器的應用方式能夠極方便的作各類響應式的交互,接下來介紹下關於 Electron 上衍生出的框架 Nightmare。css

Nightmare 是一個基於 Electron 的框架,針對 Web 自動化測試和爬蟲(其實爬蟲這個是你們本身給這個框架加的功能XD),由於其具備跟 PlantomJS 同樣的自動化測試的功能能夠在頁面上模擬用戶的行爲觸發一些異步數據加載,也能夠跟 Request 庫同樣直接訪問 URL 來抓取數據,而且能夠設置頁面的延遲時間,因此不管是手動觸發腳本仍是行爲觸發腳本都是垂手可得的(這邊注意,若是事件具有 isTrusted 的檢查的話,就沒法觸發了)。html

使用 Nightmare

爲了更快速使用 NPM 下載,可使用淘寶的鏡像地址。直接 NPM 安裝Nightmare 就完成安裝了(二進制的 Electron 依賴有點大,安裝時間可能比較長)。前端

寫一個簡單的啓動 app.js;java

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。git

Nightmare原理

利用了 Electron 提供的 Browser 的環境,同時具有了 Node.js 的 I/O 能力,因此能夠很方便實現一個爬蟲應用。Nightmare 的官網有更詳細的介紹:github

大體操做:chrome

  • 瀏覽器事件: 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 事件觸發數據的加載。json

第一步獲取須要抓取的話題深度,默認的根是如今知乎的根話題;promise

/** 
* 抓取對應的話題頁面的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/williamsta...

iKcamp原創新書《移動Web前端高效開發實戰》已在亞馬遜、京東、噹噹開售。

>> 滬江Web前端上海團隊招聘【Web前端架構師】,有意者簡歷至:zhouyao@hujiang.com <<


報名地址:http://www.huodongxing.com/ev...

相關文章
相關標籤/搜索