作者簡介 felix 螞蟻金服·資料體驗技術團隊
我們日常使用瀏覽器的步驟為:啟動瀏覽器、開啟一個網頁、進行互動。而無頭瀏覽器
指的是我們使用指令碼來執行以上過程的瀏覽器,能模擬真實的瀏覽器使用場景。
有了無頭瀏覽器,我們就能做包括但不限於以下事情:
- 對網頁進行截圖儲存為圖片或 pdf
- 抓取單頁應用(SPA)執行並渲染(解決傳統 HTTP 爬蟲抓取單頁應用難以處理非同步請求的問題)
- 做表單的自動提交、UI的自動化測試、模擬鍵盤輸入等
- 用瀏覽器自帶的一些除錯工具和效能分析工具幫助我們分析問題
- 在最新的無頭瀏覽器環境裡做測試、使用最新瀏覽器特性
- 寫爬蟲做你想做的事情~
無頭瀏覽器很多,包括但不限於:
- PhantomJS, 基於 Webkit
- SlimerJS, 基於 Gecko
- HtmlUnit, 基於 Rhnio
- TrifleJS, 基於 Trident
- Splash, 基於 Webkit
本文主要介紹 Google 提供的無頭瀏覽器(headless Chrome), 他基於 Chrome DevTools protocol 提供了不少高度封裝的介面方便我們控制瀏覽器。
簡單的程式碼示例
為了能使用
async
/await
等新特性,需要使用 v7.6.0 或更高版本的 Node.
啟動/關閉瀏覽器、開啟頁面
// 啟動瀏覽器 const browser = await puppeteer.launch({
// 關閉無頭模式,方便我們看到這個無頭瀏覽器執行的過程 // headless: false, timeout: 30000, // 預設超時為30秒,設定為0則表示不設定超時
});
// 開啟空白頁面 const page = await browser.newPage();
// 進行互動 // ... // 關閉瀏覽器 // await browser.close();
複製程式碼
設定頁面視窗大小
// 設定瀏覽器視窗 page.setViewport({
width: 1376, height: 768,
});
複製程式碼
輸入網址
// 位址列輸入網頁地址 await page.goto('https://google.com/', {
// 配置項 // waitUntil: 'networkidle', // 等待網路狀態為空閒的時候才繼續執行
});
複製程式碼
儲存網頁為圖片
開啟一個網頁,然後截圖儲存到本地:
await page.screenshot({
path: 'path/to/saved.png',
});
複製程式碼
儲存網頁為 pdf
開啟一個網頁,然後儲存 pdf 到本地:
await page.pdf({
path: 'path/to/saved.pdf', format: 'A4', // 儲存尺寸
});
複製程式碼
執行指令碼
要獲取開啟的網頁中的宿主環境,我們可以使用 Page.evaluate
方法:
// 獲取視窗資訊const dimensions = await page.evaluate(() =>
{
return {
width: document.documentElement.clientWidth, height: document.documentElement.clientHeight, deviceScaleFactor: window.devicePixelRatio
};
});
console.log('視窗資訊:', dimensions);
// 獲取 html// 獲取上下文控制程式碼const htmlHandle = await page.$('html');
// 執行計算const html = await page.evaluate(body =>
body.outerHTML, htmlHandle);
// 銷燬控制程式碼await htmlHandle.dispose();
console.log('html:', html);
複製程式碼
Page.$
可以理解為我們常用的 document.querySelector
, 而 Page.$$
則對應 document.querySelectorAll
。
自動提交表單
開啟谷歌首頁,輸入關鍵字,回車進行搜尋:
// 位址列輸入網頁地址await page.goto('https://google.com/', {
waitUntil: 'networkidle', // 等待網路狀態為空閒的時候才繼續執行
});
// 聚焦搜尋框// await page.click('#lst-ib');
await page.focus('#lst-ib');
// 輸入搜尋關鍵字await page.type('辣子雞', {
delay: 1000, // 控制 keypress 也就是每個字母輸入的間隔
});
// 回車await page.press('Enter');
複製程式碼
複雜點的程式碼示例
每一個簡單的動作連線起來,就是一連串複雜的互動,接下來我們看兩個更具體的示例。
抓取單頁應用: 模擬餓了麼外賣下單
傳統的爬蟲是基於 HTTP 協議,模擬 UserAgent 傳送 http 請求,獲取到 html 內容後使用正則解析出需要抓取的內容,這種方式面對服務端渲染直出 html 的網頁時非常便捷。
但遇到單頁應用(SPA)時,或遇到登入校驗時,這種爬蟲就顯得比較無力。
而使用無頭瀏覽器,抓取網頁時完全使用了人機互動時的操作,所以頁面的初始化完全能使用宿主瀏覽器環境渲染完備,不再需要關心這個單頁應用在前端初始化時需要涉及哪些 HTTP 請求。
無頭瀏覽器提供的各種點選、輸入等指令,完全模擬人的點選、輸入等指令,也就再也不用擔心正則寫不出來了啊哈哈哈
當然,有些場景下,使用傳統的 HTTP 爬蟲(寫正則匹配) 還是比較高效的。
在這裡就不再詳細對比這些差異了,以下這個例子僅作為展示模擬一個完整的人機互動:使用移動版餓了麼點外賣。
先看下效果:
程式碼比較長就不全貼了,關鍵是幾行:
const puppeteer = require('puppeteer');
const devices = require('puppeteer/DeviceDescriptors');
const iPhone6 = devices['iPhone 6'];
console.log('啟動瀏覽器');
const browser = await puppeteer.launch();
console.log('開啟頁面');
const page = await browser.newPage();
// 模擬移動端裝置await page.emulate(iPhone6);
console.log('位址列輸入網頁地址');
await page.goto(url);
console.log('等待頁面準備好');
await page.waitForSelector('.search-wrapper .search');
console.log('點選搜尋框');
await page.tap('.search-wrapper .search');
await page.type('麥當勞', {
delay: 200, // 每個字母之間輸入的間隔
});
console.log('回車開始搜尋');
await page.tap('button');
console.log('等待搜素結果渲染出來');
await page.waitForSelector('[class^="index-container"]');
console.log('找到搜尋到的第一家外賣店!');
await page.tap('[class^="index-container"]');
console.log('等待選單渲染出來');
await page.waitForSelector('[class^="fooddetails-food-panel"]');
console.log('直接選一個菜品吧');
await page.tap('[class^="fooddetails-cart-button"]');
// console.log('===為了看清楚,傲嬌地等兩秒===');
await page.waitFor(2000);
await page.tap('[class^=submit-btn-submitbutton]');
// 關閉瀏覽器await browser.close();
複製程式碼
關鍵步驟是:
- 載入頁面
- 等待需要點選的 DOM 渲染出來後點選
- 繼續等待下一步需要點選的 DOM 渲染出來再點選
關鍵的幾個指令:
page.tap
(或page.click
) 為點選page.waitForSelector
意思是等待指定元素出現在網頁中,如果已經出現了,則立即繼續執行下去, 後面跟的引數為selector
選擇器,與我們常用的document.querySelector
接收的引數一致page.waitFor
後面可以傳入selector
選擇器、function
函式或timeout
毫秒時間,如page.waitFor(2000)
指等待2秒再繼續執行,例子中用這個函式暫停操作主要是為了演示
以上幾個指令都可接受一個 selector
選擇器作為引數,這裡額外介紹幾個方法:
page.$(selector)
與我們常用的document.querySelector(selector)
一致,返回的是一個ElementHandle
元素控制程式碼page.$$(selector)
與我們常用的document.querySelectorAll(selector)
一致,返回的是一個陣列
在有頭瀏覽器上下文中,我們選擇一個元素的方法是:
const body = document.querySelector('body');
const bodyInnerHTML = body.innerHTML;
console.log('bodyInnerHTML: ', bodyInnerHTML);
複製程式碼
而在無頭瀏覽器裡,我們首先需要獲取一個控制程式碼,通過控制程式碼獲取到環境中的資訊後,銷燬這個控制程式碼。
// 獲取 html// 獲取上下文控制程式碼const bodyHandle = await page.$('body');
// 執行計算const bodyInnerHTML = await page.evaluate(dom =>
dom.innerHTML, bodyHandle);
// 銷燬控制程式碼await bodyHandle.dispose();
console.log('bodyInnerHTML:', bodyInnerHTML);
複製程式碼
除此之外,還可以使用 page.$eval
:
const bodyInnerHTML = await page.$eval('body', dom =>
dom.innerHTML);
console.log('bodyInnerHTML: ', bodyInnerHTML);
複製程式碼
page.evaluate
意為在瀏覽器環境執行指令碼,可傳入第二個引數作為控制程式碼,而 page.$eval
則針對選中的一個 DOM 元素執行操作。
匯出批量網頁:下載圖靈圖書
我在 圖靈社群 上買了不少電子書,以前支援推送到 mobi
格式到 kindle
或推送 pdf
格式到郵箱進行閱讀,不過經常會關閉這些推送渠道,只能留在網頁上看書。
對我來說不是很方便,而這些書籍的線上閱讀效果是伺服器渲染出來的(帶了大量標籤,無法簡單抽取出好的排版),最好的方式當然是直接線上閱讀並儲存為 pdf 或圖片了。
藉助瀏覽器的無頭模式,我寫了個簡單的下載已購買書籍為 pdf
到本地的指令碼,支援批量下載已購買的書籍。
使用方法,傳入帳號密碼和儲存路徑,如:
$ node ./demo/download-ituring-books.js '使用者名稱' '密碼' './books'複製程式碼
注意:puppeteer
的 Page.pdf()
目前僅支援在無頭模式中使用,所以要想看有頭狀態的抓取過程的話,執行到 Page.pdf()
這步會先報錯:
所以啟動這個指令碼時,需要保持無頭模式:
const browser = await puppeteer.launch({
// 關閉無頭模式,方便我們看到這個無頭瀏覽器執行的過程 // 注意若呼叫了 Page.pdf 即儲存為 pdf,則需要保持為無頭模式 // headless: false,
});
複製程式碼
看下執行效果:
我的書架裡有20多本書,下載完後是這樣子:
無頭瀏覽器還能做什麼?
無頭瀏覽器說白了就是能模擬人工在有頭瀏覽器中的各種操作。那自然很多人力活,都能使用無頭瀏覽器來做(比如上面這個下載 pdf 的過程,其實是人力開啟每一個文章頁面,然後按 ctrl+p
或 command+p
儲存到本地的自動化過程)。
那既然用自動化工具能解決的事情,就不應該浪費重複的人力勞動了,除此之外我們還可以做:
- 自動化工具如自動提交表單,自動下載
- 自動化 UI 測試如記錄下正確 DOM 結構或截圖,然後自動執行指定操作後,檢查 DOM 結構或截圖是否匹配(UI 斷言)
- 定時監控工具如定時截圖發週報,或定時巡查重要業務路徑下的頁面是否處於可用狀態,配合郵件告警
- 爬蟲如傳統 HTTP 爬蟲爬不到的地方,就可配合無頭瀏覽器渲染能力來做
- etc
感興趣的同學可以關注專欄或者傳送簡歷至’qingsheng.lqs####alibaba-inc.com’.replace(‘####’, ‘@’),歡迎有志之士加入~