用 Javascript 爬取網頁

做者:Shenesh Perera

翻譯:瘋狂的技術宅javascript

原文:https://www.scrapingbee.com/b...html

未經容許嚴禁轉載前端

Web Scraping with Javascript

本文講解怎樣用 Node.js 高效地從 Web 爬取數據。java

前提條件

本文主要針對具備必定 JavaScript 經驗的程序員。若是你對 Web 抓取有深入的瞭解,但對 JavaScript 並不熟悉,那麼本文仍然可以對你有所幫助。node

  • ✅ 會 JavaScript
  • ✅ 會用 DevTools 提取元素選擇器
  • ✅ 會一些 ES6 (可選)

你將學到

經過本文你將學到:ios

  • 學到更多關於 Node.js 的東西
  • 用多個 HTTP 客戶端來幫助 Web 抓取的過程
  • 利用多個通過實踐考驗過的庫來爬取 Web

瞭解 Node.js

Javascript 是一種簡單的現代編程語言,最初是爲了向瀏覽器中的網頁添加動態效果。當加載網站後,Javascript 代碼由瀏覽器的 Javascript 引擎運行。爲了使 Javascript 與你的瀏覽器進行交互,瀏覽器還提供了運行時環境(document、window等)。git

這意味着 Javascript 不能直接與計算機資源交互或對其進行操做。例如在 Web 服務器中,服務器必須可以與文件系統進行交互,這樣才能讀寫文件。程序員

Node.js 使 Javascript 不只可以運行在客戶端,並且還能夠運行在服務器端。爲了作到這一點,其創始人 Ryan Dahl 選擇了Google Chrome 瀏覽器的 v8 Javascript Engine,並將其嵌入到用 C++ 開發的 Node 程序中。因此 Node.js 是一個運行時環境,它容許 Javascript 代碼也能在服務器上運行。github

與其餘語言(例如 C 或 C++)經過多個線程來處理併發性相反,Node.js 利用單個主線程並並在事件循環的幫助下以非阻塞方式執行任務。web

