做者:Shenesh Perera翻譯:瘋狂的技術宅javascript
原文:https://www.scrapingbee.com/b...html
未經容許嚴禁轉載前端
本文講解怎樣用 Node.js 高效地從 Web 爬取數據。java
本文主要針對具備必定 JavaScript 經驗的程序員。若是你對 Web 抓取有深入的瞭解,但對 JavaScript 並不熟悉,那麼本文仍然可以對你有所幫助。node
經過本文你將學到:ios
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 客戶端是可以將請求發送到服務器,而後接收服務器響應的工具。下面提到的全部工具底的層都是用 HTTP 客戶端來訪問你要抓取的網站。
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 是基於 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庫。
與 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>
標記的 textContent
或 innerHTML
。可是結果中包含一些不須要的文本( 「Username: 「),必須將其刪除。
如你所見,對於一個很是簡單的用例,步驟和要作的工做都不少。這就是爲何應該依賴 HTML 解析器的緣由,咱們將在後面討論。
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 應用是嚴重依賴 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.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 項執行如下功能:
dangerously
時容許執行事件 handler 和任何 Javascript 代碼。若是你不清楚將要運行的腳本的安全性,則最好將 runScripts 設置爲「outside-only」,這會把全部提供的 Javascript 規範附加到 「window」 對象,從而阻止在 inside 上執行的任何腳本。<script>
標記聲明的任何外部腳本(例如:從 CDN 提取的 JQuery 庫)建立 DOM 後,用相同的 DOM 方法獲得第一篇文章的 upvote 按鈕,而後單擊。要驗證是否確實單擊了它,能夠檢查 classList
中是否有一個名爲 upmod
的類。若是存在於 classList
中,則返回一條消息。
打開終端並運行 node crawler.js
,而後會看到一個整潔的字符串,該字符串將代表帖子是否被贊過。儘管這個例子很簡單,但你能夠在這個基礎上構建功能強大的東西,例如,一個圍繞特定用戶的帖子進行投票的機器人。
若是你不喜歡缺少表達能力的 JSDOM ,而且實踐中要依賴於許多此類操做,或者須要從新建立許多不一樣的 DOM,那麼下面將是更好的選擇。
顧名思義,Puppeteer 容許你以編程方式操縱瀏覽器,就像操縱木偶同樣。它經過爲開發人員提供高級 API 來默認控制無頭版本的 Chrome。
Puppeteer 比上述工具更有用,由於它可使你像真正的人在與瀏覽器進行交互同樣對網絡進行爬取。這就具有了一些之前沒有的可能性:
它還能夠在 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.jpg
和 page.pdf
。
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
屬性的值。
最後,完成全部操做後,連接將打印到控制檯。