一、Puppeteer 簡介

Puppeteer 是一個node庫,他提供了一組用來操縱Chrome的API, 通俗來講就是一個 headless chrome瀏覽器 (固然你也能夠配置成有UI的,默認是沒有的)。既然是瀏覽器,那麼咱們手工能夠在瀏覽器上作的事情 Puppeteer 都能勝任, 另外,Puppeteer 翻譯成中文是」木偶」意思,因此聽名字就知道,操縱起來很方便,你能夠很方便的操縱她去實現:javascript

1) 生成網頁截圖或者 PDF 
2) 高級爬蟲,能夠爬取大量異步渲染內容的網頁 
3) 模擬鍵盤輸入、表單自動提交、登陸網頁等,實現 UI 自動化測試 
4) 捕獲站點的時間線,以便追蹤你的網站,幫助分析網站性能問題php

若是你用過 PhantomJS 的話,你會發現她們有點相似,但Puppeteer是Chrome官方團隊進行維護的,用俗話說就是」有孃家的人「,前景更好。css

二、運行環境

查看 Puppeteer 的官方 API 你會發現滿屏的 async, await 之類,這些都是 ES7 的規範,因此你須要:html

  1. Nodejs 的版本不能低於 v7.6.0, 須要支持 async, await.
  2. 須要最新的 chrome driver, 這個你在經過 npm 安裝 Puppeteer 的時候系統會自動下載的
npm install puppeteer --save 

三、基本用法

先開看看官方的入門的 DEMOjava

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

上面這段代碼就實現了網頁截圖,先大概解讀一下上面幾行代碼:node

  1. 先經過 puppeteer.launch() 建立一個瀏覽器實例 Browser 對象
  2. 而後經過 Browser 對象建立頁面 Page 對象
  3. 而後 page.goto() 跳轉到指定的頁面
  4. 調用 page.screenshot() 對頁面進行截圖
  5. 關閉瀏覽器

是否是以爲好簡單? 反正我是以爲比 PhantomJS 簡單,至於跟 selenium-webdriver 比起來, 那更不用說了。下面就介紹一下 puppeteer 的經常使用的幾個 API。linux

3.1 puppeteer.launch(options)

使用 puppeteer.launch() 運行 puppeteer,它會 return 一個 promise,使用 then 方法獲取 browser 實例, 固然高版本的 的 nodejs 已經支持 await 特性了,因此上面的例子使用 await 關鍵字,這一點須要特殊說明一下,Puppeteer 幾乎全部的操做都是 異步的, 爲了使用大量的 then 使得代碼的可讀性下降,本文全部 demo 代碼都是用 async, await 方式實現。這個 也是 Puppeteer 官方推薦的寫法。對 async/await 一臉懵逼的同窗狠狠的戳這裏git

options 參數詳解
參數名稱 參數類型 參數說明
ignoreHTTPSErrors boolean 在請求的過程當中是否忽略 Https 報錯信息,默認爲 false
headless boolean 是否以」無頭」的模式運行 chrome, 也就是不顯示 UI, 默認爲 true
executablePath string 可執行文件的路勁,Puppeteer 默認是使用它自帶的 chrome webdriver, 若是你想指定一個本身的 webdriver 路徑,能夠經過這個參數設置
slowMo number 使 Puppeteer 操做減速,單位是毫秒。若是你想看看 Puppeteer 的整個工做過程,這個參數將很是有用。
args Array(String) 傳遞給 chrome 實例的其餘參數,好比你可使用」–ash-host-window-bounds=1024x768」 來設置瀏覽器窗口大小。更多參數參數列表能夠參考這裏
handleSIGINT boolean 是否容許經過進程信號控制 chrome 進程,也就是說是否可使用 CTRL+C 關閉並退出瀏覽器.
timeout number 等待 Chrome 實例啓動的最長時間。默認爲30000(30秒)。若是傳入 0 的話則不限制時間
dumpio boolean 是否將瀏覽器進程stdout和stderr導入到process.stdout和process.stderr中。默認爲false。
userDataDir string 設置用戶數據目錄,默認linux 是在 ~/.config 目錄,window 默認在 C:\Users{USER}\AppData\Local\Google\Chrome\User Data, 其中 {USER} 表明當前登陸的用戶名
env Object 指定對Chromium可見的環境變量。默認爲process.env。
devtools boolean 是否爲每一個選項卡自動打開DevTools面板, 這個選項只有當 headless 設置爲 false 的時候有效

