使用 JS 和 NodeJS 爬取 Web 內容

雲棲號資訊:【點擊查看更多行業資訊
在這裏您可以找到不同行業的第一手的上雲資訊,還在等什麼,快來!

前提
這篇文章主要針對擁有一定 Javascript 開發經驗的開發人員。但如果你很熟悉 Web 內容爬取,那麼就算沒有 Javascript 的相關經驗,也能從本文中學到很多知識。

JS 語言開發背景
使用 DevTools 提取元素選擇器(selector)的經驗
與 ES6 Javascript 相關的經驗(可選)

成果

閱讀這篇文章能夠幫助讀者:
瞭解 NodeJS 的功能
使用多個 HTTP 客戶端來輔助 Web 抓取工作
利用多個經過實戰檢驗的現代庫來抓取 Web 內容

瞭解 NodeJS:簡介

Javascript 是一種簡單而現代化的語言,最初是爲了向瀏覽器訪問的網站添加動態行爲而創建的。網站加載後,Javascript 通過瀏覽器的 JS 引擎運行,並轉換爲計算機可以理解的一堆代碼。爲了讓 Javascript 與你的瀏覽器交互,後者提供了一個運行時環境(文檔,窗口等)。
換句話說 Javascript 這種編程語言無法直接與計算機或其資源交互,抑或操縱它們。例如,在 Web 服務器中服務器必須能夠與文件系統交互,才能讀取文件或將記錄存儲在數據庫中。

NodeJS 的理念是讓 Javascript 不僅能運行在客戶端,還能運行在服務端。爲了做到這一點,資深開發人員 Ryan Dahl 採用了谷歌 Chrome 瀏覽器的 v8 JS 引擎,並將其嵌入了到名爲 Node 的 C++ 程序中。因此 NodeJS 是一個運行時環境,它讓使用 Javascript 編寫的應用程序也能運行在服務器上。

大多數語言(例如 C 或 C++)使用多個線程來處理併發,相比之下 NodeJS 只使用單個主線程,並在事件循環(Event Loop)的幫助下用它以非阻塞方式執行任務。

我們很容易就能建立一個簡單的 Web 服務器,如下所示:

const http = require('http');
const PORT = 3000;
const server = http.createServer((req, res) => {
  res.statusCode = 200;
  res.setHeader('Content-Type', 'text/plain');
  res.end('Hello World');
});
server.listen(port, () => {
  console.log(`Server running at PORT:${port}/`);
});

如果你已安裝 NodeJS,運行 node < YourFileNameHere>.js(去掉 <> 號),然後打開瀏覽器並導航到 localhost:3000,就能看到“HelloWorld”的文本了。NodeJS 非常適合 I/O 密集型應用程序。

HTTP 客戶端:查詢 Web

HTTP 客戶端是將請求發送到服務器,然後從服務器接收響應的工具。本文要討論的工具大都在後臺使用 HTTP 客戶端來查詢你將嘗試抓取的網站服務器。

Request
Request 是 Javascript 生態系統中使用最廣泛的 HTTP 客戶端之一,不過現在 Request 庫的作者已正式聲明,不推薦大家繼續使用它了。這並不是說它就不能用了,還有很多庫仍在使用它,並且它真的很好用。使用 Request 發出 HTTP 請求非常簡單:

const request = require('request')
request('https://www.reddit.com/r/programming.json', function (
  error,
  response,
  body
) {
  console.error('error:', error)
  console.log('body:', body)
})

