Puppeteer 是 Chrome 開發團隊在 2017 年發佈的一個 Node.js 包,同時還有 Headless Chrome。用來模擬 Chrome 瀏覽器的運行。它提供了高級API來經過 DevTools 協議控制無頭 Chrome 或 Chromium ,它也能夠配置爲使用完整(非無頭)Chrome 或 Chromium。javascript
學習 Puppeteer 以前咱們先來了解一下 Chrome DevTool Protocol 和 Headless Chrome。java
總而言之 Headless Chrome 就是 Chrome 瀏覽器的無界面形態,能夠在不打開瀏覽器的前提下,使用全部 Chrome 支持的特性運行你的程序。node
官方介紹:您能夠在瀏覽器中手動執行的大多數操做均可以使用 Puppeteer 完成!示例:git
Puppeteer 中的 API 分層結構基本和瀏覽器保持一致,下面對常使用到的幾個類介紹一下:es6
注意:在v1.18.1以前,Puppeteer至少須要Node v6.4.0。從v1.18.1到v2.1.0的版本依賴於Node 8.9.0+。從v3.0.0開始,Puppeteer開始依賴於Node 10.18.1+。若要使用 async / await,只有Node v7.6.0或更高版本才支持。
Puppeteer是一個node.js包,因此安裝很簡單:github
npm install puppeteer // 或者 yarn add puppeteer
npm 在安裝 puppeteer 的時候可能會報錯!這是因爲外網致使,使用淘寶鏡像 cnpm 安裝可解決。
安裝Puppeteer時,它將下載 Chromium 的最新版本。從1.7.0版開始,官方發佈了該 puppeteer-core 軟件包,默認狀況下不會下載任何瀏覽器,用於啓動現有的瀏覽器或鏈接到遠程瀏覽器。須要注意安裝的 puppeteer-core 版本與打算鏈接的瀏覽器兼容。web
咱們使用 Puppeteer 既能夠對某個頁面進行截圖,也能夠對頁面中的某個元素進行截圖:chrome
const puppeteer = require('puppeteer'); (async () => { const browser = await puppeteer.launch(); const page = await browser.newPage(); //設置可視區域大小,默認的頁面大小爲800x600分辨率 await page.setViewport({width: 1920, height: 800}); await page.goto('https://www.baidu.com/'); //對整個頁面截圖 await page.screenshot({ path: './files/baidu_home.png', //圖片保存路徑 type: 'png', fullPage: true //邊滾動邊截圖 // clip: {x: 0, y: 0, width: 1920, height: 800} }); //對頁面某個元素截圖 let element = await page.$('#s_lg_img'); await element.screenshot({ path: './files/baidu_logo.png' }); await page.close(); await browser.close(); })();
咱們怎麼去獲取頁面中的某個元素呢?docker
page.$('#uniqueId')
:獲取某個選擇器對應的第一個元素page.$$('div')
:獲取某個選擇器對應的全部元素page.$x('//img')
:獲取某個 xPath 對應的全部元素page.waitForXPath('//img')
:等待某個 xPath 對應的元素出現page.waitForSelector('#uniqueId')
:等待某個選擇器對應的元素出現const puppeteer = require('puppeteer'); (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://www.baidu.com/'); //輸入文本 const inputElement = await page.$('#kw'); await inputElement.type('hello word', {delay: 20}); //點擊搜索按鈕 let okButtonElement = await page.$('#su'); //等待頁面跳轉完成,通常點擊某個按鈕須要跳轉時,都須要等待 page.waitForNavigation() 執行完畢才表示跳轉成功 await Promise.all([ okButtonElement.click(), page.waitForNavigation() ]); await page.close(); await browser.close(); })();
那麼 ElementHandle 都提供了哪些操做元素的函數呢?npm
elementHandle.click()
:點擊某個元素elementHandle.tap()
:模擬手指觸摸點擊elementHandle.focus()
:聚焦到某個元素elementHandle.hover()
:鼠標 hover 到某個元素上elementHandle.type('hello')
:在輸入框輸入文本Puppeteer 最強大的功能是,你能夠在瀏覽器裏執行任何你想要運行的 javascript 代碼。下面代碼是對百度首頁新聞推薦爬取數據的例子。
const puppeteer = require('puppeteer'); (async () => { const browser = await puppeteer.launch(); const page = await browser.newPage(); await page.goto('https://www.baidu.com/'); //經過 page.evaluate 在瀏覽器裏執行代碼 const resultData = await page.evaluate(async () => { let data = {}; const ListEle = [...document.querySelectorAll('#hotsearch-content-wrapper .hotsearch-item')]; data = ListEle.map((ele) => { const urlEle = ele.querySelector('a.c-link'); const titleEle = ele.querySelector('.title-content-title'); return { href: urlEle.href, title: titleEle.innerText, }; }); return data; }); console.log(resultData) await page.close(); await browser.close(); })();
有哪些函數能夠在瀏覽器環境中執行代碼呢?
page.evaluate(pageFunction[, ...args])
:在瀏覽器環境中執行函數page.evaluateHandle(pageFunction[, ...args])
:在瀏覽器環境中執行函數,返回 JsHandle 對象page.$$eval(selector, pageFunction[, ...args])
:把 selector 對應的全部元素傳入到函數並在瀏覽器環境執行page.$eval(selector, pageFunction[, ...args])
:把 selector 對應的第一個元素傳入到函數在瀏覽器環境執行page.evaluateOnNewDocument(pageFunction[, ...args])
:建立一個新的 Document 時在瀏覽器環境中執行,會在頁面全部腳本執行以前執行page.exposeFunction(name, puppeteerFunction)
:在 window 對象上註冊一個函數,這個函數在 Node 環境中執行,有機會在瀏覽器環境中調用 Node.js 相關函數庫請求在有些場景下頗有必要,攔截一下不必的請求提升性能,咱們能夠在監聽 Page 的 request 事件,並進行請求攔截,前提是要開啓請求攔截 page.setRequestInterception(true)
。
const puppeteer = require('puppeteer'); (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://www.baidu.com/'); await page.close(); await browser.close(); })();
那 page 頁面上都提供了哪些事件呢?
page.on('close')
頁面關閉page.on('console')
console API 被調用page.on('error')
頁面出錯page.on('load')
頁面加載完page.on('request')
收到請求page.on('requestfailed')
請求失敗page.on('requestfinished')
請求成功page.on('response')
收到響應page.on('workercreated')
建立 webWorkerpage.on('workerdestroyed')
銷燬 webWorkerPuppeteer 目前沒有提供原生的用於處理 WebSocket 的 API 接口,可是咱們能夠經過更底層的 Chrome DevTool Protocol (CDP) 協議得到
const puppeteer = require('puppeteer'); (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(); })();
一個 Frame 包含了一個執行上下文(Execution Context),咱們不能跨 Frame 執行函數,一個頁面中能夠有多個 Frame,主要是經過 iframe 標籤嵌入的生成的。其中在頁面上的大部分函數實際上是 page.mainFrame().xx 的一個簡寫,Frame 是樹狀結構,咱們能夠經過 frame.childFrames() 遍歷到全部的 Frame,若是想在其它 Frame 中執行函數必須獲取到對應的 Frame 才能進行相應的處理
如下是在登陸 188 郵箱時,其登陸窗口實際上是嵌入的一個 iframe,如下代碼時咱們在獲取 iframe 並進行登陸
const puppeteer = require('puppeteer'); (async () => { const browser = await puppeteer.launch({headless: false, slowMo: 50}); const page = await browser.newPage(); await page.goto('https://www.188.com'); 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 版本會作大的改版: - 一個瀏覽器同一時間只能 trace 一次 - 在 devTools 的 Performance 能夠上傳對應的 json 文件並查看分析結果 - 咱們能夠寫腳原本解析 trace.json 中的數據作自動化分析 - 經過 tracing 咱們獲取頁面加載速度以及腳本的執行性能
const puppeteer = require('puppeteer'); (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 中如何實現呢?
const puppeteer = require('puppeteer'); (async () => { const browser = await puppeteer.launch(); const page = await browser.newPage(); //經過 CDP 會話設置下載路徑 const cdp = await page.target().createCDPSession(); await cdp.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.baidu.com'); await browser.close(); });
Chrome 默認使用 /dev/shm 共享內存,可是 docker 默認/dev/shm 只有64MB,顯然是不夠使用的,提供兩種方式來解決: - 啓動 docker 時添加參數 --shm-size=1gb 來增大 /dev/shm 共享內存,可是 swarm 目前不支持 shm-size 參數 - 啓動 Chrome 添加參數 - disable-dev-shm-usage,禁止使用 /dev/shm 共享內存