您好,登錄后才能下訂單哦!
1、Puppeteer 簡介
Puppeteer 是一個node庫,他提供了一組用來操縱Chrome的API, 通俗來說就是一個 headless chrome瀏覽器 (當然你也可以配置成有UI的,默認是沒有的)。既然是瀏覽器,那么我們手工可以在瀏覽器上做的事情 Puppeteer 都能勝任, 另外,Puppeteer 翻譯成中文是”木偶”意思,所以聽名字就知道,操縱起來很方便,你可以很方便的操縱她去實現:
1) 生成網頁截圖或者 PDF
2) 高級爬蟲,可以爬取大量異步渲染內容的網頁
3) 模擬鍵盤輸入、表單自動提交、登錄網頁等,實現 UI 自動化測試
4) 捕獲站點的時間線,以便追蹤你的網站,幫助分析網站性能問題
如果你用過 PhantomJS 的話,你會發現她們有點類似,但Puppeteer是Chrome官方團隊進行維護的,用俗話說就是”有娘家的人“,前景更好。
2、運行環境
查看 Puppeteer 的官方 API 你會發現滿屏的 async, await 之類,這些都是 ES7 的規范,所以你需要:
npm install puppeteer --save
3、基本用法
先開看看官方的入門的 DEMO
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(); })();
上面這段代碼就實現了網頁截圖,先大概解讀一下上面幾行代碼:
是不是覺得好簡單? 反正我是覺得比 PhantomJS 簡單,至于跟 selenium-webdriver 比起來, 那更不用說了。下面就介紹一下 puppeteer 的常用的幾個 API。
3.1 puppeteer.launch(options)
使用 puppeteer.launch() 運行 puppeteer,它會 return 一個 promise,使用 then 方法獲取 browser 實例, 當然高版本的 的 nodejs 已經支持 await 特性了,所以上面的例子使用 await 關鍵字,這一點需要特殊說明一下,Puppeteer 幾乎所有的操作都是 異步的, 為了使用大量的 then 使得代碼的可讀性降低,本文所有 demo 代碼都是用 async, await 方式實現。這個 也是 Puppeteer 官方推薦的寫法。對 async/await 一臉懵逼的同學狠狠的戳這里
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 對象,有以下兩種方式:
Puppeteer.launch 和 Puppeteer.connect.
下面這個 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, 戳這里
4、Puppeteer 實戰
了解 API 之后我們就可以來一些實戰了,在此之前,我們先了解一下 Puppeteer 的設計原理,簡單來說 Puppeteer 跟 webdriver 以及 PhantomJS 最大的 的不同就是它是站在用戶瀏覽的角度,而 webdriver 和 PhantomJS 最初設計就是用來做自動化測試的,所以它是站在機器瀏覽的角度來設計的,所以它們 使用的是不同的設計哲學。舉個栗子,加入我需要打開京東的首頁并進行一次產品搜索,分別看看使用 Puppeteer 和 webdriver 的實現流程:
Puppeteer 的實現流程:
webdriver 的實現流程:
個人感覺 Puppeteer 設計哲學更符合任何的操作習慣,更自然一些。
下面我們就用一個簡單的需求實現來進行 Puppeteer 的入門學習。這個簡單的需求就是:
在京東商城抓取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(); });
5、Page.emulate 修改模擬器(客戶端)運行配置
Puppeteer 提供了一些 API 供我們修改瀏覽器終端的配置
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.
6、鍵盤和鼠標
鍵盤和鼠標的API比較簡單,鍵盤的幾個API如下:
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 可配置:
mouse.move(x, y, [options]) 移動鼠標到指定位置, options.steps 表示移動的步長
mouse.up([options]) 觸發 mouseup 事件
7、另外幾個有用的 API
Puppeteer 還提供幾個非常有用的 API, 比如:
7.1 Page.waitFor 系列 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() 可以得到一些頁面性能數據, 捕獲網站的時間線跟蹤,以幫助診斷性能問題。
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.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(); });
以上就是本文的全部內容,希望對大家的學習有所幫助,也希望大家多多支持億速云。
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。