Puppeteer是谷歌官方出品的一個通過DevTools協(xié)議控制headless Chrome的Node庫抱婉”肷迹可以通過Puppeteer的提供的api直接控制Chrome模擬大部分用戶操作來進行UI Test或者作為爬蟲訪問頁面來收集數(shù)據(jù)。
在Puppeteer出現(xiàn)之前胚想,測試web應用程序通常依靠兩種方式:有頭瀏覽器,和構建在JavaScript引擎之上的無頭瀏覽器。無頭瀏覽器意味著沒有用戶界面暇屋,有頭瀏覽器是最終用戶與產(chǎn)品進行交互的窗口。
這就造成了一種必須權衡的局面:速度(無頭)與可靠性(有頭)洞辣。Puppeter的設計目標是消除這種權衡咐刨,能夠讓開發(fā)人員利用Chromium瀏覽器環(huán)境來運行測試昙衅,并讓他們能夠靈活地利用有頭與無頭瀏覽器來實現(xiàn)用戶場景的測試。
Puppeteer也非常適合開發(fā)人員定鸟,開發(fā)人員可以基于熟悉的測試框架集成Puppeteer來實現(xiàn)而涉、拓展自己的測試能力。
簡單來說使用Puppeteer联予,我們可以實現(xiàn):
- 快速爬取網(wǎng)頁內(nèi)容
- 自動完成表單數(shù)據(jù)提交
- 實現(xiàn)在瀏覽器上的所有自動化操作
- 跟蹤并記錄頁面加載性能
- 瀏覽器屏幕截圖
- 實現(xiàn)自動化測試
- 從網(wǎng)頁生成PDF
環(huán)境和安裝
Puppeteer因為是一個npm的包啼县,所以安裝很簡單:
npm i puppeteer
或者
yarn add puppeteer
Puppeteer安裝時自帶一個最新版本的Chromium,可以通過設置環(huán)境變量或者npm config中的PUPPETEER_SKIP_CHROMIUM_DOWNLOAD跳過下載沸久。如果不下載的話季眷,啟動時可以通過puppeteer.launch([options])配置項中的executablePath指定Chromium的位置。
使用和例子
## 在Node.js文件中加載Puppeteer
const puppeteer = require('puppeteer');
## 使用launch() 方法用于創(chuàng)建瀏覽器的實例對象:
(async () => {
const browser = await puppeteer.launch()
})() // 或者promis形式使用 puppeteer.launch().then(async browser => {})
## 調(diào)用browser對象的newPage() 方法返回頁面對象:
(async () => {
const browser = await puppeteer.launch()
const page = await browser.newPage()
})()
## 在頁面對象上調(diào)用goto() 方法用以訪問測試頁面:
(async () => {
const browser = await puppeteer.launch()
const page = await browser.newPage()
await page.goto('https://website.com')
})()
## 使用page.$() 等同于使用Selectors API querySelector()獲取頁面元素, page.$$() 等同于querySelectorAll()獲取全部頁面滿足條件對象卷胯。
## 測試結束后可以使用close()方法關閉瀏覽器:
browser.close()
上述代碼通過puppeteer的launch方法生成了一個browser的實例子刮,對應于瀏覽器,launch方法可以傳入配置項窑睁,比較有用的是在本地調(diào)試時傳入{headless:false}可以關閉headless模式挺峡。
const browser = await puppeteer.launch({headless:false})
browser.newPage方法可以打開一個新選項卡并返回選項卡的實例page,通過page上的各種方法可以對頁面進行常用操作担钮。上述代碼就進行了截屏和打印pdf的操作沙郭。
一個很強大的方法是page.evaluate(pageFunction, ...args),可以向頁面注入我們的函數(shù)裳朋,這樣就有了無限可能
const puppeteer = require('puppeteer');
(async () => {
const browser = await puppeteer.launch();
const page = await browser.newPage();
await page.goto('http://rennaiqian.com');
// Get the "viewport" of the page, as reported by the page.
const dimensions = await page.evaluate(() => {
return {
width: document.documentElement.clientWidth,
height: document.documentElement.clientHeight,
deviceScaleFactor: window.devicePixelRatio
};
});
console.log('Dimensions:', dimensions);
await browser.close();
})();
需要注意的是evaluate方法中是無法直接使用外部的變量的病线,需要作為參數(shù)傳入,想要獲得執(zhí)行的結果也需要return出來鲤嫡。
調(diào)試技巧
- 關掉無界面模式送挑,有時查看瀏覽器顯示的內(nèi)容是很有用的。使用以下命令可以啟動完整版瀏覽器:
const browser = await puppeteer.launch({headless: false})
- 減慢速度暖眼,slowMo選項以指定的毫秒減慢Puppeteer的操作惕耕。這是另一個看到發(fā)生了什么的方法:
const browser = await puppeteer.launch({
headless:false,
slowMo:250
});
3.捕獲console的輸出,通過監(jiān)聽console事件。在page.evaluate里調(diào)試代碼時這也很方便:
page.on('console', msg => console.log('PAGE LOG:', ...msg.args));
await page.evaluate(() => console.log(`url is ${location.href}`));
4.啟動詳細日志記錄诫肠,所有公共API調(diào)用和內(nèi)部協(xié)議流量都將通過puppeteer命名空間下的debug模塊進行記錄
# Basic verbose logging
env DEBUG="puppeteer:*" node script.js
# Debug output can be enabled/disabled by namespace
env DEBUG="puppeteer:*,-puppeteer:protocol" node script.js # everything BUT protocol messages
env DEBUG="puppeteer:session" node script.js # protocol session messages (protocol messages to targets)
env DEBUG="puppeteer:mouse,puppeteer:keyboard" node script.js # only Mouse and Keyboard API calls
# Protocol traffic can be rather noisy. This example filters out all Network domain messages
env DEBUG="puppeteer:*" env DEBUG_COLORS=true node script.js 2>&1 | grep -v '"Network'
爬蟲實踐
很多網(wǎng)頁通過user-agent來判斷設備司澎,可以通過page.emulate(options)來進行模擬。options有兩個配置項栋豫,一個為userAgent挤安,另一個為viewport可以設置寬度(width)、高度(height)丧鸯、屏幕縮放(deviceScaleFactor)蛤铜、是否是移動端(isMobile)、有無touch事件(hasTouch)。
const puppeteer = require('puppeteer');
const devices = require('puppeteer/DeviceDescriptors');
const iPhone = devices['iPhone 6'];
puppeteer.launch().then(async browser => {
const page = await browser.newPage();
await page.emulate(iPhone);
await page.goto('https://www.example.com');
// other actions...
await browser.close();
});
上述代碼則模擬了iPhone6訪問某網(wǎng)站围肥,其中devices是puppeteer內(nèi)置的一些常見設備的模擬參數(shù)剿干。
很多網(wǎng)頁需要登錄,有兩種解決方案:
- 讓puppeteer去輸入賬號密碼
常用方法:點擊可以使用page.click(selector[, options])方法穆刻,也可以選擇聚焦page.focus(selector)置尔。
輸入可以使用page.type(selector, text[, options])輸入指定的字符串,還可以在options中設置delay緩慢輸入更像真人一些氢伟。也可以使用keyboard.down(key[, options])來一個字符一個字符的輸入榜轿。
- 如果是通過cookie判斷登錄狀態(tài)的可以通過page.setCookie(...cookies),想要維持cookie可以定時訪問腐芍。
Tip:有些網(wǎng)站需要掃碼差导,但是相同域名的其他網(wǎng)頁卻有登錄试躏,就可以嘗試去可以登錄的網(wǎng)頁登錄完利用cookie訪問跳過掃碼猪勇。
簡單例子
const puppeteer = require('puppeteer');
(async () => {
const browser = await puppeteer.launch({headless: false});
const page = await browser.newPage();
await page.goto('https://baidu.com');
await page.type('#kw', 'puppeteer', {delay: 100});
page.click('#su')
await page.waitFor(1000);
const targetLink = await page.evaluate(() => {
return [...document.querySelectorAll('.result a')].filter(item => {
return item.innerText && item.innerText.includes('Puppeteer的入門和實踐')
}).toString()
});
await page.goto(targetLink);
await page.waitFor(1000);
browser.close();
})()
Page對象內(nèi)置函數(shù)
page.$() 提供對頁面上的選擇器API方法querySelector()的訪問權限
page.$$() 提供對頁面上的選擇器API方法querySelectorAll()的訪問權限
page.$eval() 接收2個或多個參數(shù)。第一個是選擇器颠蕴,第二個是函數(shù)泣刹。如果有更多參數(shù),則這些參數(shù)將作為附加參數(shù)傳遞給函數(shù)犀被。
const innerTextOfButton = await page.$eval('button#submit', el => el.innerText)
click() 對頁面元素進行點擊事件模擬
await page.click('button#submit')
content() 獲取HTML的頁面源代碼
emulate() 模擬設備信息椅您,可以通過配置Agent和viewport 模擬不同設備訪問網(wǎng)站。
const puppeteer = require('puppeteer');
const device = require('puppeteer/DeviceDescriptors')['iPhone X'];
puppeteer.launch().then(async browser => {
const page = await browser.newPage()
await page.emulate(device)
//testing
await browser.close()
})
evaluate() 在頁上下文中執(zhí)行用戶指定的函數(shù)寡键。在這個函數(shù)中掀泳,可以直接訪問document對象,因此可以調(diào)用任何DOM API:
const puppeteer = require('puppeteer');
(async () => {
const browser = await puppeteer.launch()
const page = await browser.newPage()
await page.goto('https://website.com')
const result = await page.evaluate(() => {
return document.querySelectorAll('.footer-tags a').length
})
console.log(result)
})()
ocus() 選中當前元素
goBack() 后退頁面
goForward() 前進頁面
goto() 打開某一網(wǎng)址
hover() 懸停在某元素上
pdf() 生成當前頁面的PDF文檔
reload() 重新加載當前頁面
screenshot() 截圖當前頁面并存儲為PNG圖片
setViewPort() 默認的viewport 為800x600px西轩, 可以通過setViewport 來模擬不同窗口尺寸
waitFor() 等待某些條件成立员舵,內(nèi)置以下等待條件:
- waitForFunction
- waitForNavigation
- waitForRequest
- waitForResponse
- waitForSelector
- waitForXPath
await page.waitFor(waitForNameToBeFilled)
const waitForNameToBeFilled = () => page.$('input#name').value != ''