3.2 Browser 對象

當 Puppeteer 鏈接到一個 Chrome 實例的時候就會建立一個 Browser 對象,有如下兩種方式:github

Puppeteer.launch 和 Puppeteer.connect.web

下面這個 DEMO 實現斷開鏈接以後從新鏈接瀏覽器實例

const puppeteer = require('puppeteer'); puppeteer.launch().then(async browser => { // 保存 Endpoint,這樣就能夠從新鏈接 Chromium const browserWSEndpoint = browser.wsEndpoint(); // 從Chromium 斷開鏈接 browser.disconnect(); // 使用endpoint 從新和 Chromiunm 創建鏈接 const browser2 = await puppeteer.connect({browserWSEndpoint}); // Close Chromium await browser2.close(); }); 
Browser 對象 API
方法名稱 返回值 說明
browser.close() Promise 關閉瀏覽器
browser.disconnect() void 斷開瀏覽器鏈接
browser.newPage() Promise(Page) 建立一個 Page 實例
browser.pages() Promise(Array(Page)) 獲取全部打開的 Page 實例
browser.targets() Array(Target) 獲取全部活動的 targets
browser.version() Promise(String) 獲取瀏覽器的版本
browser.wsEndpoint() String 返回瀏覽器實例的 socket 鏈接 URL, 能夠經過這個 URL 重鏈接 chrome 實例

好了,Puppeteer 的API 就不一一介紹了,官方提供的詳細的 API, 戳這裏

四、Puppeteer 實戰

瞭解 API 以後咱們就能夠來一些實戰了,在此以前,咱們先了解一下 Puppeteer 的設計原理,簡單來講 Puppeteer 跟 webdriver 以及 PhantomJS 最大的 的不一樣就是它是站在用戶瀏覽的角度,而 webdriver 和 PhantomJS 最初設計就是用來作自動化測試的,因此它是站在機器瀏覽的角度來設計的,因此它們 使用的是不一樣的設計哲學。舉個栗子,加入我須要打開京東的首頁並進行一次產品搜索,分別看看使用 Puppeteer 和 webdriver 的實現流程:

Puppeteer 的實現流程:

  1. 打開京東首頁
  2. 將光標 focus 到搜索輸入框
  3. 鍵盤點擊輸入文字
  4. 點擊搜索按鈕

webdriver 的實現流程:

  1. 打開京東首頁
  2. 找到輸入框的 input 元素
  3. 設置 input 的值爲要搜索文字
  4. 觸發搜索按鈕的單機事件

我的感受 Puppeteer 設計哲學更符合任何的操做習慣,更天然一些。

下面咱們就用一個簡單的需求實現來進行 Puppeteer 的入門學習。這個簡單的需求就是:

在京東商城抓取10個手機商品,並把商品的詳情頁截圖。

首先咱們來梳理一下操做流程

  1. 打開京東首頁
  2. 輸入「手機」關鍵字並搜索
  3. 獲取前10個商品的 A 標籤,並獲取 href 屬性值,獲取商品詳情連接
  4. 分別打開10個商品的詳情頁,截取網頁圖片

要實現上面的功能須要用到查找元素,獲取屬性,鍵盤事件等,那接下來咱們就一個一個的講解一下。

4.1 獲取元素

Page 對象提供了2個 API 來獲取頁面元素

(1). Page.$(selector) 獲取單個元素,底層是調用的是 document.querySelector() , 因此選擇器的 selector 格式遵循 css 選擇器規範

let inputElement = await page.$("#search", input => input); //下面寫法等價 let inputElement = await page.$('#search'); 

(2). Page.$$(selector) 獲取一組元素,底層調用的是 document.querySelectorAll(). 返回 Promise(Array(ElemetHandle)) 元素數組.

const links = await page.$$("a"); //下面寫法等價 const links = await page.$$("a", links => links); 

最終返回的都是 ElemetHandle 對象

4.2 獲取元素屬性

Puppeteer 獲取元素屬性跟咱們平時寫前段的js的邏輯有點不同,按照一般的邏輯,應該是現獲取元素,而後在獲取元素的屬性。可是上面咱們知道 獲取元素的 API 最終返回的都是 ElemetHandle 對象,而你去查看 ElemetHandle 的 API 你會發現,它並無獲取元素屬性的 API.

事實上 Puppeteer 專門提供了一套獲取屬性的 API, Page.$eval() 和 Page.$$eval()

(1). Page.$$eval(selector, pageFunction[, …args]), 獲取單個元素的屬性,這裏的選擇器 selector 跟上面 Page.$(selector) 是同樣的。

const value = await page.$eval('input[name=search]', input => input.value); const href = await page.$eval('#a", ele => ele.href); const content = await page.$eval('.content', ele => ele.outerHTML); 

4.3 執行自定義的 JS 腳本

Puppeteer 的 Page 對象提供了一系列 evaluate 方法,你能夠經過他們來執行一些自定義的 js 代碼,主要提供了下面三個 API

(1). page.evaluate(pageFunction, …args) 返回一個可序列化的普通對象,pageFunction 表示要在頁面執行的函數, args 表示傳入給 pageFunction 的參數, 下面的 pageFunction 和 args 表示一樣的意思。

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

這個方法頗有用,好比咱們在獲取頁面的截圖的時候,默認是隻截圖當前瀏覽器窗口的尺寸大小,默認值是800x600,那若是咱們須要獲取整個網頁的完整 截圖是沒辦法辦到的。Page.screenshot() 方法提供了能夠設置截圖區域大小的參數,那麼咱們只要在頁面加載完了以後獲取頁面的寬度和高度就能夠解決 這個問題了。

(async () => { const browser = await puppeteer.launch({headless:true}); const page = await browser.newPage(); await page.goto('https://jr.dayi35.com'); await page.setViewport({width:1920, height:1080}); const documentSize = await page.evaluate(() => { return { width: document.documentElement.clientWidth, height : document.body.clientHeight, } }) await page.screenshot({path:"example.png", clip : {x:0, y:0, width:1920, height:documentSize.height}}); await browser.close(); })(); 

(2). Page.evaluateHandle(pageFunction, …args) 在 Page 上下文執行一個 pageFunction, 返回 JSHandle 實體

const aWindowHandle = await page.evaluateHandle(() => Promise.resolve(window)); aWindowHandle; // Handle for the window object. const aHandle = await page.evaluateHandle('document'); // Handle for the 'document'. 

從上面的代碼能夠看出,page.evaluateHandle() 方法也是經過 Promise.resolve 方法直接把 Promise 的最終處理結果返回, 只不過把最後返回的對象封裝成了 JSHandle 對象。本質上跟 evaluate 沒有什麼區別。

下面這段代碼實現獲取頁面的動態(包括js動態插入的元素) HTML 代碼.

const aHandle = await page.evaluateHandle(() => document.body); const resultHandle = await page.evaluateHandle(body => body.innerHTML, aHandle); console.log(await resultHandle.jsonValue()); await resultHandle.dispose(); 

(3). page.evaluateOnNewDocument(pageFunction, …args), 在文檔頁面載入前調用 pageFunction, 若是頁面中有 iframe 或者 frame, 則函數調用 的上下文環境將變成子頁面的,即iframe 或者 frame, 因爲是在頁面加載前調用,這個函數通常是用來初始化 javascript 環境的,好比重置或者 初始化一些全局變量。

4.4 Page.exposeFunction

除此上面三個 API 以外,還有一相似的很是有用的 API, 那就是 Page.exposeFunction,這個 API 用來在頁面註冊全局函數,很是有用:

由於有時候須要在頁面處理一些操做的時候須要用到一些函數,雖然能夠經過 Page.evaluate() API 在頁面定義函數,好比:

const docSize = await page.evaluate(()=> { function getPageSize() { return { width: document.documentElement.clientWidth, height : document.body.clientHeight, } } return getPageSize(); }); 

可是這樣的函數不是全局的,須要在每一個 evaluate 中去從新定義,沒法作到代碼複用,在一個就是 nodejs 有不少工具包能夠很輕鬆的實現很複雜的功能 好比要實現 md5 加密函數,這個用純 js 去實現就不太方便了,而用 nodejs 倒是幾行代碼的事情。

下面代碼實現給 Page 上下文的 window 對象添加 md5 函數:

const puppeteer = require('puppeteer'); const crypto = require('crypto'); puppeteer.launch().then(async browser => { const page = await browser.newPage(); page.on('console', msg => console.log(msg.text)); await page.exposeFunction('md5', text => crypto.createHash('md5').update(text).digest('hex') ); await page.evaluate(async () => { // use window.md5 to compute hashes const myString = 'PUPPETEER'; const myHash = await window.md5(myString); console.log(`md5 of ${myString} is ${myHash}`); }); await browser.close(); }); 

能夠看出,Page.exposeFunction API 使用起來是很方便的,也很是有用,在好比給 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)); 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(); }); 

五、Page.emulate 修改模擬器(客戶端)運行配置

Puppeteer 提供了一些 API 供咱們修改瀏覽器終端的配置

  1. Page.setViewport() 修改瀏覽器視窗大小
  2. Page.setUserAgent() 設置瀏覽器的 UserAgent 信息
  3. Page.emulateMedia() 更改頁面的CSS媒體類型,用於進行模擬媒體仿真。 可選值爲 「screen」, 「print」, 「null」, 若是設置爲 null 則表示禁用媒體仿真。
  4. Page.emulate() 模擬設備,參數設備對象,好比 iPhone, Mac, Android 等
page.setViewport({width:1920, height:1080}); //設置視窗大小爲 1920x1080 page.setUserAgent('Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/60.0.3112.90 Safari/537.36'); page.emulateMedia('print'); //設置打印機媒體樣式 

除此以外咱們還能夠模擬非 PC 機設備, 好比下面這段代碼模擬 iPhone 6 訪問google:

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

Puppeteer 支持不少設備模擬仿真,好比Galaxy, iPhone, IPad 等,想要知道詳細設備支持,請戳這裏 DeviceDescriptors.js.

六、鍵盤和鼠標

鍵盤和鼠標的API比較簡單,鍵盤的幾個API以下:

  • keyboard.down(key[, options]) 觸發 keydown 事件
  • keyboard.press(key[, options]) 按下某個鍵,key 表示鍵的名稱,好比 ‘ArrowLeft’ 向左鍵,詳細的鍵名映射請戳這裏
  • keyboard.sendCharacter(char) 輸入一個字符
  • keyboard.type(text, options) 輸入一個字符串
  • keyboard.up(key) 觸發 keyup 事件
page.keyboard.press("Shift"); //按下 Shift 鍵 page.keyboard.sendCharacter('嗨'); page.keyboard.type('Hello'); // 一次輸入完成 page.keyboard.type('World', {delay: 100}); // 像用戶同樣慢慢輸入 

鼠標操做:

  • mouse.click(x, y, [options]) 移動鼠標指針到指定的位置,而後按下鼠標,這個其實 mouse.move 和 mouse.down 或 mouse.up 的快捷操做
  • mouse.down([options]) 觸發 mousedown 事件,options 可配置:
    • options.button 按下了哪一個鍵,可選值爲[left, right, middle], 默認是 left, 表示鼠標左鍵
    • options.clickCount 按下的次數,單擊,雙擊或者其餘次數
    • delay 按鍵延時時間
  • mouse.move(x, y, [options]) 移動鼠標到指定位置, options.steps 表示移動的步長
  • mouse.up([options]) 觸發 mouseup 事件

七、另外幾個有用的 API

Puppeteer 還提供幾個很是有用的 API, 好比:

7.1 Page.waitFor 系列 API

  • page.waitFor(selectorOrFunctionOrTimeout[, options[, …args]]) 下面三個的綜合 API
  • page.waitForFunction(pageFunction[, options[, …args]]) 等待 pageFunction 執行完成以後
  • page.waitForNavigation(options) 等待頁面基本元素加載完以後,好比同步的 HTML, CSS, JS 等代碼
  • page.waitForSelector(selector[, options]) 等待某個選擇器的元素加載以後,這個元素能夠是異步加載的,這個 API 很是有用,你懂的。

好比我想獲取某個經過 js 異步加載的元素,那麼直接獲取確定是獲取不到的。這個時候就可使用 page.waitForSelector 來解決:

await page.waitForSelector('.gl-item'); //等待元素加載以後,不然獲取不到異步加載的元素 const links = await page.$$eval('.gl-item > .gl-i-wrap > .p-img > a', links => { return links.map(a => { return { href: a.href.trim(), name: a.title } }); }); 

其實上面的代碼就能夠解決咱們最上面的需求,抓取京東的產品,由於是異步加載的,因此使用這種方式。

7.2 page.getMetrics()

經過 page.getMetrics() 能夠獲得一些頁面性能數據, 捕獲網站的時間線跟蹤,以幫助診斷性能問題。

  • Timestamp 度量標準採樣的時間戳
  • Documents 頁面文檔數
  • Frames 頁面 frame 數
  • JSEventListeners 頁面內事件監聽器數
  • Nodes 頁面 DOM 節點數
  • LayoutCount 頁面佈局總數
  • RecalcStyleCount 樣式重算數
  • LayoutDuration 全部頁面佈局的合併持續時間
  • RecalcStyleDuration 全部頁面樣式從新計算的組合持續時間。
  • ScriptDuration 全部腳本執行的持續時間
  • TaskDuration 全部瀏覽器任務時長
  • JSHeapUsedSize JavaScript 佔用堆大小
  • JSHeapTotalSize JavaScript 堆總量

八、總結和源碼

本文經過一個實際需求來學習了 Puppeteer 的一些基本的經常使用的 API, API 的版本是 v0.13.0-alpha. 最新邦本的 API 請參考 Puppeteer 官方API.

總的來講,Puppeteer 真是一款不錯的 headless 工具,操做簡單,功能強大。用來作UI自動化測試,和一些小工具都是很不錯的。

下面貼上咱們開始的需求實現源碼,僅供參考:

//延時函數 function sleep(delay) { return new Promise((resolve, reject) => { setTimeout(() => { try { resolve(1) } catch (e) { reject(0) } }, delay) }) } const puppeteer = require('puppeteer'); puppeteer.launch({ ignoreHTTPSErrors:true, headless:false,slowMo:250, timeout:0}).then(async browser => { let page = await browser.newPage(); await page.setJavaScriptEnabled(true); await page.goto("https://www.jd.com/"); const searchInput = await page.$("#key"); await searchInput.focus(); //定位到搜索框 await page.keyboard.type("手機"); const searchBtn = await page.$(".button"); await searchBtn.click(); await page.waitForSelector('.gl-item'); //等待元素加載以後,不然獲取不異步加載的元素 const links = await page.$$eval('.gl-item > .gl-i-wrap > .p-img > a', links => { return links.map(a => { return { href: a.href.trim(), title: a.title } }); }); page.close(); const aTags = links.splice(0, 10); for (var i = 1; i < aTags.length; i++) { page = await browser.newPage() page.setJavaScriptEnabled(true); await page.setViewport({width:1920, height:1080}); var a = aTags[i]; await page.goto(a.href, {timeout:0}); //防止頁面太長,加載超時 //注入代碼,慢慢把滾動條滑到最底部,保證全部的元素被所有加載 let scrollEnable = true; let scrollStep = 500; //每次滾動的步長 while (scrollEnable) { scrollEnable = await page.evaluate((scrollStep) => { let scrollTop = document.scrollingElement.scrollTop; document.scrollingElement.scrollTop = scrollTop + scrollStep; return document.body.clientHeight > scrollTop + 1080 ? true : false }, scrollStep); await sleep(100); } await page.waitForSelector("#footer-2014", {timeout:0}); //判斷是否到達底部了 let filename = "images/items-"+i+".png"; //這裏有個Puppeteer的bug一直沒有解決,發現截圖的高度最大隻能是16384px, 超出部分被截掉了。 await page.screenshot({path:filename, fullPage:true}); page.close(); } browser.close(); }); 

《THE END》