Puppeteer: 更友好的 Headless Chrome Node API

很早很早以前,前端就有了對 headless 瀏覽器的需求,最多的應用場景有兩個javascript

  1. UI 自動化測試:擺脫手工瀏覽點擊頁面確認功能模式
  2. 爬蟲:解決頁面內容異步加載等問題

也就有了不少傑出的實現,前端常用的莫過於 PhantomJSselenium-webdriver,但兩個庫有一個共性——難用!環境安裝複雜,API 調用不友好,1027 年 Chrome 團隊連續放了兩個大招 Headless Chrome 和對應的 NodeJS API Puppeteer,直接讓 PhantomJS 和 Selenium IDE for Firefox 做者懸宣佈不必繼續維護其產品html

Puppeteer

如同其 github 項目介紹:Puppeteer 是一個經過 DevTools Protocol 控制 headless chrome 的 high-level Node 庫,也能夠經過設置使用 非 headless Chrome前端

咱們手工能夠在瀏覽器上作的事情 Puppeteer 都能勝任java

  1. 生成網頁截圖或者 PDF
  2. 爬取大量異步渲染內容的網頁,基本就是人肉爬蟲
  3. 模擬鍵盤輸入、表單自動提交、UI 自動化測試

官方提供了一個 playground,能夠快速體驗一下。關於其具體使用不在贅述,官網的 demo 足矣讓徹底不瞭解的同窗入門git

const puppeteer = require('puppeteer');

(async () => {
  const browser = await puppeteer.launch();
  const page = await browser.newPage();
  await page.goto('https://example.com');
  await page.screenshot({path: 'example.png'});

  await browser.close();
})();

實現網頁截圖就這麼簡單,本身也實現了一個簡單的爬取百度圖片的搜索結果的 demo,代碼不過 40 行,用過 selenium-webdriver 的同窗看了會流淚,接下來介紹幾個好玩的特性github

哲學

雖然 Puppeteer API 足夠簡單,但若是是從 webdriver 流轉過來的同窗會很不適應,主要是在 webdirver 中咱們操做網頁更多的是從程序的視角,而在 Puppeteer 中網頁瀏覽者的視角。舉個簡單的例子,咱們但願對一個表單的 input 作輸入web

webdriver 流程chrome

  1. 經過選擇器找到頁面 input 元素
  2. 給元素設置值
const input = await driver.findElement(By.id('kw'));
    await input.sendKeys('test');

Puppeteer 流程api

  1. 光標應該 focus 到元素上
  2. 鍵盤點擊輸入
await page.focus('#kw');
await page.keyboard.sendCharacter('test');

在使用中能夠多感覺一下區別,會發現 Puppeteer 的使用會天然不少瀏覽器

async/await

看官方的例子就能夠看出來,幾乎全部的操做都是異步的,若是堅持使用回調或者 Promise.then 寫出來的代碼會很是醜陋且難讀,Puppeteer 官方推薦的也是使用高版本 Node 用 async/await 語法

const puppeteer = require('puppeteer');

(async () => {
  const browser = await puppeteer.launch();
  const page = await browser.newPage();
  await page.goto('https://news.ycombinator.com', {waitUntil: 'networkidle'});
  await page.pdf({path: 'hn.pdf', format: 'A4'});

  await browser.close();
})();

查找元素

這是 UI 自動化測試最經常使用的功能了,Puppeteer 的處理也至關簡單

  1. page.$(selector)
  2. page.$$(selector)

這兩個函數分別會在頁面內執行 document.querySelectordocument.querySelectorAll,但返回值卻不是 DOM 對象,如同 jQuery 的選擇器,返回的是通過本身包裝的 Promise<ElementHandle>,ElementHandle 幫咱們封裝了經常使用的 clickboundingBox 等方法

獲取 DOM 屬性

咱們寫爬蟲爬取頁面圖片列表,感受能夠經過 page.$$(selector) 獲取到頁面的元素列表,而後再去轉成 DOM 對象,獲取 src,而後並不行,想作對獲取元素對應 DOM 屬性的獲取,須要用專門的 API

  1. page.$eval(selector, pageFunction[, ...args])
  2. page.$$eval(selector, pageFunction[, ...args])

大概用法

const searchValue = await page.$eval('#search', el => el.value);
const preloadHref = await page.$eval('link[rel=preload]', el => el.href);
const html = await page.$eval('.main-container', e => e.outerHTML);
const divsCounts = await page.$$eval('div', divs => divs.length);

值得注意的是若是 pageFunction 返回的是 Promise,那麼 page.$eval 會等待方法 resolve

evaluate

若是咱們有一些及其個性的需求,沒法經過 page.$() 或者 page.$eval() 實現,能夠用大招——evaluate,有幾個相關的 API

  1. page.evaluate(pageFunction, …args)
  2. page.evaluateHandle(pageFunction, …args):
  3. page.evaluateOnNewDocument(pageFunction, ...args)

這幾個函數很是相似,都是能夠在頁面環境執行咱們舒心的 JavaScript,區別主要在執行環境和返回值上

前兩個函數都是在當前頁面環境內執行,的主要區別在返回值上,第一個返回一個 Serializable 的 Promise,第二個返回值是前面提到的 ElementHandle 對象父類型 JSHandle 的 Promise

const result = await page.evaluate(() => {
  return Promise.resolve(8 * 7);
});
console.log(result); // prints "56"

const aWindowHandle = await page.evaluateHandle(() => Promise.resolve(window));
aWindowHandle; // Handle for the window object. 至關於把返回對象作了一層包裹

page.evaluateOnNewDocument(pageFunction, ...args) 是在 browser 環境中執行,執行時機是文檔被建立完成可是 script 沒有執行階段,常常用於修改 JavaScript 環境

