1.puppeteer簡介
puppeteer是一個node庫,是Google chrome團隊官方的無界面(headless)chrome工具。它提供了一組用來操縱Chrome的 API,允許通過 JS代碼操縱Chrome瀏覽器,完成數據爬取、Web程序自動測試等任務。Puppeteer項目在GitHub上開源。
puppeteer核心功能
1.利用網頁生成PDF、圖片
2.爬取SPA應用,并生成預渲染內容(“SSR”服務端渲染)
3.從網站抓取內容
4.自動化表單提交、UI測試、鍵盤輸入
5.幫助創建最新的自動化測試環境(chrome),可以直接運行測試用例
6.捕獲站點的時間線,以便追蹤網站,幫助分析網站性能問題
Chrome Headless環境要求
Puppeteer要求node版本不低于v6.4.0,但是async/await只在Node v7.6.0或更高的版本支持。
需要最近版本的Chromium瀏覽器
2.環境安裝
安裝node 8.+
若已經安裝了node,cmd中輸入node -v?查看node的版本。若要更新node到最新版本,只需要下載安裝最新版本的node即可(安裝的時候要注意,選擇路徑要與之前安裝node的路徑相同<查看之前安裝的node的路徑:where node>,這樣就相當于更新了)。
初始化項目
新建一個文件夾作為工作目錄
我這里新建文件夾名稱為myPuppeteer,進入該文件夾,在此處打開命令行窗口cmd,輸入npm init?初始化npm,一步步回車即可。
安裝Puppeteer
命令行輸入以下命令:npm i puppeteer?--save
3.頁面截圖小案例
初始化步驟完成后,node_modules放在根目錄下,依次打開以下文件夾:node_modules—puppeteer—examples.
進入到examples文件夾下后,里面有很多js文件,這些一般都是一些小例子。以screenshot.js為例,看一個頁面截圖的例子。?
我們以記事本方式打開該文件,主要js代碼如下:
'use strict';constpuppeteer =require('puppeteer');//引入puppeteer庫.
(async() => {
? ? ? ? ? ? ? ? ? constbrowser = await puppeteer.launch();//用指定選項啟動一個Chromium瀏覽器實例。
? ? ? ? ? ? ? ? ? constpage = await browser.newPage();//創建一個頁面.
? ? ? ? ? ? ? ? ? await page.goto('http://example.com');//到指定頁面的網址.
? ? ? ? ? ? ? ? ? await page.screenshot({path:'example.png'});//截圖并保存到當前路徑,名稱為example.png.
? ? ? ? ? ? ? ? ? await browser.close();//關閉已打開的頁面,browser不能再使用。
? ? ? ? ? ? ? ? })();
方式1.在pycharm中新建一個file,命名為test.js,將上面的代碼拷貝到test.js中并保存,在terminal中輸入node screenshot.js//運行名為screenshot.js的文件.
方式2.還是在當前文件夾(examples)下,此處打開命令行窗口,輸入:node screenshot.js//運行名為screenshot.js的文件.
運行完成后,就會在當前目錄下看到名為example.png的圖片,該圖片即運行該js后截的圖片。
4、Puppeteer 實戰
了解 API 之后我們就可以來一些實戰了,在此之前,我們先了解一下 Puppeteer 的設計原理,簡單來說 Puppeteer 跟 webdriver 以及 PhantomJS 最大的 的不同就是它是站在用戶瀏覽的角度,而 webdriver 和 PhantomJS 最初設計就是用來做自動化測試的,所以它是站在機器瀏覽的角度來設計的,所以它們 使用的是不同的設計哲學。舉個栗子,加入我需要打開京東的首頁并進行一次產品搜索,分別看看使用 Puppeteer 和 webdriver 的實現流程:
Puppeteer 的實現流程:
打開京東首頁
將光標 focus 到搜索輸入框
鍵盤點擊輸入文字
點擊搜索按鈕
webdriver 的實現流程:
打開京東首頁
找到輸入框的 input 元素
設置 input 的值為要搜索文字
觸發搜索按鈕的單機事件
個人感覺 Puppeteer 設計哲學更符合任何的操作習慣,更自然一些。
下面我們就用一個簡單的需求實現來進行 Puppeteer 的入門學習。這個簡單的需求就是:
在京東商城抓取10個手機商品,并把商品的詳情頁截圖。
首先我們來梳理一下操作流程
打開京東首頁
輸入“手機”關鍵字并搜索
獲取前10個商品的 A 標簽,并獲取 href 屬性值,獲取商品詳情鏈接
分別打開10個商品的詳情頁,截取網頁圖片
要實現上面的功能需要用到查找元素,獲取屬性,鍵盤事件等,那接下來我們就一個一個的講解一下。
4.1 獲取元素
Page 對象提供了2個 API 來獲取頁面元素
(1). Page.$(selector) 獲取單個元素,底層是調用的是 document.querySelector() , 所以選擇器的 selector 格式遵循?css 選擇器規范
letinputElement=awaitpage.$("#search",input=>input);//下面寫法等價letinputElement=awaitpage.$('#search');
(2). Page.$$(selector) 獲取一組元素,底層調用的是 document.querySelectorAll(). 返回 Promise(Array(ElemetHandle)) 元素數組.
constlinks=awaitpage.$$("a");//下面寫法等價constlinks=awaitpage.$$("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) 是一樣的。
constvalue=awaitpage.$eval('input[name=search]',input=>input.value);consthref=awaitpage.$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 表示同樣的意思。
constresult=awaitpage.evaluate(() =>{returnPromise.resolve(8*7);});console.log(result);//prints"56"
這個方法很有用,比如我們在獲取頁面的截圖的時候,默認是只截圖當前瀏覽器窗口的尺寸大小,默認值是800x600,那如果我們需要獲取整個網頁的完整 截圖是沒辦法辦到的。Page.screenshot() 方法提供了可以設置截圖區域大小的參數,那么我們只要在頁面加載完了之后獲取頁面的寬度和高度就可以解決 這個問題了。
(async()=>{constbrowser=awaitpuppeteer.launch({headless:true});constpage=awaitbrowser.newPage();awaitpage.goto('https://jr.dayi35.com');awaitpage.setViewport({width:1920,height:1080});constdocumentSize=awaitpage.evaluate(()=>{return{width:document.documentElement.clientWidth,height:document.body.clientHeight,}})awaitpage.screenshot({path:"example.png",clip:{x:0,y:0,width:1920,height:documentSize.height}});awaitbrowser.close();})();
(2). Page.evaluateHandle(pageFunction, …args) 在 Page 上下文執行一個 pageFunction, 返回 JSHandle 實體
constaWindowHandle=awaitpage.evaluateHandle(()=>Promise.resolve(window));aWindowHandle;// Handle for the window object. constaHandle=awaitpage.evaluateHandle('document');// Handle for the 'document'.
從上面的代碼可以看出,page.evaluateHandle() 方法也是通過 Promise.resolve 方法直接把 Promise 的最終處理結果返回, 只不過把最后返回的對象封裝成了 JSHandle 對象。本質上跟 evaluate 沒有什么區別。
下面這段代碼實現獲取頁面的動態(包括js動態插入的元素) HTML 代碼.
constaHandle=awaitpage.evaluateHandle(()=>document.body);constresultHandle=awaitpage.evaluateHandle(body=>body.innerHTML,aHandle);console.log(awaitresultHandle.jsonValue());awaitresultHandle.dispose();
(3). page.evaluateOnNewDocument(pageFunction, …args), 在文檔頁面載入前調用 pageFunction, 如果頁面中有 iframe 或者 frame, 則函數調用 的上下文環境將變成子頁面的,即iframe 或者 frame, 由于是在頁面加載前調用,這個函數一般是用來初始化 javascript 環境的,比如重置或者 初始化一些全局變量。
4.4 Page.exposeFunction
除此上面三個 API 之外,還有一類似的非常有用的 API, 那就是?Page.exposeFunction,這個 API 用來在頁面注冊全局函數,非常有用:
因為有時候需要在頁面處理一些操作的時候需要用到一些函數,雖然可以通過 Page.evaluate() API 在頁面定義函數,比如:
constdocSize=awaitpage.evaluate(()=>{function getPageSize() {return{width:document.documentElement.clientWidth,height:document.body.clientHeight,}}returngetPageSize();});
但是這樣的函數不是全局的,需要在每個 evaluate 中去重新定義,無法做到代碼復用,在一個就是 nodejs 有很多工具包可以很輕松的實現很復雜的功能 比如要實現 md5 加密函數,這個用純 js 去實現就不太方便了,而用 nodejs 卻是幾行代碼的事情。
下面代碼實現給 Page 上下文的 window 對象添加 md5 函數:
constpuppeteer=require('puppeteer');constcrypto=require('crypto');puppeteer.launch().then(asyncbrowser=>{constpage=awaitbrowser.newPage();page.on('console',msg=>console.log(msg.text));awaitpage.exposeFunction('md5',text=>crypto.createHash('md5').update(text).digest('hex'));awaitpage.evaluate(async()=>{// use window.md5 to compute hashesconstmyString='PUPPETEER';constmyHash=awaitwindow.md5(myString);console.log(`md5 of ${myString} is ${myHash}`);});awaitbrowser.close();});
可以看出,Page.exposeFunction API 使用起來是很方便的,也非常有用,在比如給 window 對象注冊 readfile 全局函數:
constpuppeteer=require('puppeteer');constfs=require('fs');puppeteer.launch().then(asyncbrowser=>{constpage=awaitbrowser.newPage();page.on('console',msg=>console.log(msg.text));awaitpage.exposeFunction('readfile',asyncfilePath=>{returnnewPromise((resolve,reject)=>{fs.readFile(filePath,'utf8',(err,text)=>{if(err)reject(err);elseresolve(text);});});});awaitpage.evaluate(async()=>{// use window.readfile to read contents of a fileconstcontent=awaitwindow.readfile('/etc/hosts');console.log(content);});awaitbrowser.close();});
5、Page.emulate 修改模擬器(客戶端)運行配置
Puppeteer 提供了一些 API 供我們修改瀏覽器終端的配置
Page.setViewport() 修改瀏覽器視窗大小
Page.setUserAgent() 設置瀏覽器的 UserAgent 信息
Page.emulateMedia() 更改頁面的CSS媒體類型,用于進行模擬媒體仿真。 可選值為 “screen”, “print”, “null”, 如果設置為 null 則表示禁用媒體仿真。
Page.emulate() 模擬設備,參數設備對象,比如 iPhone, Mac, Android 等
page.setViewport({width:1920,height:1080});//設置視窗大小為 1920x1080page.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:
constpuppeteer=require('puppeteer');constdevices=require('puppeteer/DeviceDescriptors');constiPhone=devices['iPhone 6'];puppeteer.launch().then(asyncbrowser=>{constpage=awaitbrowser.newPage();awaitpage.emulate(iPhone);awaitpage.goto('https://www.google.com');// other actions...awaitbrowser.close();});
Puppeteer 支持很多設備模擬仿真,比如Galaxy, iPhone, IPad 等,想要知道詳細設備支持,請戳這里?DeviceDescriptors.js.
6、鍵盤和鼠標
鍵盤和鼠標的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 事件
7、另外幾個有用的 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 來解決:
awaitpage.waitForSelector('.gl-item');//等待元素加載之后,否則獲取不到異步加載的元素constlinks=awaitpage.$$eval('.gl-item > .gl-i-wrap > .p-img > a',links=>{returnlinks.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 堆總量
8、總結和源碼
本文通過一個實際需求來學習了 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.setViewport({width:1920, height:1080});
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,11);
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 =1000;//每次滾動的步長
? ? ? 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-2017", {timeout:0}); //判斷是否到達底部了
//? ? let filename = "images/items-"+i+".png";
//這里有個Puppeteer的bug一直沒有解決,發現截圖的高度最大只能是16384px, 超出部分被截掉了。
//? ? await page.screenshot({path:filename, fullPage:true});
? ? ? await page.screenshot({path:"images/items-"+i+".png"});
page.close();
}
browser.close();
});