你可以在 Github 上找到 Request 庫( https://github.com/request/request ),運行 npm install request 就能安裝完成。這裏可以參考棄用通知及細節( https://github.com/request/request/issues/3142 )。如果你因爲這個庫過時了而覺得不放心,後面還有更多推薦!

Axios
Axios 是基於 promise 的 HTTP 客戶端,可在瀏覽器和 NodeJS 中運行。如果你使用 Typescript,則 axios 可以覆蓋內置類型。通過 Axios 發起 HTTP 請求是很簡單的,它默認內置 Promise 支持,不像 Request 還得用回調:

const axios = require('axios')
axios
    .get('https://www.reddit.com/r/programming.json')
    .then((response) => {
        console.log(response)
    })
    .catch((error) => {
        console.error(error)
    });

如果你喜歡 Promises API 的 async/await 語法糖,那麼也可以用它們,但由於頂級的 await 仍處於第 3 階段( https://github.com/tc39/proposal-top-level-await ),
我們只能用 Async Function 來代替:

async function getForum() {
    try {
        const response = await axios.get(
            'https://www.reddit.com/r/programming.json'
        )
        console.log(response)
    } catch (error) {
        console.error(error)
    }
}

你只需調用 getForum 即可!你可以在 Github 上找到 Axios 庫( https://github.com/axios/axios ),運行 npm install axios 即可安裝。

Superagent
類似 Axios,Superagent 是另一款強大的 HTTP 客戶端,它支持 Promise 和 async/await 語法糖。它的 API 像 Axios 一樣簡單,但 Superagent 的依賴項更多,並且沒那麼流行。
在 Superagent 中,使用 promise、async/await 或 callbacks 發出 HTTP 請求的方式如下:

const superagent = require("superagent")
const forumURL = "https://www.reddit.com/r/programming.json"
// callbacks
superagent
    .get(forumURL)
    .end((error, response) => {
        console.log(response)
    })
// promises
superagent
    .get(forumURL)
    .then((response) => {
        console.log(response)
    })
    .catch((error) => {
        console.error(error)
    })
// promises with async/await
async function getForum() {
    try {
        const response = await superagent.get(forumURL)
        console.log(response)
    } catch (error) {
        console.error(error)
    }

你可以在 Github 上找到 Superagent 庫( https://github.com/visionmedia/superagent ),運行 npm install superagent 即可安裝。
對於下文介紹的 Web 抓取工具,本文將使用 Axios 作爲 HTTP 客戶端。

正則表達式:困難的方法
在沒有任何依賴項的情況下開始抓取 Web 內容,最簡單的方法是:使用 HTTP 客戶端查詢網頁時,在收到的 HTML 字符串上應用一組正則表達式——但這種方法繞的路太遠了。正則表達式沒那麼靈活,並且很多專業人士和業餘愛好者都很難寫出正確的正則表達式。
對於複雜的 Web 抓取任務來說,正則表達式很快就會遇到瓶頸了。不管怎樣我們先來試一下。假設有一個帶用戶名的標籤,我們需要其中的用戶名,那麼使用正則表達式時的方法差不多是這樣:

const htmlString = '<label>Username: John Doe</label>'
const result = htmlString.match(/<label>(.+)<\/label>/)
console.log(result[1], result[1].split(": ")[1])
// Username: John Doe, John Doe

在 Javascript 中,match() 通常返回一個數組,該數組包含與正則表達式匹配的所有內容。第二個元素(在索引 1 中)將找到 textContent 或 < label> 標籤的 innerHTML,這正是我們想要的。但是這個結果會包含一些我們不需要的文本(“Username: ”),必須將其刪除。
如你所見,對於一個非常簡單的用例,這種方法用起來都很麻煩。所以我們應該使用 HTML 解析器之類的工具,後文具體討論。

Cheerio:用於遍歷 DOM 的核心 JQuery

Cheerio 是一個高效輕便的庫,它允許你在服務端使用 JQuery 的豐富而強大的 API。如果你以前使用過 JQuery,那麼很容易就能上手 Cheerio。它把 DOM 所有不一致性和瀏覽器相關的特性都移除掉了,並公開了一個高效的 API 來解析和操作 DOM。

const cheerio = require('cheerio')
const $ = cheerio.load('<h2 class="title">Hello world</h2>')
$('h2.title').text('Hello there!')
$('h2').addClass('welcome')
$.html()
// <h2 class="title welcome">Hello there!</h2>

如你所見,Cheerio 用起來和 JQuery 很像。
但是,它的工作機制和 Web 瀏覽器是不一樣的,這意味着它不能:
渲染任何已解析或操縱的 DOM 元素
應用 CSS 或加載任何外部資源
執行 JavaScript
因此,如果你試圖爬取的網站或 Web 應用程序有很多 Javascript 內容(例如“單頁應用程序”),那麼 Cheerio 並不是你的最佳選擇,你可能還得依賴後文討論的其他一些選項。
爲了展示 Cheerio 的強大能力,我們將嘗試在 Reddit 中爬取 r/programming 論壇,獲取其中的帖子標題列表。
首先,運行以下命令來安裝 Cheerio 和 axios:npm install cheerio axios。
然後創建一個名爲 crawler.js 的新文件,並複製 / 粘貼以下代碼:

const axios = require('axios');
const cheerio = require('cheerio');
const getPostTitles = async () => {
    try {
        const { data } = await axios.get(
            'https://old.reddit.com/r/programming/'
        );
        const $ = cheerio.load(data);
        const postTitles = [];
        $('div > p.title > a').each((_idx, el) => {
            const postTitle = $(el).text()
            postTitles.push(postTitle)
        });
        return postTitles;
    } catch (error) {
        throw error;
    }
};
getPostTitles()
.then((postTitles) => console.log(postTitles));

getPostTitles() 是一個異步函數,它將爬取舊版 reddit 的 r/programming 論壇。首先,使用 axios HTTP 客戶端庫的一個簡單 HTTP GET 請求獲取網站的 HTML,然後使用 cheerio.load() 函數將 html 數據輸入到 Cheerio 中。
接下來使用瀏覽器的開發工具,你可以獲得通常可以定位所有 postcard 的選擇器。如果你用過 JQuery,肯定非常熟悉 $(‘div > p.title > a’)。這將獲取所有帖子,因爲你只想獲得每個帖子的標題,所以必須遍歷每個帖子(使用 each() 函數來遍歷)。
要從每個標題中提取文本,必須在 Cheerio 的幫助下獲取 DOM 元素(el 表示當前元素)。然後在每個元素上調用 text() 以獲取文本。
現在,你可以彈出一個終端並運行 node crawler.js,然後你將看到一個由大約 25 或 26 個帖子標題組成的長長的數組。儘管這是一個非常簡單的用例,但它展示了 Cheerio 提供的 API 用起來是多麼簡單。
如果你的用例需要執行 Javascript 並加載外部資源,那麼可以考慮以下幾個選項。

JSDOM:給 Node 用的 DOM

JSDOM 是用在 NodeJS 中的,文檔對象模型(DOM)的純 Javascript 實現,如前所述,DOM 對 Node 不可用,而 JSDOM 就是最近似的替代品。它多少模擬了瀏覽器的機制。
創建了一個 DOM 後,我們就可以通過編程方式與要爬取的 Web 應用程序或網站交互,像點擊按鈕這樣的操作也能做了。如果你熟悉 DOM 的操作方法,那麼 JSDOM 用起來也會很簡單。

const { JSDOM } = require('jsdom')
const { document } = new JSDOM(
    '<h2 class="title">Hello world</h2>'
).window
const heading = document.querySelector('.title')
heading.textContent = 'Hello there!'
heading.classList.add('welcome')
heading.innerHTML
// <h2 class="title welcome">Hello there!</h2>

如你所見,JSDOM 創建了一個 DOM,然後你就可以像操縱瀏覽器 DOM 那樣,用相同的方法和屬性來操縱這個 DOM。
爲了演示如何使用 JSDOM 與網站交互,我們將獲取 Redditr/programming 論壇的第一篇帖子,並對其點贊,然後我們將驗證該帖子是否已被點贊。
首先運行以下命令來安裝 jsdom 和 axios:npm install jsdom axios
然後創建一個名爲 rawler.js 的文件,並複製 / 粘貼以下代碼:

const { JSDOM } = require("jsdom")
const axios = require('axios')
const upvoteFirstPost = async () => {
  try {
    const { data } = await axios.get("https://old.reddit.com/r/programming/");
    const dom = new JSDOM(data, {
      runScripts: "dangerously",
      resources: "usable"
    });
    const { document } = dom.window;
    const firstPost = document.querySelector("div > div.midcol > div.arrow");
    firstPost.click();
    const isUpvoted = firstPost.classList.contains("upmod");
    const msg = isUpvoted
      ? "Post has been upvoted successfully!"
      : "The post has not been upvoted!";
    return msg;
  } catch (error) {
    throw error;
  }
};
upvoteFirstPost().then(msg => console.log(msg));

upvoteFirstPost() 是一個異步函數,它將在 r/programming 中獲取第一個帖子,然後對其點贊。爲此,axios 發送 HTTP GET 請求以獲取指定 URL 的 HTML。然後向 JSDOM 提供先前獲取的 HTML 來創建新的 DOM。JSDOM 構造器將 HTML 作爲第一個參數,將選項作爲第二個參數,添加的 2 個選項會執行以下函數:

  • runScripts:設置爲“dangerously”時,它允許執行事件處理程序和任何 Javascript 代碼。如果你不清楚應用程序將運行的腳本是否可信,則最好將 runScripts 設置爲“outside-only”,這會將所有 Javascript 規範提供的全局變量附加到 window 對象,從而防止任何腳本在內部執行。
  • resources:設置爲“usable”時,它允許加載使用

【雲棲號在線課堂】每天都有產品技術專家分享!
課程地址:https://yqh.aliyun.com/zhibo

立即加入社羣,與專家面對面,及時瞭解課程最新動態!
【雲棲號在線課堂 社羣】https://c.tb.cn/F3.Z8gvnK

原文發佈時間:2020-06-15
本文作者:Shenesh Perera
本文來自:“InfoQ ”,瞭解相關信息可以關注“InfoQ

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章