JavaScript 自動化爬蟲入門指北(Chrome + Puppeteer + Node JS)
和 Headless Chrome 一起裝逼一起飛
Udemy Black Friday Sale — Thousands of Web Development & Software Development courses are on sale for only $10 for a limited time! Full details and course recommendations can be found here.
內容簡介
本文將會教你如何用 JavaScript 自動化 web 爬蟲,技術上用到了 Google 團隊開發的 Puppeteer。 Puppeteer 執行在 Node 環境,可以用來操作 headless Chrome。何謂 Headless Chrome?通俗來講就是在不開啟 Chrome 瀏覽器的情況下使用提供的 API 模擬使用者的瀏覽行為。
如果你還是不理解,你可以想象成使用 JavaScript 全自動化操作 Chrome 瀏覽器。
前言
先確保你已經安裝了 Node 8 及以上的版本,沒有的話,可以先到 官網 裡下載安裝。注意,一定要選“Current”處顯示的版本號大於 8 的。
如果你是第一次接觸 Node,最好先看一下入門教程:Learn Node JS — The 3 Best Online Node JS Courses.
安裝好 Node 之後,建立一個專案資料夾,然後安裝 Puppeteer。安裝 Puppeteer 的過程中會附帶下載匹配版本的 Chromium(譯者注:國內網路環境可能會出現安裝失敗的問題,可以設定環境變數 PUPPETEER_SKIP_CHROMIUM_DOWNLOAD = 1
跳過下載,副作用是每次使用 launch
方法時,需要手動指定瀏覽器的執行路徑):
npm install --save puppeteer
複製程式碼
例 1 —— 網頁截圖
Puppeteer 安裝好之後,我們就可以開始寫一個簡單的例子。這個例子直接照搬自官方文件,它可以對給定的網站進行截圖。
首先建立一個 js 檔案,名字隨便起,這裡我們用 test.js
作為示例,輸入以下程式碼:
const puppeteer = require('puppeteer');
async function getPic() {
const browser = await puppeteer.launch();
const page = await browser.newPage();
await page.goto('https://google.com');
await page.screenshot({path: 'google.png'});
await browser.close();
}
getPic();
複製程式碼
下面我們來逐行分析上面的程式碼。
- 第 1 行: 引入依賴。
- 第 3–10 行: 核心程式碼,自動化過程在這裡完成。
- 第 12 行: 執行
getPic()
方法。
細心的讀者會發現,getPic()
前面有個 async
字首,它表示 getPic()
方法是個非同步方法。async
和 await
成對出現,屬於 ES 2017 新特性。介於它是個非同步方法,所以呼叫之後返回的是 Promise
物件。當 async
方法返回值時,對應的 Promise
物件會將這個值傳遞給 resolve
(如果丟擲異常,那麼會將錯誤資訊傳遞給 Reject
)。
在 async
方法中,可以使用 await
表示式暫停方法的執行,直到表示式裡的 Promise
物件完全解析之後再繼續向下執行。看不懂沒關係,後面我再詳細講解,到時候你就明白了。
接下來,我們將會深入分析 getPic()
方法:
- 第 4 行:
const browser = await puppeteer.launch();
複製程式碼
這段程式碼用於啟動 puppeteer,實質上開啟了一個 Chrome 的例項,然後將這個例項物件賦給變數 browser
。因為使用了 await
關鍵字,程式碼執行到這裡會阻塞(暫停),直到 Promise
解析完畢(無論執行結果是否成功)
- 第 5 行:
const page = await browser.newPage();
複製程式碼
接下來,在上文獲取到的瀏覽器例項中新建一個頁面,等到其返回之後將新建的頁面物件賦給變數 page
。
- 第 6 行:
await page.goto('https://google.com');
複製程式碼
使用上文獲取到的 page
物件,用它來載入我們給的 URL 地址,隨後程式碼暫停執行,等待頁面載入完畢。
- 第 7 行:
await page.screenshot({path: 'google.png'});
複製程式碼
等到頁面載入完成之後,就可以對頁面進行截圖了。screenshot()
方法接受一個物件引數,可以用來配置截圖儲存的路徑。注意,不要忘了加上 await
關鍵字。
- 第 9 行:
await browser.close();
複製程式碼
最後,關閉瀏覽器。
執行示例
在命令列輸入以下命令執行示例程式碼:
node test.js
複製程式碼
以下是示例裡的截圖結果:
是不是很厲害?這只是熱身,下面教你怎麼在非 headless 環境下執行程式碼。
非 headless?百聞不如一見,自己先動手試一下吧,把第 4 行的程式碼:
const browser = await puppeteer.launch();
複製程式碼
換成這句:
const browser = await puppeteer.launch({headless: false});
複製程式碼
然後再次執行:
node test.js
複製程式碼
是不是更炫酷了?當配置了 {headless: false}
之後,就可以直觀的看到程式碼是怎麼操控 Chrome 瀏覽器的。
這裡還有一個小問題,之前我們的截圖有點沒截完整的感覺,那是因為 page
物件預設的截圖尺寸有點小的緣故,我們可以通過下面的程式碼重新設定 page
的視口大小,然後再擷取:
await page.setViewport({width: 1000, height: 500})
複製程式碼
這下就好多了:
最終程式碼如下:
const puppeteer = require('puppeteer');
async function getPic() {
const browser = await puppeteer.launch({headless: false});
const page = await browser.newPage();
await page.goto('https://google.com');
await page.setViewport({width: 1000, height: 500})
await page.screenshot({path: 'google.png'});
await browser.close();
}
getPic();
複製程式碼
例 2 —— 爬取資料
通過上面的例子,你應該掌握了 Puppeteer 的基本用法,下面再來看一個稍微複雜點的例子。
開始前,不妨先看看 官方文件。你會發現 Puppeteer 能幹很多事,像是模擬滑鼠的點選、填充表單資料、輸入文字、讀取頁面資料等。
在接下來的教程裡,我們將爬一個叫 Books To Scrape 的網站,這個網站是專門用來給開發者做爬蟲練習用的。
還是在之前建立的資料夾裡,新建一個 js 檔案,這裡用 scrape.js
作為示例,然後輸入以下程式碼:
const puppeteer = require('puppeteer');
let scrape = async () => {
// Actual Scraping goes Here...
// Return a value
};
scrape().then((value) => {
console.log(value); // Success!
});
複製程式碼
有了上一個例子的經驗,這段程式碼要看懂應該不難。如果你還是看不懂的話......那也沒啥問題就是了。
首先,還是引入 puppeteer
依賴,然後定義一個 scrape()
方法,用來寫爬蟲程式碼。這個方法返回一個值,到時候我們會處理這個返回值(示例程式碼是直接列印出這個值)
先在 scrape 方法中新增下面這一行測試一下:
let scrape = async () => {
return 'test';
};
複製程式碼
在命令列輸入 node scrape.js
,不出問題的話,控制檯會列印一個 test
字串。測試通過後,我們來繼續完善 scrape
方法。
步驟 1:前期準備
和例 1 一樣,先獲取瀏覽器例項,再新建一個頁面,然後載入 URL:
let scrape = async () => {
const browser = await puppeteer.launch({headless: false});
const page = await browser.newPage();
await page.goto('http://books.toscrape.com/');
await page.waitFor(1000);
// Scrape
browser.close();
return result;
};
複製程式碼
再來分析一下上面的程式碼:
首先,我們建立了一個瀏覽器例項,將 headless
設定為 false
,這樣就能直接看到瀏覽器的操作過程:
const browser = await puppeteer.launch({headless: false});
複製程式碼
然後建立一個新標籤頁:
const page = await browser.newPage();
複製程式碼
訪問 books.toscrape.com
:
await page.goto('http://books.toscrape.com/');
複製程式碼
下面這一步可選,讓程式碼暫停執行 1 秒,保證頁面能完全載入完畢:
await page.waitFor(1000);
複製程式碼
任務完成之後關閉瀏覽器,返回執行結果。
browser.close();
return result;
複製程式碼
步驟 1 結束。
步驟 2: 開爬
開啟 Books to Scrape 網站之後,想必你也發現了,這裡面有海量的書籍,只是資料都是假的而已。先從簡單的開始,我們先抓取頁面裡第一本書的資料,返回它的標題和價格資訊(紅色邊框選中的那本)。
查一下文件,注意到這個方法能模擬頁面點選:
page.click(selector[, options])
selector
選擇器,定位需要進行點選的元素,如果有多個元素匹配,以第一個為準。
這裡可以使用開發者工具檢視元素的選擇器,在圖片上右擊選中 inspect:
上面的操作會開啟開發者工具欄,之前選中的元素也會被高亮顯示,這個時候點選前面的三個小點,選擇 copy - copy selector:
有了元素的選擇器之後,再加上之前查到的元素點選方法,得到如下程式碼:
await page.click('#default > div > div > div > div > section > div:nth-child(2) > ol > li:nth-child(1) > article > div.image_container > a > img');
複製程式碼
然後就會觀察到瀏覽器點選了第一本書的圖片,頁面也會跳轉到詳情頁。
在詳情頁裡,我們只關心書的標題和價格資訊 —— 見圖中紅框標註。
為了獲取這些資料,需要用到 page.evaluate()
方法。這個方法可以用來執行瀏覽器內建 DOM API ,例如 querySelector()
。
首先建立 page.evaluate()
方法,將其返回值儲存在 result
變數中:
const result = await page.evaluate(() => {
// return something
});
複製程式碼
同樣,要在方法裡選擇我們要用到的元素,再次開啟開發者工具,選擇需要 inspect 的元素:
標題是個簡單的 h1
元素,使用下面的程式碼獲取:
let title = document.querySelector('h1');
複製程式碼
其實我們需要的只是元素裡的文字部分,可以在後面加上 .innerText
,程式碼如下:
let title = document.querySelector('h1').innerText;
複製程式碼
獲取價格資訊同理:
剛好價格元素上有個 price_color
class,可以用這個 class 作為選擇器獲取到價格對應的元素:
let price = document.querySelector('.price_color').innerText;
複製程式碼
這樣,標題和價格都有了,把它們放到一個物件裡返回:
return {
title,
price
}
複製程式碼
回顧剛才的操作,我們獲取到了標題和價格資訊,將它們儲存在一個物件裡返回,返回結果賦給 result
變數。所以,現在你的程式碼應該是這樣:
const result = await page.evaluate(() => {
let title = document.querySelector('h1').innerText;
let price = document.querySelector('.price_color').innerText;
return {
title,
price
}
});
複製程式碼
然後只需要將 result
返回即可,返回結果會列印到控制檯:
return result;
複製程式碼
最後,綜合起來程式碼如下:
const puppeteer = require('puppeteer');
let scrape = async () => {
const browser = await puppeteer.launch({headless: false});
const page = await browser.newPage();
await page.goto('http://books.toscrape.com/');
await page.click('#default > div > div > div > div > section > div:nth-child(2) > ol > li:nth-child(1) > article > div.image_container > a > img');
await page.waitFor(1000);
const result = await page.evaluate(() => {
let title = document.querySelector('h1').innerText;
let price = document.querySelector('.price_color').innerText;
return {
title,
price
}
});
browser.close();
return result;
};
scrape().then((value) => {
console.log(value); // Success!
});
複製程式碼
在控制檯執行程式碼:
node scrape.js
// { title: 'A Light in the Attic', price: '£51.77' }
複製程式碼
操作正確的話,在控制檯會看到正確的輸出結果,到此為止,你已經完成了 web 爬蟲。
例 3 —— 後期完善
稍加思考一下你會發現,標題和價格資訊是直接展示在首頁的,所以,完全沒必要進入詳情頁去抓取這些資料。既然這樣,不妨再進一步思考,能否抓取所有書的標題和價格資訊?
所以,抓取的方式其實有很多,需要你自己去發現。另外,上面提到的直接在主頁抓取資料也不一定可行,因為有些標題可能會顯示不全。
拔高題
目標 —— 抓取主頁所有書籍的標題和價格資訊,並且用陣列的形式儲存返回。正確的輸出應該是這樣:
開幹吧,夥計,其實實現起來和上面的例子相差無幾,如果你覺得實在太難,可以參考下面的提示。
提示:
其實最大的區別在於你需要遍歷整個結果集,程式碼的大致結構如下:
const result = await page.evaluate(() => {
let data = []; // 建立一個空陣列
let elements = document.querySelectorAll('xxx'); // 選擇所有相關元素
// 遍歷所有的元素
// 提取標題資訊
// 提取價格資訊
data.push({title, price}); // 將資料插入到陣列中
return data; // 返回資料集
});
複製程式碼
如果提示了還是做不出來的話,好吧,以下是參考答案。在以後的教程中,我會在下面這段程式碼的基礎上再做一些擴充,同時也會涉及一些更高階的爬蟲技術。你可以在 這裡 提交你的郵箱地址進行訂閱,有新的內容更新時我們會通知你。
參考答案:
const puppeteer = require('puppeteer');
let scrape = async () => {
const browser = await puppeteer.launch({headless: false});
const page = await browser.newPage();
await page.goto('http://books.toscrape.com/');
const result = await page.evaluate(() => {
let data = []; // 建立一個陣列儲存結果
let elements = document.querySelectorAll('.product_pod'); // 選擇所有書籍
for (var element of elements){ // 遍歷書籍列表
let title = element.childNodes[5].innerText; // 提取標題資訊
let price = element.childNodes[7].children[0].innerText; // 提取價格資訊
data.push({title, price}); // 組合資料放入陣列
}
return data; // 返回資料集
});
browser.close();
return result; // 返回資料
};
scrape().then((value) => {
console.log(value); // 列印結果
});
複製程式碼
結語:
謝謝觀看!如果你有學習 NodeJS 的意向,可以移步 Learn Node JS — The 3 Best Online Node JS Courses。
每週我都會發布 4 篇有關 web 開發的技術文章,歡迎訂閱!或者你也可以在 Twitter 上 關注我
掘金翻譯計劃 是一個翻譯優質網際網路技術文章的社群,文章來源為 掘金 上的英文分享文章。內容覆蓋 Android、iOS、前端、後端、區塊鏈、產品、設計、人工智慧等領域,想要檢視更多優質譯文請持續關注 掘金翻譯計劃、官方微博、知乎專欄。