Puppeteer 入門與實戰
本文首發於 vivo網際網路技術 微信公眾號
連結:https://mp.weixin.qq.com/s/P-YdQPOQ9GZgjDEP7VG8ag
作者:Wang Zhenzheng
Puppeteer 是 Chrome開發團隊2017年釋出的一個 Node.js包,提供了一組用來操縱Chrome的API,通俗來說就是一個Headless Chrome瀏覽器,這Headless Chrome也可以配置成有UI的 。利用Puppeteer可以做到爬取頁面資料,頁面截圖或者生成PDF檔案,前端自動化測試(模擬輸入/點選/鍵盤行為)以及捕獲站點的時間線,分析網站效能問題。
一、起因
雖說Puppeteer是Chrome開發團隊2017年釋出的一個 Node.js包,但是在團隊日常工作中基本沒有使用。前段時間在開發一個聊天工具的時候,需要引入emoji表情,但是業務方的需求是要使用Google emoji,那我們就需要在
尷尬的是這個頁面是直出的,不是通過介面呼叫,那就需要我們換個思路,我們發現這些emoji的DOM是在一個class為emoji-grid的ul下,那麼如果拿到該ul節點下的全部img的url,然後遍歷到本地,是不是就做到將emoji表情儲存下來。
依據這個思路,我們就想到使用Puppeteer,在介紹Puppeteer之前我們先將這段簡單的捕獲moji表情的程式碼放出來。
const puppeteer = require('puppeteer') const request = require('request') const fs = require('fs') async function getEmojiImage (url) { // 返回解析為Promise的瀏覽器 const browser = await puppeteer.launch() // 返回新的頁面物件 const page = await browser.newPage() // 頁面物件訪問對應的url地址 await page.goto(url, { waitUntil: 'networkidle2' }) // 等待3000ms,等待瀏覽器的載入 await page.waitFor(3000) // 可以在page.evaluate的回撥函式中訪問瀏覽器物件,可以進行DOM操作 const emojis = await page.evaluate(() => { let ol = document.getElementsByClassName('emoji-grid')[0] let imgs = ol.getElementsByTagName('img') let url = [] for (let i = 0; i < 97; i++) { url.push(imgs[i].getAttribute('src')) } // 返回所有emoji的url地址陣列 return url }) // 定義一個存在的json let json = [] for (let i = 0; i < emojis.length; i++) { const name = emojis[i].slice(emojis[i].lastIndexOf('/') + 1) // 將emoji寫入本地檔案中 request(emojis[i]).pipe(fs.createWriteStream('./' + (i < 10 ? '0' + i : i) + name)) json.push({ name, url: `./a/a/${name}` // 你的url地址 }) console.log(`${name}----emoji寫入成功`) } // 寫入json檔案 fs.writeFile('./google-emoji.json', JSON.stringify(json), function () {}) // 關閉無頭瀏覽器 await browser.close() } getEmojiImage('https://emojipedia.org/google/')
在瞭解Puppeteer之前,我們先來看下Headless Chrome。
二、Headless Chrome
Headless Chrome在Chrome59中釋出,用於在headless環境中執行Chrome瀏覽器,也就是在非Chrome環境中執行Chrome。它將Chromium和Blink渲染引擎提供的所有現代Web平臺功能引入命令列。
headless如何在終端中使用:我們嘗試通過終端命令開啟vivo 的官網
chrome --headless --disable-gpu --remote-debugging-port=8080 https://vivo.com.cn
注意:在Mac上使用前,建議先繫結Chrome的別名
alias chrome="/Applications/Google\ Chrome.app/Contents/MacOS/Google\ Chrome"
此時,Headless Chrome已經成功運行了,瀏覽器輸入http://127.0.0.1:8080,你會看到如下的vivo介面:
除此之外,還可以以命令列的形式去執行以下常見的操作:
1、列印DOM:
chrome --headless --disable-gpu --dump-dom https://vivo.com.cn
2、建立一個PDF檔案
chrome --headless --disable-gpu --print-to-pdf https://vivo.com.cn
3、截圖
chrome --headless --disable-gpu --screenshot https://vivo.com.cn // 設定截圖的尺寸 chrome --headless --disable-gpu --screenshot --window-size=1280,1696 https://vivo.com.cn
那麼,Puppeteer是什麼?又可以做些什麼?Puppeteer是一個node庫,提供了一組用來操縱Chrome的API,通俗來說就是一個Headless Chrome瀏覽器,這Headless Chrome也可以配置成有UI的,預設是沒有的。
三、Puppeteer
Puppeteer可以做些什麼呢?我們從文章開始的一個demo中可以發現,Puppeteer可以爬取頁面資料。除此之外,結合Headless Chrome的一些命令列,Puppeteer可以做到一下幾點:
- 爬取頁面資料
- 頁面截圖或者生成PDF檔案
- 前端自動化測試(模擬輸入/點選/鍵盤行為)
- 捕獲站點的時間線,分析網站效能問題
1、初探
這是Puppeteer官方提供的一張API分層結構圖
(圖片來源於網路)
從圖上我們可以發現,Puppeteer是通過使用Chrome DevTools Protocol(CDP)協議與瀏覽器進行通訊,而Browser對應一個瀏覽器例項,可以擁有瀏覽器上下文,一個Browser可以包含多個BrowserContext。Page表示一個Tab頁面,一個BrowserContext可以包含多個Page。每個頁面都有一個主的Frame,ExecutionContext是Frame提供的一個JavasSript執行環境。
2、Browser
一切的起源都是從Browser開始的,我們先來梳理下Browser例項以後發生了什麼。
首先,通過puppeteer.launch()建立一個Browser例項
const browser = await puppeteer.launch({ // --remote-debugging-port=3333會啟一個埠,在瀏覽器中訪問http://127.0.0.1:3333/可以檢視 args: ['--remote-debugging-port=3333'] }) console.log(browser.wsEndpoint())
通過列印的browser.wsEndpoint(),我們看到輸出一個如下的連結:
ws://127.0.0.1:57546/devtools/browser/5d6ee624-6b5e-4b8c-b284-5e4800eac853
這就是devTool用於連線除錯頁面的連線了,這個websocket連線遵循CDP協議,我們看下這裡面具體有什麼。
{"id":46,"method":"CSS.getMatchedStylesForNode","params":{"nodeId":5}} {"id":47,"method":"CSS.getComputedStyleForNode","params":{"nodeId":5}}
每條資訊的格式是有一個遞增的id值,然後有method和params引數。這些訊息指揮者被除錯頁面做出各種各樣的動作。換而言之,任何一個實現了CDP的程式都可以用來除錯頁面,chrome 這個協議等於是開放了用程式控制頁面動作的介面。比如我們可以這樣子模擬一個alert到頁面。
{"id":190,"method":"Runtime.compileScript","params":{"expression":"alert()","sourceURL":"","persistScript":false,"executionContextId":3}}
這種直接操作太不友好,而Puppeteer正是實現了遵循CDP的Node頂層API,使我們可以呼叫簡單方便的操作對應的指令。
3、Page
browser.newPage()為Browser中瀏覽器上下文的方法。我們看下newPage()的程式碼實現。
/** * @param {?string} contextId * @return {!Promise<!Puppeteer.Page>} */ async _createPageInContext(contextId) { const {targetId} = await this._connection.send('Target.createTarget', {url: 'about:blank', browserContextId: contextId || undefined}); const target = await this._targets.get(targetId); assert(await target._initializedPromise, 'Failed to create target for page'); const page = await target.page(); return page; }
this._connection.send('Target.createTarget',{})使用CDP中的Target.createTarget建立頁面了頁面,同樣,在我們其他API時也是在使用CDP中的方法,例如page.goto()實際上是執行的是client.send('Page.navigate', {});。而在Page中的一些操作,如點選/模擬輸入,則是呼叫的DomWorld例項,DomWorld通過FrameManager管理,Page物件主要使用三種manager來管理常見操作:
-
FrameManager:頁面行為管理。如跳轉goto,點選clcik,模擬輸入type,等待載入waitFor等
-
NetworkManager:網路行為管理。如設定每個請求忽略快取setCacheEnabled,請求攔截setRequestInterception等
-
EmulationManager:模擬行為管理。只有一個方法,emulateViewport,模擬裝置與視口尺寸
四、應用
除了文章開始的抓取emoji表情外,我們嘗試將Puppeteer應用在一個前端自動化測試的場景中,我們在後臺管理系統開發測試中,經常會碰到表單的提交,對於表單中不同欄位的校驗需要模擬不同的場景,人工的點選效率低,而且每次都需要重複表單輸入,比較繁瑣。
基於該場景,我們使用Puppeteer實現自動填寫-儲存-列印介面返回資料-截圖。
STEP 1
建立一個Browser類的例項,並通過引數設定初始化它(更多設定引數參考官網API)
const browser = await puppeteer.launch({ devtools: true, //是否為每個選項卡自動開啟DevTools面板 headless: false, //是否以無頭模式執行瀏覽器。預設是true,除非devtools選項是true defaultViewport: { width: 1000, height: 1200 }, //為每個頁面設定一個預設視口大小 ignoreHTTPSErrors: true //是否在導航期間忽略 HTTPS 錯誤 })
STEP 2
建立一個 Page 例項,導航到一個url
const page = await browser.newPage() await page.goto(url, { waitUntil: 'networkidle0' })
waitUntil引數是來確定滿足什麼條件才認為頁面跳轉完成。包括以下事件:
- load - 頁面的load事件觸發時
- domcontentloaded - 頁面的DOMContentLoaded事件觸發時
- networkidle0 - 不再有網路連線時觸發(至少500毫秒後)
- networkidle2 - 只有2個網路連線時觸發(至少500毫秒後)
該處用到的是不再有網路連線認為頁面跳轉完成。值得注意的是,後臺管理系統會有token的校驗,此處有兩種解決方案,一種是等待頁面自動跳轉到登陸處,模擬登陸操作然後返回;一種是直接在cookie裡設定token資訊。我們採用第二種,程式碼如下:
const cookies = [ { name: 'token', value: 'system tokens', //你係統自己的token domain: 'domain' //需要種在哪個domain下 } ] await page.setCookie(...cookies)
STEP 3
模擬頁面輸入操作和點選事件,我們程式碼就只列舉兩個,不一一展開了。
// 操作input輸入 132 ,delay引數表示輸入延遲 await page.type('.el-form-item:nth-child(1) input', '132', { delay: 20 }) // 操作點選 await page.click('.el-form-item:nth-child(2) .el-form-item__content label:nth-child(1)')
STEP 4
監測頁面是否有API響應,響應後將響應資料列印在控制檯。
page.on('response', response => { const req = response.request() console.log(`Response的請求地址:${req.url()},請求方式是:${req.method()}, 請求返回的狀態${response.status()},`) let message = response.text() message.then(function (result) { console.log(`返回的資料:${result}`) }) })
STEP 5
將操作後的頁面資訊截圖儲存
// 擷取url中的路徑標示,作為儲存圖片的命名,防止儲存後覆蓋 const testName = decodeURIComponent(url.split('#/')[1]).replace(/\//g, '-') await page.screenshot({ path: `${testName}.png`, fullPage: true })
STEP 6
關閉Browser—await browser.close()
至此,我們完成了一個表單的自動化校驗和測試。我們看下效果:
1.前端校驗通過,請求到服務端介面的資料
2.如果前端校驗沒通過,直接截圖生成
五、拓展
- 模擬線上環境點檢操作走查
- 定時爬去週報日報資料,生成截圖傳送給相關人員檢視
六、參考
-
https://developers.google.com/web/updates/2017/04/headless-chrome
-
https://peter.sh/experiments/chromium-command-line-switches/
更多內容敬請關注vivo 網際網路技術微信公眾號
注:轉載文章請先與微訊號:Labs2020聯絡。