分分鐘教你用node.js寫個爬蟲

寫在前面

十分感謝你們的點贊和關注。其實,這是我第一次在 掘金上寫文章。由於我也是前段時間偶然之間纔開始瞭解和學習爬蟲,並且學習node的時間也不是很長。雖然用node作過一些後端的項目,但其實在node和爬蟲方面我仍是一個新人,這篇文章主要是想和你們分享一下node和爬蟲方面的基本知識,但願對你們有幫助,也想和你們一塊兒交流,一塊兒學習,再次謝謝你們的支持!

對了,我開通了我的的 GitHub主頁,裏面有本身的技術文章,還會有我的的隨想、思考和日誌。之後全部的文章都會第一時間更新到這裏,而後同步到其餘平臺。有喜歡的朋友能夠沒事去逛逛,再次感謝你們的支持!javascript

1、什麼是爬蟲

網絡爬蟲(又被稱爲網頁蜘蛛,網絡機器人,在 FOAF社區中間,更常常的稱爲網頁追逐者),是一種按照必定的規則,自動地抓取萬維網信息的程序或者腳本。另一些不常使用的名字還有螞蟻、自動索引、模擬程序或者蠕蟲。
WIKIPEDIA 爬蟲介紹

2、爬蟲的分類

  • 通用網絡爬蟲(全網爬蟲)
爬行對象從一些 種子URL 擴充到整個 Web,主要爲門戶站點搜索引擎和大型 Web 服務提供商採集數據。


  • 聚焦網絡爬蟲(主題網絡爬蟲)
指選擇性 地爬行那些與預先定義好的主題相關頁面的網絡爬蟲。
  • 增量式網絡爬蟲
指對已下載網頁採起增量式更新和 只爬行新產生的或者已經發生變化網頁 的爬蟲,它可以在必定程度上保證所爬行的頁面是儘量新的頁面。
  • Deep Web 爬蟲
爬行對象是一些在用戶填入關鍵字搜索或登陸後才能訪問到的 深層網頁信息的爬蟲。

3、爬蟲的爬行策略

  • 通用網絡爬蟲(全網爬蟲)
深度優先策略、廣度優先策略


  • 聚焦網絡爬蟲(主題網絡爬蟲)
基於內容評價的爬行策略(內容相關性),基於連接結構評價的爬行策略、基於加強學習的爬行策略(連接重要性),基於語境圖的爬行策略(距離,圖論中兩節點間邊的權重)
  • 增量式網絡爬蟲
統一更新法、個體更新法、基於分類的更新法、自適應調頻更新法
  • Deep Web 爬蟲
Deep Web 爬蟲爬行過程當中最重要部分就是表單填寫,包含兩種類型:基於領域知識的表單填寫、基於網頁結構分析的表單填寫

現代的網頁爬蟲的行爲一般是四種策略組合的結果:html

選擇策略:決定所要下載的頁面;
從新訪問策略:決定何時檢查頁面的更新變化;
平衡禮貌策略:指出怎樣避免站點超載;
並行策略:指出怎麼協同達到分佈式抓取的效果;


4、寫一個簡單網頁爬蟲的流程

  1. 肯定爬取對象(網站/頁面)
  2. 分析頁面內容(目標數據/DOM結構)
  3. 肯定開發語言、框架、工具等
  4. 編碼 測試,爬取數據
  5. 優化

一個簡單的百度新聞爬蟲

肯定爬取對象(網站/頁面)

百度新聞news.baidu.com/

分析頁面內容(目標數據/DOM結構)

······

肯定開發語言、框架、工具等

node.js (express) + SublimeText 3

編碼,測試,爬取數據

coding ···

Let's start

新建項目目錄