註冊函數

page.exposeFunction(name, puppeteerFunction) 用於在 window 對象註冊一個函數,咱們能夠添加一個 window.readfile 函數

const puppeteer = require('puppeteer');
const fs = require('fs');

puppeteer.launch().then(async browser => {
  const page = await browser.newPage();
  page.on('console', msg => console.log(msg.text));
  
  // 註冊 window.readfile
  await page.exposeFunction('readfile', async filePath => {
    return new Promise((resolve, reject) => {
      fs.readFile(filePath, 'utf8', (err, text) => {
        if (err)
          reject(err);
        else
          resolve(text);
      });
    });
  });
  
  await page.evaluate(async () => {
    // use window.readfile to read contents of a file
    const content = await window.readfile('/etc/hosts');
    console.log(content);
  });
  await browser.close();
});

修改終端

Puppeteer 提供了幾個有用的方法讓咱們能夠修改設備信息

  1. page.setViewport(viewport)
  2. page.setUserAgent(userAgent)
await page.setViewport({
  width: 1920,
  height: 1080
});

await page.setUserAgent('Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/61.0.3163.100 Safari/537.36');

page.emulateMedia(mediaType):能夠用來修改頁面訪問的媒體類型,但僅僅支持

  1. screen
  2. print
  3. null:禁用 media emulation

page.emulate(options):前面介紹的幾個函數至關於這個函數的快捷方式,這個函數能夠設置多個內容

  1. viewport
    1. width
    2. height
    3. deviceScaleFactor
    4. isMobile
    5. hasTouch
    6. isLandscape
  2. userAgent

puppeteer/DeviceDescriptors 還給咱們提供了幾個大禮包

const puppeteer = require('puppeteer');
const devices = require('puppeteer/DeviceDescriptors');
const iPhone = devices['iPhone 6'];

puppeteer.launch().then(async browser => {
  const page = await browser.newPage();
  await page.emulate(iPhone);
  await page.goto('https://www.google.com');
  // other actions...
  await browser.close();
});

鍵盤

  1. keyboard.down
  2. keyboard.up
  3. keyboard.press
  4. keyboard.type
  5. keyboard.sendCharacter
// 直接輸入、按鍵
page.keyboard.type('Hello World!');
page.keyboard.press('ArrowLeft');

// 按住不放
page.keyboard.down('Shift');
for (let i = 0; i < ' World'.length; i++)
  page.keyboard.press('ArrowLeft');
page.keyboard.up('Shift');

page.keyboard.press('Backspace');
page.keyboard.sendCharacter('嗨');

鼠標 & 屏幕

  1. mouse.click(x, y, [options]): options 能夠設置
    1. button
    2. clickCount
  2. mouse.move(x, y, [options]): options 能夠設置
    1. steps
  3. mouse.down([options])
  4. mouse.up([options])
  5. touchscreen.tap(x, y)

頁面跳轉控制

這幾個 API 比較簡單,不在展開介紹

  1. page.goto(url, options)
  2. page.goback(options)
  3. page.goForward(options)

事件

Puppeteer 提供了對一些頁面常見事件的監聽,用法和 jQuery 很相似,經常使用的有

  1. console:調用 console API
  2. dialog:頁面出現彈窗
  3. error:頁面 crash
  4. load
  5. pageerror:頁面內未捕獲錯誤
page.on('load', async () => {
  console.log('page loading done, start fetch...');

  const srcs = await page.$$eval((img) => img.src);
  console.log(`get ${srcs.length} images, start download`);

  srcs.forEach(async (src) => {
    // sleep
    await page.waitFor(200);
    await srcToImg(src, mn);
  });

  await browser.close();

});

性能

經過 page.getMetrics() 能夠獲得一些頁面性能數據

  • Timestamp The timestamp when the metrics sample was taken.
  • Documents 頁面文檔數
  • Frames 頁面 frame 數
  • JSEventListeners 頁面內事件監聽器數
  • Nodes 頁面 DOM 節點數
  • LayoutCount 頁面 layout 數
  • RecalcStyleCount 樣式重算數
  • LayoutDuration 頁面 layout 時間
  • RecalcStyleDuration 樣式重算時長
  • ScriptDuration script 時間
  • TaskDuration 全部瀏覽器任務時長
  • JSHeapUsedSize JavaScript 佔用堆大小
  • JSHeapTotalSize JavaScript 堆總量
{ 
  Timestamp: 382305.912236,
  Documents: 5,
  Frames: 3,
  JSEventListeners: 129,
  Nodes: 8810,
  LayoutCount: 38,
  RecalcStyleCount: 56,
  LayoutDuration: 0.596341000346001,
  RecalcStyleDuration: 0.180430999898817,
  ScriptDuration: 1.24401400075294,
  TaskDuration: 2.21657899935963,
  JSHeapUsedSize: 15430816,
  JSHeapTotalSize: 23449600 
}

最後

本文知識介紹了部分經常使用的 API,所有的 API 能夠在 github 上查看,因爲 Puppeteer 尚未發佈正式版,API 迭代比較迅速,在使用中遇到問題也能夠在 issue 中反饋。

在 0.11 版本中只有 page.$eval 並無 page.$$eval,使用的時候只能經過 page.evaluate,經過你們的反饋,在 0.12 中已經添加了該功能,整體而言 Puppeteer 仍是一個十分值得期待的 Node headless API

參考

Getting Started with Headless Chrome

無頭瀏覽器 Puppeteer 初探

Getting started with Puppeteer and Chrome Headless for Web Scraping

相關文章
相關標籤/搜索