天天看點

Puppeteer 入門與實戰

Puppeteer是Chrome開發團隊2017年釋出的一個 Node.js包,提供了一組用來操縱Chrome的API,通俗來說就是一個Headless Chrome浏覽器,這Headless Chrome也可以配置成有UI的 。利用Puppeteer可以做到爬取頁面資料,頁面截屏或者生成PDF檔案,前端自動化測試(模拟輸入/點選/鍵盤行為)以及捕獲站點的時間線,分析網站性能問題。

本文首發于 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,那我們就需要在emojipedia上将這些圖儲存下來。這麼多的圖如果一張一張儲存,那就枉為開發了。首先想到的是調用該頁面的api接口,從接口中拿到對應的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界面:

Puppeteer 入門與實戰

除此之外,還可以以指令行的形式去執行以下常見的操作:

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 入門與實戰

(圖檔來源于網絡)

從圖上我們可以發現,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 入門與實戰

基于該場景,我們使用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.前端校驗通過,請求到服務端接口的資料

Puppeteer 入門與實戰

2.如果前端校驗沒通過,直接截圖生成

Puppeteer 入門與實戰

五、拓展

  1. 模拟線上環境點檢操作走查
  2. 定時爬去周報日報資料,生成截圖發送給相關人員檢視

六、參考

  1. https://developers.google.com/web/updates/2017/04/headless-chrome
  2. https://peter.sh/experiments/chromium-command-line-switches/
  3. https://zhaoqize.github.io/puppeteer-api-zh_CN/#/

更多内容敬請關注 vivo 網際網路技術 微信公衆号

Puppeteer 入門與實戰

注:轉載文章請先與微信号:Labs2020 聯系。

分享 vivo 網際網路技術幹貨與沙龍活動,推薦最新行業動态與熱門會議。