要建立一個簡單的 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}/`);
});

若是你已安裝了 Node.js,能夠試着運行上面的代碼。 Node.js 很是適合 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/re... 找到棄用通知及其含義。

Axios

Axios 是基於 promise 的 HTTP 客戶端,可在瀏覽器和 Node.js 中運行。若是你用 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 仍處於 stage 3 ,因此咱們只好先用異步函數來代替:

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!能夠在 https://github.com/axios/axios 上找到Axios庫。

Superagent

與 Axios 同樣,Superagent 是另外一個強大的 HTTP 客戶端,它支持 Promise 和 async/await 語法糖。它具備像 Axios 這樣至關簡單的 API,可是 Superagent 因爲存在更多的依賴關係而且不那麼流行。

用 promise、async/await 或回調向 Superagent 發出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)
    }
}

能夠在 https://github.com/visionmedi... 找到 Superagent。

正則表達式:艱難的路

在沒有任何依賴性的狀況下,最簡單的進行網絡抓取的方法是,使用 HTTP 客戶端查詢網頁時,在收到的 HTML 字符串上使用一堆正則表達式。正則表達式不那麼靈活,並且不少專業人士和業餘愛好者都難以編寫正確的正則表達式。

讓咱們試一試,假設其中有一個帶有用戶名的標籤,咱們須要該用戶名,這相似於你依賴正則表達式時必須執行的操做

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中)將找到咱們想要的 <label> 標記的 textContentinnerHTML。可是結果中包含一些不須要的文本( 「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 用起來很是類似。

可是,儘管它的工做方式不一樣於網絡瀏覽器,也就這意味着它不能:

  • 渲染任何解析的或操縱 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 中。

而後在瀏覽器的 Dev Tools 幫助下,能夠得到能夠定位全部列表項的選擇器。若是你使用過 JQuery,則必須很是熟悉 $('div> p.title> a')。這將獲得全部帖子,由於你只但願單獨獲取每一個帖子的標題,因此必須遍歷每一個帖子,這些操做是在 each() 函數的幫助下完成的。

要從每一個標題中提取文本,必須在 Cheerio 的幫助下獲取 DOM元素( el 指代當前元素)。而後在每一個元素上調用 text() 可以爲你提供文本。

如今,打開終端並運行 node crawler.js,而後你將看到大約存有標題的數組,它會很長。儘管這是一個很是簡單的用例,但它展現了 Cheerio 提供的 API 的簡單性質。

若是你的用例須要執行 Javascript 並加載外部源,那麼如下幾個選項將頗有幫助。

JSDOM:Node 的 DOM

JSDOM 是在 Node.js 中使用的文檔對象模型的純 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 與網站進行交互,咱們將得到 Reddit r/programming 論壇的第一篇帖子並對其進行投票,而後驗證該帖子是否已被投票。

首先運行如下命令來安裝 jsdom 和 axios:npm install jsdom axios

而後建立名爲 crawler.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。而後經過先前獲取的 HTML 來建立新的 DOM。 JSDOM 構造函數把HTML 做爲第一個參數,把 option 做爲第二個參數,已添加的 2 個 option 項執行如下功能:

  • runScripts:設置爲 dangerously 時容許執行事件 handler 和任何 Javascript 代碼。若是你不清楚將要運行的腳本的安全性,則最好將 runScripts 設置爲「outside-only」,這會把全部提供的 Javascript 規範附加到 「window」 對象,從而阻止在 inside 上執行的任何腳本。
  • resources:設置爲「usable」時,容許加載用 <script> 標記聲明的任何外部腳本(例如:從 CDN 提取的 JQuery 庫)

建立 DOM 後,用相同的 DOM 方法獲得第一篇文章的 upvote 按鈕,而後單擊。要驗證是否確實單擊了它,能夠檢查 classList 中是否有一個名爲 upmod 的類。若是存在於 classList 中,則返回一條消息。

打開終端並運行 node crawler.js,而後會看到一個整潔的字符串,該字符串將代表帖子是否被贊過。儘管這個例子很簡單,但你能夠在這個基礎上構建功能強大的東西,例如,一個圍繞特定用戶的帖子進行投票的機器人。

若是你不喜歡缺少表達能力的 JSDOM ,而且實踐中要依賴於許多此類操做,或者須要從新建立許多不一樣的 DOM,那麼下面將是更好的選擇。

Puppeteer:無頭瀏覽器

顧名思義,Puppeteer 容許你以編程方式操縱瀏覽器,就像操縱木偶同樣。它經過爲開發人員提供高級 API 來默認控制無頭版本的 Chrome。

摘自 Puppeter Docs Puppeteer 比上述工具更有用,由於它可使你像真正的人在與瀏覽器進行交互同樣對網絡進行爬取。這就具有了一些之前沒有的可能性:

  • 你能夠獲取屏幕截圖或生成頁面 PDF。
  • 能夠抓取單頁應用並生成預渲染的內容。
  • 自動執行許多不一樣的用戶交互,例如鍵盤輸入、表單提交、導航等。

它還能夠在 Web 爬取以外的其餘任務中發揮重要做用,例如 UI 測試、輔助性能優化等。

一般你會想要截取網站的屏幕截圖,也許是爲了瞭解競爭對手的產品目錄,能夠用 puppeteer 來作到。首先運行如下命令安裝 puppeteer,:npm install puppeteer

這將下載 Chromium 的 bundle 版本,根據操做系統的不一樣,該版本大約 180 MB 至 300 MB。若是你要禁用此功能。

讓咱們嘗試在 Reddit 中獲取 r/programming 論壇的屏幕截圖和 PDF,建立一個名爲 crawler.js的新文件,而後複製粘貼如下代碼:

const puppeteer = require('puppeteer')

async function getVisual() {
    try {
        const URL = 'https://www.reddit.com/r/programming/'
        const browser = await puppeteer.launch()
        const page = await browser.newPage()

        await page.goto(URL)
        await page.screenshot({ path: 'screenshot.png' })
        await page.pdf({ path: 'page.pdf' })

        await browser.close()
    } catch (error) {
        console.error(error)
    }
}

getVisual()

getVisual() 是一個異步函數,它將獲 URL 變量中 url 對應的屏幕截圖和 pdf。首先,經過 puppeteer.launch() 建立瀏覽器實例,而後建立一個新頁面。能夠將該頁面視爲常規瀏覽器中的選項卡。而後經過以 URL 爲參數調用 page.goto() ,將先前建立的頁面定向到指定的 URL。最終,瀏覽器實例與頁面一塊兒被銷燬。

完成操做並完成頁面加載後,將分別使用 page.screenshot()page.pdf() 獲取屏幕截圖和 pdf。你也能夠偵聽 javascript load 事件,而後執行這些操做,在生產環境級別下強烈建議這樣作。

在終端上運行 node crawler.js ,幾秒鐘後,你會注意到已經建立了兩個文件,分別名爲 screenshot.jpgpage.pdf

Nightmare:Puppeteer 的替代者

Nightmare 是相似 Puppeteer 的高級瀏覽器自動化庫,該庫使用 Electron,但聽說速度是其前身 PhantomJS 的兩倍。

若是你在某種程度上不喜歡 Puppeteer 或對 Chromium 捆綁包的大小感到沮喪,那麼 nightmare 是一個理想的選擇。首先,運行如下命令安裝 nightmare 庫:npm install nightmare

而後,一旦下載了 nightmare,咱們將用它經過 Google 搜索引擎找到 ScrapingBee 的網站。建立一個名爲crawler.js的文件,而後將如下代碼複製粘貼到其中:

const Nightmare = require('nightmare')
const nightmare = Nightmare()

nightmare
    .goto('https://www.google.com/')
    .type("input[title='Search']", 'ScrapingBee')
    .click("input[value='Google Search']")
    .wait('#rso > div:nth-child(1) > div > div > div.r > a')
    .evaluate(
        () =>
            document.querySelector(
                '#rso > div:nth-child(1) > div > div > div.r > a'
            ).href
    )
    .end()
    .then((link) => {
        console.log('Scraping Bee Web Link': link)
    })
    .catch((error) => {
        console.error('Search failed:', error)
    })

首先建立一個 Nighmare 實例,而後經過調用 goto() 將該實例定向到 Google 搜索引擎,加載後,使用其選擇器獲取搜索框,而後使用搜索框的值(輸入標籤)更改成「ScrapingBee」。完成後,經過單擊 「Google搜索」 按鈕提交搜索表單。而後告訴 Nightmare 等到第一個連接加載完畢,一旦完成,它將使用 DOM 方法來獲取包含該連接的定位標記的 href 屬性的值。

最後,完成全部操做後,連接將打印到控制檯。

總結

  • Node.js 是 Javascript 在服務器端的運行時環境。因爲事件循環機制,它具備「非阻塞」性質。
  • HTTP客戶端(例如 Axios、Superagent 和 Request)用於將 HTTP 請求發送到服務器並接收響應。
  • CheerioJQuery 的優勢抽出來,在服務器端 進行 Web 爬取是惟一的目的,但不執行 Javascript 代碼。
  • JSDOM 根據標準 Javascript規範 從 HTML 字符串中建立一個 DOM,並容許你對其執行DOM操做。
  • Puppeteer and Nightmare高級(high-level )瀏覽器自動化庫,可以讓你以編程方式去操做 Web 應用,就像真實的人正在與之交互同樣。

本文首發微信公衆號:前端先鋒

歡迎掃描二維碼關注公衆號,天天都給你推送新鮮的前端技術文章

歡迎掃描二維碼關注公衆號,天天都給你推送新鮮的前端技術文章

歡迎繼續閱讀本專欄其它高贊文章:


相關文章
相關標籤/搜索