很早很早以前,前端就有了對 headless 瀏覽器的需求,最多的應用場景有兩個javascript
也就有了不少傑出的實現,前端常用的莫過於 PhantomJS 和 selenium-webdriver,但兩個庫有一個共性——難用!環境安裝複雜,API 調用不友好,1027 年 Chrome 團隊連續放了兩個大招 Headless Chrome 和對應的 NodeJS API Puppeteer,直接讓 PhantomJS 和 Selenium IDE for Firefox 做者懸宣佈不必繼續維護其產品html
如同其 github 項目介紹:Puppeteer 是一個經過 DevTools Protocol 控制 headless chrome 的 high-level Node 庫,也能夠經過設置使用 非 headless Chrome前端
咱們手工能夠在瀏覽器上作的事情 Puppeteer 都能勝任java
官方提供了一個 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
const input = await driver.findElement(By.id('kw')); await input.sendKeys('test');
Puppeteer 流程api
await page.focus('#kw'); await page.keyboard.sendCharacter('test');
在使用中能夠多感覺一下區別,會發現 Puppeteer 的使用會天然不少瀏覽器
看官方的例子就能夠看出來,幾乎全部的操做都是異步的,若是堅持使用回調或者 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 的處理也至關簡單
這兩個函數分別會在頁面內執行 document.querySelector
和 document.querySelectorAll
,但返回值卻不是 DOM 對象,如同 jQuery 的選擇器,返回的是通過本身包裝的 Promise<ElementHandle>
,ElementHandle 幫咱們封裝了經常使用的 click
、boundingBox
等方法
咱們寫爬蟲爬取頁面圖片列表,感受能夠經過 page.$$(selector)
獲取到頁面的元素列表,而後再去轉成 DOM 對象,獲取 src,而後並不行,想作對獲取元素對應 DOM 屬性的獲取,須要用專門的 API
大概用法
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
若是咱們有一些及其個性的需求,沒法經過 page.$() 或者 page.$eval() 實現,能夠用大招——evaluate,有幾個相關的 API
這幾個函數很是相似,都是能夠在頁面環境執行咱們舒心的 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 提供了幾個有用的方法讓咱們能夠修改設備信息
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):能夠用來修改頁面訪問的媒體類型,但僅僅支持
page.emulate(options):前面介紹的幾個函數至關於這個函數的快捷方式,這個函數能夠設置多個內容
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(); });
// 直接輸入、按鍵 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('嗨');
這幾個 API 比較簡單,不在展開介紹
Puppeteer 提供了對一些頁面常見事件的監聽,用法和 jQuery 很相似,經常使用的有
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
Getting started with Puppeteer and Chrome Headless for Web Scraping