雲栖号資訊:【 點選檢視更多行業資訊】
在這裡您可以找到不同行業的第一手的上雲資訊,還在等什麼,快來!
前提
這篇文章主要針對擁有一定 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”,了解相關資訊可以關注“
”