1.在合適的磁盤目錄下建立項目目錄 baiduNews(個人項目目錄是: F:\web\baiduNews

注:由於在寫這篇文章的時候用的電腦真心比較渣。安裝WebStorm或者VsCode跑項目有些吃力。因此後面的命令行操做我都是在Window自帶的DOS命令行窗口中執行的。前端

初始化package.json

1.在DOS命令行中進入項目根目錄 baiduNews
2.執行 npm init,初始化 package.json文件

安裝依賴

express (使用express來搭建一個簡單的Http服務器。固然,你也可使用node中自帶的 http模塊)
superagent (superagent是node裏一個很是方便的、輕量的、漸進式的第三方客戶端請求代理模塊,用他來請求目標頁面)
cheerio (cheerio至關於node版的jQuery,用過jQuery的同窗會很是容易上手。它主要是用來獲取抓取到的頁面元素和其中的數據信息)
// 我的比較喜歡使用yarn來安裝依賴包,固然你也可使用 npm install 來安裝依賴,看我的習慣。
yarn add express yarn add superagent yarn add cheerio複製代碼

依賴安裝完成後你能夠在package.json中查看剛纔安裝的依賴是否成功。
安裝正確後以下圖:java


開始coding

1、使用express啓動一個簡單的本地Http服務器node

一、在項目根目錄下建立index.js文件(後面都會在這個index文件中進行coding)git

二、建立好index.js後,咱們首先實例化一個express對象,用它來啓動一個本地監聽3000端口的Http服務。程序員

const express = require('express');
const app = express();

// ...

let server = app.listen(3000, function () {
  let host = server.address().address;
  let port = server.address().port;
  console.log('Your App is running at http://%s:%s', host, port);
});複製代碼

對,就是這麼簡單,不到10行代碼,搭建啓動一個簡單的本地Http服務。github

三、按照國際慣例,咱們但願在訪問本機地址http://localhost:3000的時候,這個服務能給咱們犯規一個Hello World!index.js中加入以下代碼:web

app.get('/', function (req, res) {
  res.send('Hello World!');
});複製代碼
此時,在DOS中項目根目錄 baiduNews下執行 node index.js,讓項目跑起來。以後,打開瀏覽器,訪問 http://localhost:3000,你就會發現頁面上顯示'Hellow World!'字樣。
這樣,在後面咱們獲取到百度新聞首頁的信息後,就能夠在訪問 http://localhost:3000時看到這些信息。

2、抓取百度新聞首頁的新聞信息ajax

一、 首先,咱們先來分析一下百度新聞首頁的頁面信息。



百度新聞首頁大致上分爲「熱點新聞」、「本地新聞」、「國內新聞」、「國際新聞」......等。此次咱們先來嘗試抓取左側 「熱點新聞」和下方的 「本地新聞」兩處的新聞數據。


F12打開 Chrome的控制檯,審查頁面元素,通過查看左側「熱點新聞」信息所在 DOM的結構,咱們發現全部的「熱點新聞」信息(包括新聞標題和新聞頁面連接)都在 id#pane-news<div>下面 <ul><li>下的 <a>標籤中。用 jQuery的選擇器表示爲: #pane-news ul li a

二、爲了爬取新聞數據,首先咱們要用superagent請求目標頁面,獲取整個新聞首頁信息

// 引入所須要的第三方包
const superagent= require('superagent');

let hotNews = [];                                // 熱點新聞
let localNews = [];                              // 本地新聞

/** * index.js * [description] - 使用superagent.get()方法來訪問百度新聞首頁 */
superagent.get('http://news.baidu.com/').end((err, res) => {
  if (err) {
    // 若是訪問失敗或者出錯,會這行這裏
    console.log(`熱點新聞抓取失敗 - ${err}`)
  } else {
   // 訪問成功,請求http://news.baidu.com/頁面所返回的數據會包含在res
   // 抓取熱點新聞數據
   hotNews = getHotNews(res)
  }
});複製代碼

三、獲取頁面信息後,咱們來定義一個函數getHotNews()來抓取頁面內的「熱點新聞」數據。

/** * index.js * [description] - 抓取熱點新聞頁面 */
// 引入所須要的第三方包
const cheerio = require('cheerio');

let getHotNews = (res) => {
  let hotNews = [];
  // 訪問成功,請求http://news.baidu.com/頁面所返回的數據會包含在res.text中。
  
  /* 使用cheerio模塊的cherrio.load()方法,將HTMLdocument做爲參數傳入函數 之後就可使用相似jQuery的$(selectior)的方式來獲取頁面元素 */
  let $ = cheerio.load(res.text);

  // 找到目標數據所在的頁面元素,獲取數據
  $('div#pane-news ul li a').each((idx, ele) => {
    // cherrio中$('selector').each()用來遍歷全部匹配到的DOM元素
    // 參數idx是當前遍歷的元素的索引,ele就是當前便利的DOM元素
    let news = {
      title: $(ele).text(),        // 獲取新聞標題
      href: $(ele).attr('href')    // 獲取新聞網頁連接
    };
    hotNews.push(news)              // 存入最終結果數組
  });
  return hotNews
};複製代碼

這裏要多說幾點:

  1. async/await聽說是異步編程的終級解決方案,它可讓咱們以同步的思惟方式來進行異步編程。Promise解決了異步編程的「回調地獄」,async/await同時使異步流程控制變得友好而有清晰,有興趣的同窗能夠去了解學習一下,真的很好用。
  2. superagent模塊提供了不少好比getpostdelte等方法,能夠很方便地進行Ajax請求操做。在請求結束後執行.end()回調函數。.end()接受一個函數做爲參數,該函數又有兩個參數error和res。當請求失敗,error會包含返回的錯誤信息,請求成功,error值爲null,返回的數據會包含在res參數中。
  3. cheerio模塊的.load()方法,將HTML document做爲參數傳入函數,之後就可使用相似jQuery的$(selectior)的方式來獲取頁面元素。同時可使用相似於jQuery中的.each()來遍歷元素。此外,還有不少方法,你們能夠自行Google/Baidu。

四、將抓取的數據返回給前端瀏覽器

前面, const app = express();實例化了一個 express對象 app
app.get('', async() => {})接受兩個參數,第一個參數接受一個String類型的路由路徑,表示Ajax的請求路徑。第二個參數接受一個函數Function,當請求此路徑時就會執行這個函數中的代碼。
/** * [description] - 跟路由 */
// 當一個get請求 http://localhost:3000時,就會後面的async函數
app.get('/', async (req, res, next) => {
  res.send(hotNews);
});複製代碼
在DOS中項目根目錄 baiduNews下執行 node index.js,讓項目跑起來。以後,打開瀏覽器,訪問 http://localhost:3000,你就會發現抓取到的數據返回到了前端頁面。我運行代碼後瀏覽器展現的返回信息以下:
注:由於個人Chrome安裝了JSONView擴展程序,因此返回的數據在頁面展現的時候會被自動格式化爲結構性的JSON格式,方便查看。


OK!!這樣,一個簡單的百度「熱點新聞」的爬蟲就大功告成啦!!

簡單總結一下,其實步驟很簡單:

  1. express啓動一個簡單的Http服務
  2. 分析目標頁面DOM結構,找到所要抓取的信息的相關DOM元素
  3. 使用superagent請求目標頁面
  4. 使用cheerio獲取頁面元素,獲取目標數據
  5. 返回數據到前端瀏覽器

如今,繼續咱們的目標,抓取「本地新聞」數據(編碼過程當中,咱們會遇到一些有意思的問題)
有了前面的基礎,咱們天然而然的會想到利用和上面相同的方法「本地新聞」數據。
一、 分析頁面中「本地新聞」部分的DOM結構,以下圖:


F12打開控制檯,審查「本地新聞」 DOM元素,咱們發現,「本地新聞」分爲兩個主要部分,「左側新聞」和右側的「新聞資訊」。這全部目標數據都在 id#local_newsdiv中。「左側新聞」數據又在 id#localnews-focusul標籤下的 li標籤下的 a標籤中,包括新聞標題和頁面連接。「本地資訊」數據又在 id#localnews-zixundiv下的 ul標籤下的 li標籤下的 a標籤中,包括新聞標題和頁面連接。

二、OK!分析了DOM結構,肯定了數據的位置,接下來和爬取「熱點新聞」同樣,循序漸進,定義一個getLocalNews()函數,爬取這些數據。

/** * [description] - 抓取本地新聞頁面 */
let getLocalNews = (res) => {
  let localNews = [];
  let $ = cheerio.load(res);
    
  // 本地新聞
  $('ul#localnews-focus li a').each((idx, ele) => {
    let news = {
      title: $(ele).text(),
      href: $(ele).attr('href'),
    };
    localNews.push(news)
  });
    
  // 本地資訊
  $('div#localnews-zixun ul li a').each((index, item) => {
    let news = {
      title: $(item).text(),
      href: $(item).attr('href')
    };
    localNews.push(news);
  });

  return localNews
};複製代碼

對應的,在superagent.get()中請求頁面後,咱們須要調用getLocalNews()函數,來爬去本地新聞數據。
superagent.get()函數修改成:

superagent.get('http://news.baidu.com/').end((err, res) => {
  if (err) {
    // 若是訪問失敗或者出錯,會這行這裏
    console.log(`熱點新聞抓取失敗 - ${err}`)
  } else {
   // 訪問成功,請求http://news.baidu.com/頁面所返回的數據會包含在res
   // 抓取熱點新聞數據
   hotNews = getHotNews(res)
   localNews = getLocalNews(res)
  }
});複製代碼

同時,咱們要在app.get()路由中也要將數據返回給前端瀏覽器。app.get()路由代碼修改成:

/** * [description] - 跟路由 */
// 當一個get請求 http://localhost:3000時,就會後面的async函數
app.get('/', async (req, res, next) => {
  res.send({
    hotNews: hotNews,
    localNews: localNews
  });
});複製代碼
編碼完成,激動不已!! DOS中讓項目跑起來,用瀏覽器訪問 http://localhost:3000

尷尬的事情發生了!!返回的數據只有熱點新聞,而本地新聞返回一個空數組[ ]。檢查代碼,發現也沒有問題,但爲何一直返回的空數組呢?
通過一番緣由查找,才返現問題出在哪裏!!

一個有意思的問題

爲了找到緣由,首先,咱們看看用 superagent.get('http://news.baidu.com/').end((err, res) => {})請求百度新聞首頁在回調函數 .end()中的第二個參數res中到底拿到了什麼內容?
// 新定義一個全局變量 pageRes
let pageRes = {};        // supergaent頁面返回值

// superagent.get()中將res存入pageRes
superagent.get('http://news.baidu.com/').end((err, res) => {
  if (err) {
    // 若是訪問失敗或者出錯,會這行這裏
    console.log(`熱點新聞抓取失敗 - ${err}`)
  } else {
   // 訪問成功,請求http://news.baidu.com/頁面所返回的數據會包含在res
   // 抓取熱點新聞數據
   // hotNews = getHotNews(res)
   // localNews = getLocalNews(res)
   pageRes = res
  }
});

// 將pageRes返回給前端瀏覽器,便於查看
app.get('/', async (req, res, next) => {
  res.send({
    // {}hotNews: hotNews,
    // localNews: localNews,
    pageRes: pageRes
  });
});複製代碼
訪問瀏覽器 http://localhost:3000,頁面展現以下內容:


能夠看到,返回值中的 text字段應該就是整個頁面的 HTML代碼的字符串格式。爲了方便咱們觀察,能夠直接把這個 text字段值返回給前端瀏覽器,這樣咱們就可以清晰地看到通過瀏覽器渲染後的頁面。

修改給前端瀏覽器的返回值

app.get('/', async (req, res, next) => {
  res.send(pageRes.text)
}複製代碼

訪問瀏覽器http://localhost:3000,頁面展現以下內容:


審查元素才發現,原來咱們抓取的目標數據所在的 DOM元素中是空的,裏面沒有數據!
到這裏,一切水落石出!在咱們使用 superagent.get()訪問百度新聞首頁時, res中包含的獲取的頁面內容中,咱們想要的「本地新聞」數據尚未生成, DOM節點元素是空的,因此出現前面的狀況!抓取後返回的數據一直是空數組 [ ]


在控制檯的 Network中咱們發現頁面請求了一次這樣的接口:
http://localhost:3000/widget?id=LocalNews&ajax=json&t=1526295667917,接口狀態 404
這應該就是百度新聞獲取 「本地新聞」的接口,到這裏一切都明白了!「本地新聞」是在頁面加載後動態請求上面這個接口獲取的,因此咱們用 superagent.get()請求的頁面再去請求這個接口時,接口 URLhostname部分變成了本地 IP地址,而本機上沒有這個接口,因此 404,請求不到數據。

找到緣由,咱們來想辦法解決這個問題!!

  1. 直接使用superagent訪問正確合法的百度「本地新聞」的接口,獲取數據後返回給前端瀏覽器。
  2. 使用第三方npm包,模擬瀏覽器訪問百度新聞首頁,在這個模擬瀏覽器中當「本地新聞」加載成功後,抓取數據,返回給前端瀏覽器。

以上方法都可,咱們來試試比較有意思的第二種方法

使用Nightmare自動化測試工具

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

Nightmare是一個基於Electron的框架,針對Web自動化測試和爬蟲,由於其具備跟PlantomJS同樣的自動化測試的功能能夠在頁面上模擬用戶的行爲觸發一些異步數據加載,也能夠跟Request庫同樣直接訪問URL來抓取數據,而且能夠設置頁面的延遲時間,因此不管是手動觸發腳本仍是行爲觸發腳本都是垂手可得的。

安裝依賴

// 安裝nightmare
yarn add nightmare複製代碼

爲獲取「本地新聞」,繼續coding...

index.js中新增以下代碼:

const Nightmare = require('nightmare');          // 自動化測試包,處理動態頁面
const nightmare = Nightmare({ show: true });     // show:true 顯示內置模擬瀏覽器

/** * [description] - 抓取本地新聞頁面 * [nremark] - 百度本地新聞在訪問頁面後加載js定位IP位置後獲取對應新聞, * 因此抓取本地新聞須要使用 nightmare 一類的自動化測試工具, * 模擬瀏覽器環境訪問頁面,使js運行,生成動態頁面再抓取 */
// 抓取本地新聞頁面
nightmare
.goto('http://news.baidu.com/')
.wait("div#local_news")
.evaluate(() => document.querySelector("div#local_news").innerHTML)
.then(htmlStr => {
  // 獲取本地新聞數據
  localNews = getLocalNews(htmlStr)
})
.catch(error => {
  console.log(`本地新聞抓取失敗 - ${error}`);
})複製代碼

修改getLocalNews()函數爲:

/** * [description]- 獲取本地新聞數據 */
let getLocalNews = (htmlStr) => {
  let localNews = [];
  let $ = cheerio.load(htmlStr);

  // 本地新聞
  $('ul#localnews-focus li a').each((idx, ele) => {
    let news = {
      title: $(ele).text(),
      href: $(ele).attr('href'),
    };
    localNews.push(news)
  });

  // 本地資訊
  $('div#localnews-zixun ul li a').each((index, item) => {
    let news = {
      title: $(item).text(),
      href: $(item).attr('href')
    };
    localNews.push(news);
  });

  return localNews
}複製代碼

修改app.get('/')路由爲:

/** * [description] - 跟路由 */
// 當一個get請求 http://localhost:3000時,就會後面的async函數
app.get('/', async (req, res, next) => {
  res.send({
    hotNews: hotNews,
    localNews: localNews
  })
});複製代碼
此時, DOS命令行中從新讓項目跑起來,瀏覽器訪問 https://localhost:3000,看看頁面展現的信息,看是否抓取到了 「本地新聞」數據!

至此,一個簡單而又完整的抓取百度新聞頁面「熱點新聞」和「本地新聞」的爬蟲就大功告成啦!!

最後總結一下,總體思路以下:

  1. express啓動一個簡單的Http服務
  2. 分析目標頁面DOM結構,找到所要抓取的信息的相關DOM元
  3. 使用superagent請求目標頁面
  4. 動態頁面(須要加載頁面後運行JS或請求接口的頁面)可使用Nightmare模擬瀏覽器訪問
  5. 使用cheerio獲取頁面元素,獲取目標數據

完整代碼

爬蟲完整代碼GitHub地址: 完整代碼

後面,應該還會作一些進階,來爬取某些網站上比較好看的圖片(手動滑稽),會牽扯到併發控制反-反爬蟲的一些策略。再用爬蟲取爬去一些須要登陸和輸入驗證碼的網站,歡迎到時你們關注和指正交流。

我想說

再次感謝你們的點贊和關注和評論,謝謝你們的支持,謝謝!我本身以爲我算是一個愛文字,愛音樂,同時也喜歡coding的半文藝程序員。以前也一直想着寫一寫技術性和其餘偏文學性的文章。雖然本身的底子沒有多麼優秀,但老是以爲在寫文章的過程當中,不管是技術性的仍是偏文學性的,這個過程當中能夠督促本身去思考,督促本身去學習和交流。 畢竟天天忙忙碌碌之餘,仍是要活出本身不同的生活。因此,之後若是有一些好的文章我會積極和你們分享!再次感謝你們的支持!
相關文章
相關標籤/搜索