puppeteer前端利器

Puppeteer 是 Chrome 開發團隊在 2017 年發佈的一個 Node.js 包,同時還有 Headless Chrome。用來模擬 Chrome 瀏覽器的運行。它提供了高級API來經過 DevTools 協議控制無頭 Chrome 或 Chromium ,它也能夠配置爲使用完整(非無頭)Chrome 或 Chromium。javascript

image

學習 Puppeteer 以前咱們先來了解一下 Chrome DevTool Protocol 和 Headless Chrome。java

什麼是 Chrome DevTool Protocol

  • CDP 基於 WebSocket,利用 WebSocket 實現與瀏覽器內核的快速數據通道。
  • CDP 分爲多個域(DOM,Debugger,Network,Profiler,Console...),每一個域中都定義了相關的命令和事件(Commands and Events)。
  • 咱們能夠基於 CDP 封裝一些工具對 Chrome 瀏覽器進行調試及分析,好比咱們經常使用的 「Chrome 開發者工具」 就是基於 CDP 實現的。
  • 不少有用的工具都是基於 CDP 實現的,好比 Chrome 開發者工具chrome-remote-interfacePuppeteer 等。

什麼是 Headless Chrome

  • 能夠在無界面的環境中運行 Chrome。
  • 經過命令行或者程序語言操做 Chrome。
  • 無需人的干預,運行更穩定。
  • 在啓動 Chrome 時添加參數 --headless,即可以 headless 模式啓動 Chrome。
  • chrome 啓動時能夠加一些什麼參數,你們能夠點擊這裏查看。

總而言之 Headless Chrome 就是 Chrome 瀏覽器的無界面形態,能夠在不打開瀏覽器的前提下,使用全部 Chrome 支持的特性運行你的程序。node

Puppeteer 是什麼

  • Puppeteer 是 Node.js 工具引擎。
  • Puppeteer 提供了一系列 API,經過 Chrome DevTools Protocol 協議控制 Chromium/Chrome 瀏覽器的行爲。
  • Puppeteer 默認狀況下是以 headless 啓動 Chrome 的,也能夠經過參數控制啓動有界面的 Chrome。
  • Puppeteer 默認綁定最新的 Chromium 版本,也能夠本身設置不一樣版本的綁定。
  • Puppeteer 讓咱們不須要了解太多的底層 CDP 協議實現與瀏覽器的通訊。

Puppeteer 能作什麼

官方介紹:您能夠在瀏覽器中手動執行的大多數操做均可以使用 Puppeteer 完成!示例:git

  • 生成頁面的屏幕截圖和PDF。
  • 爬取 SPA 或 SSR 網站。
  • 自動化表單提交,UI測試,鍵盤輸入等。
  • 建立最新的自動化測試環境。使用最新的JavaScript和瀏覽器功能,直接在最新版本的Chrome中運行測試。
  • 捕獲站點的時間線跟蹤,以幫助診斷性能問題。
  • 測試Chrome擴展程序。
  • ...

Puppeteer API 分層結構

Puppeteer 中的 API 分層結構基本和瀏覽器保持一致,下面對常使用到的幾個類介紹一下:es6

image

  • Browser: 對應一個瀏覽器實例,一個 Browser 能夠包含多個 BrowserContext
  • BrowserContext: 對應瀏覽器一個上下文會話,就像咱們打開一個普通的 Chrome 以後又打開一個隱身模式的瀏覽器同樣,BrowserContext 具備獨立的 Session(cookie 和 cache 獨立不共享),一個 BrowserContext 能夠包含多個 Page
  • Page:表示一個 Tab 頁面,經過 browserContext.newPage()/browser.newPage() 建立,browser.newPage() 建立頁面時會使用默認的 BrowserContext,一個 Page 能夠包含多個 Frame
  • Frame: 一個框架,每一個頁面有一個主框架(page.MainFrame()),也能夠多個子框架,主要由 iframe 標籤建立產生的
  • ExecutionContext: 是 javascript 的執行環境,每個 Frame 都一個默認的 javascript 執行環境
  • ElementHandle: 對應 DOM 的一個元素節點,經過該該實例能夠實現對元素的點擊,填寫表單等行爲,咱們能夠經過選擇器,xPath 等來獲取對應的元素
  • JsHandle:對應 DOM 中的 javascript 對象,ElementHandle 繼承於 JsHandle,因爲咱們沒法直接操做 DOM 中對象,因此封裝成 JsHandle 來實現相關功能
  • CDPSession:能夠直接與原生的 CDP 進行通訊,經過 session.send 函數直接發消息,經過 session.on 接收消息,能夠實現 Puppeteer API 中沒有涉及的功能
  • Coverage:獲取 JavaScript 和 CSS 代碼覆蓋率
  • Tracing:抓取性能數據進行分析
  • Response: 頁面收到的響應
  • Request: 頁面發出的請求

Puppeteer 安裝與環境

注意:在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 使用

Case1: 截圖

咱們使用 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'):等待某個選擇器對應的元素出現

Case2: 模擬用戶操做

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'):在輸入框輸入文本

Case3: 植入 javascript 代碼

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 相關函數庫

Case4: 請求攔截

請求在有些場景下頗有必要,攔截一下不必的請求提升性能,咱們能夠在監聽 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') 建立 webWorker
  • page.on('workerdestroyed') 銷燬 webWorker

Case5: 獲取 WebSocket 響應

Puppeteer 目前沒有提供原生的用於處理 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();
})();

Case6: 如何抓取 iframe 中的元素

一個 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();
})();

Case7: 頁面性能分析

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();
})();

Case8: 文件的上傳和下載

在自動化測試中,常常會遇到對於文件的上傳和下載的需求,那麼在 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();
})();

Case9: 跳轉新 tab 頁處理

在點擊一個按鈕跳轉到新的 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;

Case10: 模擬不一樣的設備

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 共享內存
  • 儘可能使用同一個瀏覽器實例,這樣能夠實現緩存共用
  • 經過請求攔截不必加載的資源
  • 像咱們本身打開 Chrome 同樣,tab 頁多必然會卡,因此必須有效控制 tab 頁個數
  • 一個 Chrome 實例啓動時間長了不免會出現內存泄漏,頁面奔潰等現象,因此定時重啓 Chrome 實例是有必要的
  • 爲了加快性能,關閉不必的配置,好比:-no-sandbox(沙箱功能),--disable-extensions(擴展程序)等
  • 儘可能避免使用 page.waifFor(1000),讓程序本身決定效果會更好
  • 由於和 Chrome 實例鏈接時使用的 Websocket,會存在 Websocket sticky session 問題.

參考文獻

相關文章
相關標籤/搜索