Puppeteer 是 Chrome 開發團隊在 2017 年發佈的一個 Node.js 包,用來模擬 Chrome 瀏覽器的運行。咱們團隊從 Puppeteer 剛發佈出來就開始成爲忠實用戶了(主要是由於 PhantomJs 坑太多了),本文主要在介紹 Puppeteer 的同時,結合咱們平時的實踐作一個分享。javascript
學習 Puppeteer 以前咱們先來了解一下 Chrome DevTool Protocolcss
GET /json/version # 獲取瀏覽器的一些元信息
GET /json or /json/list # 當前瀏覽器上打開的一些頁面信息
GET /json/protocol # 獲取當前 CDP 的協議信息
GET /json/new?{url} # 開啓一共新的 Tab 頁面
GET /json/activate/{targetId} # 激活某個頁面成爲當前顯示的頁面
GET /json/close/{targetId} # 關閉某個頁面
GET /devtools/inspector.html # 打開當前頁面的開發者調試工具
WebSocket /devtools/page/{targetId} # 獲取某個頁面的 websocket 地址
複製代碼
alias chrome="/Applications/Google\ Chrome.app/Contents/MacOS/Google\ Chrome" # Mac OS X 命令別名
chrome --headless --remote-debugging-port=9222 --disable-gpu # 開啓遠程調試
chrome --headless --disable-gpu --dump-dom https://www.baidu.com # 獲取頁面 DOM
chrome --headless --disable-gpu --screenshot https://www.baidu.com # 截圖
複製代碼
官方稱:「Most things that you can do manually in the browser can be done using Puppeteer」,那麼具體能夠作些什麼呢?html
Puppeteer 中的 API 分層結構基本和瀏覽器保持一致,下面對常使用到的幾個類介紹一下:前端
puppeteer 提供了兩種方法用於建立一個 Browser 實例:java
const puppeteer = require('puppeteer');
let request = require('request-promise-native');
//使用 puppeteer.launch 啓動 Chrome
(async () => {
const browser = await puppeteer.launch({
headless: false, //有瀏覽器界面啓動
slowMo: 100, //放慢瀏覽器執行速度,方便測試觀察
args: [ //啓動 Chrome 的參數,詳見上文中的介紹
'–no-sandbox',
'--window-size=1280,960'
],
});
const page = await browser.newPage();
await page.goto('https://www.baidu.com');
await page.close();
await browser.close();
})();
//使用 puppeteer.connect 鏈接一個已經存在的 Chrome 實例
(async () => {
//經過 9222 端口的 http 接口獲取對應的 websocketUrl
let version = await request({
uri: "http://127.0.0.1:9222/json/version",
json: true
});
//直接鏈接已經存在的 Chrome
let browser = await puppeteer.connect({
browserWSEndpoint: version.webSocketDebuggerUrl
});
const page = await browser.newPage();
await page.goto('https://www.baidu.com');
await page.close();
await browser.disconnect();
})();
複製代碼
這兩種方式的對比:git
在實踐中咱們常常會遇到如何判斷一個頁面加載完成了,什麼時機去截圖,什麼時機去點擊某個按鈕等問題,那咱們到底如何去等待加載呢?github
下面咱們把等待加載的 API 分爲三類進行介紹:web
Pupeeteer 中的基本上全部的操做都是異步的,以上幾個 API 都涉及到關於打開一個頁面,什麼狀況下才能判斷這個函數執行完畢呢,這些函數都提供了兩個參數 waitUtil 和 timeout,waitUtil 表示直到什麼出現就算執行完畢,timeout 表示若是超過這個時間尚未結束就拋出異常。chrome
await page.goto('https://www.baidu.com', {
timeout: 30 * 1000,
waitUntil: [
'load', //等待 「load」 事件觸發
'domcontentloaded', //等待 「domcontentloaded」 事件觸發
'networkidle0', //在 500ms 內沒有任何網絡鏈接
'networkidle2' //在 500ms 內網絡鏈接個數不超過 2 個
]
});
複製代碼
以上 waitUtil 有四個事件,業務能夠根據需求來設置其中一個或者多個觸發才覺得結束,networkidle0 和 networkidle2 中的 500ms 對時間性能要求高的用戶來講,仍是有點長的docker
await page.waitForXPath('//img');
await page.waitForSelector('#uniqueId');
await page.waitForResponse('https://d.youdata.netease.com/api/dash/hello');
await page.waitForRequest('https://d.youdata.netease.com/api/dash/hello');
複製代碼
若是上面提供的等待方式都不能知足咱們的需求,puppeteer 還提供咱們提供兩個函數:
await page.goto(url, {
timeout: 120000,
waitUntil: 'networkidle2'
});
//咱們能夠在頁面中定義本身認爲加載完的事件,在合適的時間點咱們將該事件設置爲 true
//如下是咱們項目在觸發截圖時的判斷邏輯,若是 renderdone 出現且爲 true 那麼就截圖,若是是 Object,說明頁面加載出錯了,咱們能夠捕獲該異常進行提示
let renderdoneHandle = await page.waitForFunction('window.renderdone', {
polling: 120
});
const renderdone = await renderdoneHandle.jsonValue();
if (typeof renderdone === 'object') {
console.log(`加載頁面失敗:報表${renderdone.componentId}出錯 -- ${renderdone.message}`);
}else{
console.log('頁面加載成功');
}
複製代碼
在使用 Puppeteer 時咱們幾乎必定會遇到在這兩個環境之間交換數據:運行 Puppeteer 的 Node.js 環境和 Puppeteer 操做的頁面 Page DOM,理解這兩個環境很重要
下面介紹 10 個關於使用 Puppeteer 的用例,並在介紹用例的時候會穿插的講解一些 API,告訴你們如何使用 Puppeteer:
咱們使用 Puppeteer 既能夠對某個頁面進行截圖,也能夠對頁面中的某個元素進行截圖:
(async () => {
const browser = await puppeteer.launch();
const page = await browser.newPage();
//設置可視區域大小
await page.setViewport({width: 1920, height: 800});
await page.goto('https://youdata.163.com');
//對整個頁面截圖
await page.screenshot({
path: './files/capture.png', //圖片保存路徑
type: 'png',
fullPage: true //邊滾動邊截圖
// clip: {x: 0, y: 0, width: 1920, height: 800}
});
//對頁面某個元素截圖
let [element] = await page.$x('/html/body/section[4]/div/div[2]');
await element.screenshot({
path: './files/element.png'
});
await page.close();
await browser.close();
})();
複製代碼
咱們怎麼去獲取頁面中的某個元素呢?
(async () => {
const browser = await puppeteer.launch({
slowMo: 100, //放慢速度
headless: false,
defaultViewport: {width: 1440, height: 780},
ignoreHTTPSErrors: false, //忽略 https 報錯
args: ['--start-fullscreen'] //全屏打開頁面
});
const page = await browser.newPage();
await page.goto('https://demo.youdata.com');
//輸入帳號密碼
const uniqueIdElement = await page.$('#uniqueId');
await uniqueIdElement.type('admin@admin.com', {delay: 20});
const passwordElement = await page.$('#password', {delay: 20});
await passwordElement.type('123456');
//點擊肯定按鈕進行登陸
let okButtonElement = await page.$('#btn-ok');
//等待頁面跳轉完成,通常點擊某個按鈕須要跳轉時,都須要等待 page.waitForNavigation() 執行完畢才表示跳轉成功
await Promise.all([
okButtonElement.click(),
page.waitForNavigation()
]);
console.log('admin 登陸成功');
await page.close();
await browser.close();
})();
複製代碼
那麼 ElementHandle 都提供了哪些操做元素的函數呢?
請求在有些場景下頗有必要,攔截一下不必的請求提升性能,咱們能夠在監聽 Page 的 request 事件,並進行請求攔截,前提是要開啓請求攔截 page.setRequestInterception(true)。
(async () => {
const browser = await puppeteer.launch();
const page = await browser.newPage();
const blockTypes = new Set(['image', 'media', 'font']);
await page.setRequestInterception(true); //開啓請求攔截
page.on('request', request => {
const type = request.resourceType();
const shouldBlock = blockTypes.has(type);
if(shouldBlock){
//直接阻止請求
return request.abort();
}else{
//對請求重寫
return request.continue({
//能夠對 url,method,postData,headers 進行覆蓋
headers: Object.assign({}, request.headers(), {
'puppeteer-test': 'true'
})
});
}
});
await page.goto('https://demo.youdata.com');
await page.close();
await browser.close();
})();
複製代碼
那 page 頁面上都提供了哪些事件呢?
Puppeteer 目前沒有提供原生的用於處理 WebSocket 的 API 接口,可是咱們能夠經過更底層的 Chrome DevTool Protocol (CDP) 協議得到
(async () => {
const browser = await puppeteer.launch();
const page = await browser.newPage();
//建立 CDP 會話
let cdpSession = await page.target().createCDPSession();
//開啓網絡調試,監聽 Chrome DevTools Protocol 中 Network 相關事件
await cdpSession.send('Network.enable');
//監聽 webSocketFrameReceived 事件,獲取對應的數據
cdpSession.on('Network.webSocketFrameReceived', frame => {
let payloadData = frame.response.payloadData;
if(payloadData.includes('push:query')){
//解析payloadData,拿到服務端推送的數據
let res = JSON.parse(payloadData.match(/\{.*\}/)[0]);
if(res.code !== 200){
console.log(`調用websocket接口出錯:code=${res.code},message=${res.message}`);
}else{
console.log('獲取到websocket接口數據:', res.result);
}
}
});
await page.goto('https://netease.youdata.163.com/dash/142161/reportExport?pid=700209493');
await page.waitForFunction('window.renderdone', {polling: 20});
await page.close();
await browser.close();
})();
複製代碼
Puppeteer 最強大的功能是,你能夠在瀏覽器裏執行任何你想要運行的 javascript 代碼,下面是我在爬 188 郵箱的收件箱用戶列表時,發現每次打開收件箱再關掉都會多處一個 iframe 來,隨着打開收件箱的增多,iframe 增多到瀏覽器卡到沒法運行,因此我在爬蟲代碼里加了刪除無用 iframe 的腳本:
(async () => {
const browser = await puppeteer.launch();
const page = await browser.newPage();
await page.goto('https://webmail.vip.188.com');
//註冊一個 Node.js 函數,在瀏覽器裏運行
await page.exposeFunction('md5', text =>
crypto.createHash('md5').update(text).digest('hex')
);
//經過 page.evaluate 在瀏覽器裏執行刪除無用的 iframe 代碼
await page.evaluate(async () => {
let iframes = document.getElementsByTagName('iframe');
for(let i = 3; i < iframes.length - 1; i++){
let iframe = iframes[i];
if(iframe.name.includes("frameBody")){
iframe.src = 'about:blank';
try{
iframe.contentWindow.document.write('');
iframe.contentWindow.document.clear();
}catch(e){}
//把iframe從頁面移除
iframe.parentNode.removeChild(iframe);
}
}
//在頁面中調用 Node.js 環境中的函數
const myHash = await window.md5('PUPPETEER');
console.log(`md5 of ${myString} is ${myHash}`);
});
await page.close();
await browser.close();
})();
複製代碼
有哪些函數能夠在瀏覽器環境中執行代碼呢?
一個 Frame 包含了一個執行上下文(Execution Context),咱們不能跨 Frame 執行函數,一個頁面中能夠有多個 Frame,主要是經過 iframe 標籤嵌入的生成的。其中在頁面上的大部分函數實際上是 page.mainFrame().xx 的一個簡寫,Frame 是樹狀結構,咱們能夠經過 frame.childFrames() 遍歷到全部的 Frame,若是想在其它 Frame 中執行函數必須獲取到對應的 Frame 才能進行相應的處理
如下是在登陸 188 郵箱時,其登陸窗口實際上是嵌入的一個 iframe,如下代碼時咱們在獲取 iframe 並進行登陸
(async () => {
const browser = await puppeteer.launch({headless: false, slowMo: 50});
const page = await browser.newPage();
await page.goto('https://www.188.com');
//點擊使用密碼登陸
let passwordLogin = await page.waitForXPath('//*[@id="qcode"]/div/div[2]/a');
await passwordLogin.click();
for (const frame of page.mainFrame().childFrames()){
//根據 url 找到登陸頁面對應的 iframe
if (frame.url().includes('passport.188.com')){
await frame.type('.dlemail', 'admin@admin.com');
await frame.type('.dlpwd', '123456');
await Promise.all([
frame.click('#dologin'),
page.waitForNavigation()
]);
break;
}
}
await page.close();
await browser.close();
})();
複製代碼
Puppeteer 提供了對頁面性能分析的工具,目前功能仍是比較弱的,只能獲取到一個頁面性能執行的數據,如何分析須要咱們本身根據數據進行分析,聽說在 2.0 版本會作大的改版:
(async () => {
const browser = await puppeteer.launch();
const page = await browser.newPage();
await page.tracing.start({path: './files/trace.json'});
await page.goto('https://www.google.com');
await page.tracing.stop();
/* continue analysis from 'trace.json' */
browser.close();
})();
複製代碼
在自動化測試中,常常會遇到對於文件的上傳和下載的需求,那麼在 Puppeteer 中如何實現呢?
(async () => {
const browser = await puppeteer.launch();
const page = await browser.newPage();
//經過 CDP 會話設置下載路徑
await page.target().createCDPSession().send('Page.setDownloadBehavior', {
behavior: 'allow', //容許全部下載請求
downloadPath: 'path/to/download' //設置下載路徑
});
//點擊按鈕觸發下載
await (await page.waitForSelector('#someButton')).click();
//等待文件出現,輪訓判斷文件是否出現
await waitForFile('path/to/download/filename');
//上傳時對應的 inputElement 必須是<input>元素
let inputElement = await page.waitForXPath('//input[@type="file"]');
await inputElement.uploadFile('/path/to/file');
browser.close();
})();
複製代碼
在點擊一個按鈕跳轉到新的 Tab 頁時會新開一個頁面,這個時候咱們如何獲取改頁面對應的 Page 實例呢?能夠經過監聽 Browser 上的 targetcreated 事件來實現,表示有新的頁面建立:
let page = await browser.newPage();
await page.goto(url);
let btn = await page.waitForSelector('#btn');
//在點擊按鈕以前,事先定義一個 Promise,用於返回新 tab 的 Page 對象
const newPagePromise = new Promise(res =>
browser.once('targetcreated',
target => res(target.page())
)
);
await btn.click();
//點擊按鈕後,等待新tab對象
let newPage = await newPagePromise;
複製代碼
Puppeteer 提供了模擬不一樣設備的功能,其中 puppeteer.devices 對象上定義不少設備的配置信息,這些配置信息主要包含 viewport 和 userAgent,而後經過函數 page.emulate 實現不一樣設備的模擬
const puppeteer = require('puppeteer');
const iPhone = puppeteer.devices['iPhone 6'];
puppeteer.launch().then(async browser => {
const page = await browser.newPage();
await page.emulate(iPhone);
await page.goto('https://www.google.com');
await browser.close();
});
複製代碼
Headless Chrome vs PhantomJS Benchmark
Chrome 默認使用 /dev/shm 共享內存,可是 docker 默認/dev/shm 只有64MB,顯然是不夠使用的,提供兩種方式來解決:
- 啓動 docker 時添加參數 --shm-size=1gb 來增大 /dev/shm 共享內存,可是 swarm 目前不支持 shm-size 參數
- 啓動 Chrome 添加參數 - disable-dev-shm-usage,禁止使用 /dev/shm 共享內存
複製代碼