使用puppeteer實(shí)現(xiàn)瀏覽器自動(dòng)化
Puppeteer 是一個(gè) Node 庫(kù),它提供了一個(gè)高級(jí) API 來(lái)通過(guò) DevTools 協(xié)議控制 Chromium 或 Chrome。Puppeteer 默認(rèn)以 headless 模式運(yùn)行,但是可以通過(guò)修改配置文件運(yùn)行“有頭”模式。
可以實(shí)現(xiàn)手動(dòng)操作瀏覽器的所有功能
生成頁(yè)面 PDF。
抓取 SPA(單頁(yè)應(yīng)用)并生成預(yù)渲染內(nèi)容(即“SSR”(服務(wù)器端渲染))。
自動(dòng)提交表單,進(jìn)行 UI 測(cè)試,鍵盤(pán)輸入等。
創(chuàng)建一個(gè)時(shí)時(shí)更新的自動(dòng)化測(cè)試環(huán)境。 使用最新的 JavaScript 和瀏覽器功能直接在最新版本的Chrome中執(zhí)行測(cè)試。
捕獲網(wǎng)站的 timeline trace,用來(lái)幫助分析性能問(wèn)題。
測(cè)試瀏覽器擴(kuò)展。
安裝
安裝有瀏覽器的版本
npm i puppeteer
這個(gè)版本自帶Chromium瀏覽器,體積在300多M
2024.1更新
現(xiàn)在默認(rèn)不會(huì)安裝Chromium,如果要安裝,運(yùn)行上述代碼以后,再運(yùn)行"node node_modules\puppeteer\install.js",如果提示沒(méi)有install.js,則運(yùn)行"node_modules\puppeteer\install.mjs"
如果電腦有谷歌瀏覽器,可以使用下面代碼
npm i puppeteer-core
簡(jiǎn)單使用
截圖
(async () => {
const browser = await puppeteer.launch();
const page = await browser.newPage();
//設(shè)置可視區(qū)域大小
await page.setViewport({width: 1920, height: 800});
await page.goto('https://youdata.163.com');
//對(duì)整個(gè)頁(yè)面截圖
await page.screenshot({
path: './files/capture.png', //圖片保存路徑
type: 'png',
fullPage: true //邊滾動(dòng)邊截圖
// clip: {x: 0, y: 0, width: 1920, height: 800}
});
//對(duì)頁(yè)面某個(gè)元素截圖
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();
})();
模擬用戶(hù)登錄
(async () => {
const browser = await puppeteer.launch({
slowMo: 100, //放慢速度
headless: false,
defaultViewport: {width: 1440, height: 780},
ignoreHTTPSErrors: false, //忽略 https 報(bào)錯(cuò)
args: ['--start-fullscreen'] //全屏打開(kāi)頁(yè)面
});
const page = await browser.newPage();
await page.goto('https://demo.youdata.com');
//輸入賬號(hào)密碼
const uniqueIdElement = await page.$('#uniqueId');
await uniqueIdElement.type('admin@admin.com', {delay: 20});
const passwordElement = await page.$('#password', {delay: 20});
await passwordElement.type('123456');
//點(diǎn)擊確定按鈕進(jìn)行登錄
let okButtonElement = await page.$('#btn-ok');
//等待頁(yè)面跳轉(zhuǎn)完成,一般點(diǎn)擊某個(gè)按鈕需要跳轉(zhuǎn)時(shí),都需要等待 page.waitForNavigation() 執(zhí)行完畢才表示跳轉(zhuǎn)成功
await Promise.all([
okButtonElement.click(),
page.waitForNavigation()
]);
console.log('admin 登錄成功');
await page.close();
await browser.close();
})();
那么 ElementHandle 都提供了哪些操作元素的函數(shù)呢?
elementHandle.click():點(diǎn)擊某個(gè)元素
elementHandle.tap():模擬手指觸摸點(diǎn)擊
elementHandle.focus():聚焦到某個(gè)元素
elementHandle.hover():鼠標(biāo) hover 到某個(gè)元素上
elementHandle.type('hello'):在輸入框輸入文本
請(qǐng)求攔截
(async () => {
const browser = await puppeteer.launch();
const page = await browser.newPage();
const blockTypes = new Set(['image', 'media', 'font']);
await page.setRequestInterception(true); //開(kāi)啟請(qǐng)求攔截
page.on('request', request => {
const type = request.resourceType();
const shouldBlock = blockTypes.has(type);
if(shouldBlock){
//直接阻止請(qǐng)求
return request.abort();
}else{
//對(duì)請(qǐng)求重寫(xiě)
return request.continue({
//可以對(duì) url,method,postData,headers 進(jìn)行覆蓋
headers: Object.assign({}, request.headers(), {
'puppeteer-test': 'true'
})
});
}
});
await page.goto('https://demo.youdata.com');
await page.close();
await browser.close();
})();
那 page 頁(yè)面上都提供了哪些事件呢?
page.on('close') 頁(yè)面關(guān)閉
page.on('console') console API 被調(diào)用
page.on('error') 頁(yè)面出錯(cuò)
page.on('load') 頁(yè)面加載完
page.on('request') 收到請(qǐng)求
page.on('requestfailed') 請(qǐng)求失敗
page.on('requestfinished') 請(qǐng)求成功
page.on('response') 收到響應(yīng)
page.on('workercreated') 創(chuàng)建 webWorker
page.on('workerdestroyed') 銷(xiāo)毀 webWorker
獲取 WebSocket 響應(yīng)
Puppeteer 目前沒(méi)有提供原生的用于處理 WebSocket 的 API 接口,但是我們可以通過(guò)更底層的 Chrome DevTool Protocol (CDP) 協(xié)議獲得
(async () => {
const browser = await puppeteer.launch();
const page = await browser.newPage();
//創(chuàng)建 CDP 會(huì)話(huà)
let cdpSession = await page.target().createCDPSession();
//開(kāi)啟網(wǎng)絡(luò)調(diào)試,監(jiān)聽(tīng) Chrome DevTools Protocol 中 Network 相關(guān)事件
await cdpSession.send('Network.enable');
//監(jiān)聽(tīng) webSocketFrameReceived 事件,獲取對(duì)應(yīng)的數(shù)據(jù)
cdpSession.on('Network.webSocketFrameReceived', frame => {
let payloadData = frame.response.payloadData;
if(payloadData.includes('push:query')){
//解析payloadData,拿到服務(wù)端推送的數(shù)據(jù)
let res = JSON.parse(payloadData.match(/\{.*\}/)[0]);
if(res.code !== 200){
console.log(`調(diào)用websocket接口出錯(cuò):code=${res.code},message=${res.message}`);
}else{
console.log('獲取到websocket接口數(shù)據(jù):', 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();
})();
植入 javascript 代碼
Puppeteer 最強(qiáng)大的功能是,你可以在瀏覽器里執(zhí)行任何你想要運(yùn)行的 javascript 代碼,下面是我在爬 188 郵箱的收件箱用戶(hù)列表時(shí),發(fā)現(xiàn)每次打開(kāi)收件箱再關(guān)掉都會(huì)多處一個(gè) iframe 來(lái),隨著打開(kāi)收件箱的增多,iframe 增多到瀏覽器卡到無(wú)法運(yùn)行,所以我在爬蟲(chóng)代碼里加了刪除無(wú)用 iframe 的腳本:
(async () => {
const browser = await puppeteer.launch();
const page = await browser.newPage();
await page.goto('https://webmail.vip.188.com');
//注冊(cè)一個(gè) Node.js 函數(shù),在瀏覽器里運(yùn)行
await page.exposeFunction('md5', text =>
crypto.createHash('md5').update(text).digest('hex')
);
//通過(guò) page.evaluate 在瀏覽器里執(zhí)行刪除無(wú)用的 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從頁(yè)面移除
iframe.parentNode.removeChild(iframe);
}
}
//在頁(yè)面中調(diào)用 Node.js 環(huán)境中的函數(shù)
const myHash = await window.md5('PUPPETEER');
console.log(`md5 of ${myString} is ${myHash}`);
});
await page.close();
await browser.close();
})();
page.evaluate(pageFunction[, ...args]):在瀏覽器環(huán)境中執(zhí)行函數(shù)
page.evaluateHandle(pageFunction[, ...args]):在瀏覽器環(huán)境中執(zhí)行函數(shù),返回 JsHandle 對(duì)象
page.$$eval(selector, pageFunction[, ...args]):把 selector 對(duì)應(yīng)的所有元素傳入到函數(shù)并在瀏覽器環(huán)境執(zhí)行
page.$eval(selector, pageFunction[, ...args]):把 selector 對(duì)應(yīng)的第一個(gè)元素傳入到函數(shù)在瀏覽器環(huán)境執(zhí)行
page.evaluateOnNewDocument(pageFunction[, ...args]):創(chuàng)建一個(gè)新的 Document 時(shí)在瀏覽器環(huán)境中執(zhí)行,會(huì)在頁(yè)面所有腳本執(zhí)行之前執(zhí)行
page.exposeFunction(name, puppeteerFunction):在 window 對(duì)象上注冊(cè)一個(gè)函數(shù),這個(gè)函數(shù)在 Node 環(huán)境中執(zhí)行,有機(jī)會(huì)在瀏覽器環(huán)境中調(diào)用 Node.js 相關(guān)函數(shù)庫(kù)
抓取 iframe 中的元素
一個(gè) Frame 包含了一個(gè)執(zhí)行上下文(Execution Context),我們不能跨 Frame 執(zhí)行函數(shù),一個(gè)頁(yè)面中可以有多個(gè) Frame,主要是通過(guò) iframe 標(biāo)簽嵌入的生成的。其中在頁(yè)面上的大部分函數(shù)其實(shí)是 page.mainFrame().xx 的一個(gè)簡(jiǎn)寫(xiě),F(xiàn)rame 是樹(shù)狀結(jié)構(gòu),我們可以通過(guò) frame.childFrames() 遍歷到所有的 Frame,如果想在其它 Frame 中執(zhí)行函數(shù)必須獲取到對(duì)應(yīng)的 Frame 才能進(jìn)行相應(yīng)的處理
以下是在登錄 188 郵箱時(shí),其登錄窗口其實(shí)是嵌入的一個(gè) iframe,以下代碼時(shí)我們?cè)讷@取 iframe 并進(jìn)行登錄
(async () => {
const browser = await puppeteer.launch({headless: false, slowMo: 50});
const page = await browser.newPage();
await page.goto('https://www.188.com');
//點(diǎn)擊使用密碼登錄
let passwordLogin = await page.waitForXPath('//*[@id="qcode"]/div/div[2]/a');
await passwordLogin.click();
for (const frame of page.mainFrame().childFrames()){
//根據(jù) url 找到登錄頁(yè)面對(duì)應(yīng)的 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();
})();
文件的上傳和下載
在自動(dòng)化測(cè)試中,經(jīng)常會(huì)遇到對(duì)于文件的上傳和下載的需求,那么在 Puppeteer 中如何實(shí)現(xiàn)呢?
(async () => {
const browser = await puppeteer.launch();
const page = await browser.newPage();
//通過(guò) CDP 會(huì)話(huà)設(shè)置下載路徑
const cdp = await page.target().createCDPSession();
await cdp.send('Page.setDownloadBehavior', {
behavior: 'allow', //允許所有下載請(qǐng)求
downloadPath: 'path/to/download' //設(shè)置下載路徑
});
//點(diǎn)擊按鈕觸發(fā)下載
await (await page.waitForSelector('#someButton')).click();
//等待文件出現(xiàn),輪訓(xùn)判斷文件是否出現(xiàn)
await waitForFile('path/to/download/filename');
//上傳時(shí)對(duì)應(yīng)的 inputElement 必須是<input>元素
let inputElement = await page.waitForXPath('//input[@type="file"]');
await inputElement.uploadFile('/path/to/file');
browser.close();
})();
模擬選擇文件
點(diǎn)擊元素觸發(fā)選擇文件框,不會(huì)顯示,直接返回選擇文件
const [fileChooser] = await Promise.all([
page.waitForFileChooser(),
page.click('#mydropzone'), // some button that triggers file selection
]);
await fileChooser.accept(['D:\\down\\tmp.zip']);
跳轉(zhuǎn)新 tab 頁(yè)處理
在點(diǎn)擊一個(gè)按鈕跳轉(zhuǎn)到新的 Tab 頁(yè)時(shí)會(huì)新開(kāi)一個(gè)頁(yè)面,這個(gè)時(shí)候我們?nèi)绾潍@取改頁(yè)面對(duì)應(yīng)的 Page 實(shí)例呢?可以通過(guò)監(jiān)聽(tīng) Browser 上的 targetcreated 事件來(lái)實(shí)現(xiàn),表示有新的頁(yè)面創(chuàng)建:
let page = await browser.newPage();
await page.goto(url);
let btn = await page.waitForSelector('#btn');
//在點(diǎn)擊按鈕之前,事先定義一個(gè) Promise,用于返回新 tab 的 Page 對(duì)象
const newPagePromise = new Promise(res =>
browser.once('targetcreated',
target => res(target.page())
)
);
await btn.click();
//點(diǎn)擊按鈕后,等待新tab對(duì)象
let newPage = await newPagePromise;
模擬不同的設(shè)備
Puppeteer 提供了模擬不同設(shè)備的功能,其中 puppeteer.devices 對(duì)象上定義很多設(shè)備的配置信息,這些配置信息主要包含 viewport 和 userAgent,然后通過(guò)函數(shù) page.emulate 實(shí)現(xiàn)不同設(shè)備的模擬
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();
});
其他信息
官方中文文檔
https://zhaoqize.github.io/puppeteer-api-zh_CN/#/