溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點擊 登錄注冊 即表示同意《億速云用戶服務(wù)條款》

詳解Puppeteer 入門教程

發(fā)布時間:2020-09-30 15:54:15 來源:腳本之家 閱讀:203 作者:小一輩無產(chǎn)階級碼農(nóng) 欄目:web開發(fā)

1、Puppeteer 簡介

Puppeteer 是一個node庫,他提供了一組用來操縱Chrome的API, 通俗來說就是一個 headless chrome瀏覽器 (當然你也可以配置成有UI的,默認是沒有的)。既然是瀏覽器,那么我們手工可以在瀏覽器上做的事情 Puppeteer 都能勝任, 另外,Puppeteer 翻譯成中文是”木偶”意思,所以聽名字就知道,操縱起來很方便,你可以很方便的操縱她去實現(xiàn):

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

如果你用過 PhantomJS 的話,你會發(fā)現(xiàn)她們有點類似,但Puppeteer是Chrome官方團隊進行維護的,用俗話說就是”有娘家的人“,前景更好。

2、運行環(huán)境

查看 Puppeteer 的官方 API 你會發(fā)現(xiàn)滿屏的 async, await 之類,這些都是 ES7 的規(guī)范,所以你需要:

  1. Nodejs 的版本不能低于 v7.6.0, 需要支持 async, await.
  2. 需要最新的 chrome driver, 這個你在通過 npm 安裝 Puppeteer 的時候系統(tǒng)會自動下載的
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();
})();

上面這段代碼就實現(xiàn)了網(wǎng)頁截圖,先大概解讀一下上面幾行代碼:

  1. 先通過 puppeteer.launch() 創(chuàng)建一個瀏覽器實例 Browser 對象
  2. 然后通過 Browser 對象創(chuàng)建頁面 Page 對象
  3. 然后 page.goto() 跳轉(zhuǎn)到指定的頁面
  4. 調(diào)用 page.screenshot() 對頁面進行截圖
  5. 關(guān)閉瀏覽器

是不是覺得好簡單? 反正我是覺得比 PhantomJS 簡單,至于跟 selenium-webdriver 比起來, 那更不用說了。下面就介紹一下 puppeteer 的常用的幾個 API。

3.1 puppeteer.launch(options)

使用 puppeteer.launch() 運行 puppeteer,它會 return 一個 promise,使用 then 方法獲取 browser 實例, 當然高版本的 的 nodejs 已經(jīng)支持 await 特性了,所以上面的例子使用 await 關(guān)鍵字,這一點需要特殊說明一下,Puppeteer 幾乎所有的操作都是 異步的, 為了使用大量的 then 使得代碼的可讀性降低,本文所有 demo 代碼都是用 async, await 方式實現(xiàn)。這個 也是 Puppeteer 官方推薦的寫法。對 async/await 一臉懵逼的同學狠狠的戳這里

options 參數(shù)詳解

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

3.2 Browser 對象

當 Puppeteer 連接到一個 Chrome 實例的時候就會創(chuàng)建一個 Browser 對象,有以下兩種方式:

Puppeteer.launch 和 Puppeteer.connect.

下面這個 DEMO 實現(xiàn)斷開連接之后重新連接瀏覽器實例

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 關(guān)閉瀏覽器
browser.disconnect() void 斷開瀏覽器連接
browser.newPage() Promise(Page) 創(chuàng)建一個 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 實戰(zhàn)

了解 API 之后我們就可以來一些實戰(zhàn)了,在此之前,我們先了解一下 Puppeteer 的設(shè)計原理,簡單來說 Puppeteer 跟 webdriver 以及 PhantomJS 最大的 的不同就是它是站在用戶瀏覽的角度,而 webdriver 和 PhantomJS 最初設(shè)計就是用來做自動化測試的,所以它是站在機器瀏覽的角度來設(shè)計的,所以它們 使用的是不同的設(shè)計哲學。舉個栗子,加入我需要打開京東的首頁并進行一次產(chǎn)品搜索,分別看看使用 Puppeteer 和 webdriver 的實現(xiàn)流程:

Puppeteer 的實現(xiàn)流程:

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

webdriver 的實現(xiàn)流程:

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

個人感覺 Puppeteer 設(shè)計哲學更符合任何的操作習慣,更自然一些。

下面我們就用一個簡單的需求實現(xiàn)來進行 Puppeteer 的入門學習。這個簡單的需求就是:

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

首先我們來梳理一下操作流程

  1. 打開京東首頁
  2. 輸入“手機”關(guān)鍵字并搜索
  3. 獲取前10個商品的 A 標簽,并獲取 href 屬性值,獲取商品詳情鏈接
  4. 分別打開10個商品的詳情頁,截取網(wǎng)頁圖片

要實現(xiàn)上面的功能需要用到查找元素,獲取屬性,鍵盤事件等,那接下來我們就一個一個的講解一下。

4.1 獲取元素

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

(1). Page.$(selector) 獲取單個元素,底層是調(diào)用的是 document.querySelector() , 所以選擇器的 selector 格式遵循css 選擇器規(guī)范

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

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

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

最終返回的都是 ElemetHandle 對象

4.2 獲取元素屬性

Puppeteer 獲取元素屬性跟我們平時寫前段的js的邏輯有點不一樣,按照通常的邏輯,應(yīng)該是現(xiàn)獲取元素,然后在獲取元素的屬性。但是上面我們知道 獲取元素的 API 最終返回的都是 ElemetHandle 對象,而你去查看 ElemetHandle 的 API 你會發(fā)現(xiàn),它并沒有獲取元素屬性的 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 執(zhí)行自定義的 JS 腳本

Puppeteer 的 Page 對象提供了一系列 evaluate 方法,你可以通過他們來執(zhí)行一些自定義的 js 代碼,主要提供了下面三個 API

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

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

這個方法很有用,比如我們在獲取頁面的截圖的時候,默認是只截圖當前瀏覽器窗口的尺寸大小,默認值是800x600,那如果我們需要獲取整個網(wǎng)頁的完整 截圖是沒辦法辦到的。Page.screenshot() 方法提供了可以設(shè)置截圖區(qū)域大小的參數(shù),那么我們只要在頁面加載完了之后獲取頁面的寬度和高度就可以解決 這個問題了。

(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 上下文執(zhí)行一個 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 的最終處理結(jié)果返回, 只不過把最后返回的對象封裝成了 JSHandle 對象。本質(zhì)上跟 evaluate 沒有什么區(qū)別。

下面這段代碼實現(xiàn)獲取頁面的動態(tài)(包括js動態(tài)插入的元素) 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), 在文檔頁面載入前調(diào)用 pageFunction, 如果頁面中有 iframe 或者 frame, 則函數(shù)調(diào)用 的上下文環(huán)境將變成子頁面的,即iframe 或者 frame, 由于是在頁面加載前調(diào)用,這個函數(shù)一般是用來初始化 javascript 環(huán)境的,比如重置或者 初始化一些全局變量。

4.4 Page.exposeFunction

除此上面三個 API 之外,還有一類似的非常有用的 API, 那就是 Page.exposeFunction,這個 API 用來在頁面注冊全局函數(shù),非常有用:

因為有時候需要在頁面處理一些操作的時候需要用到一些函數(shù),雖然可以通過 Page.evaluate() API 在頁面定義函數(shù),比如:

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

 return getPageSize();
});

但是這樣的函數(shù)不是全局的,需要在每個 evaluate 中去重新定義,無法做到代碼復用,在一個就是 nodejs 有很多工具包可以很輕松的實現(xiàn)很復雜的功能 比如要實現(xiàn) md5 加密函數(shù),這個用純 js 去實現(xiàn)就不太方便了,而用 nodejs 卻是幾行代碼的事情。

下面代碼實現(xiàn)給 Page 上下文的 window 對象添加 md5 函數(shù):

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 全局函數(shù):

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 供我們修改瀏覽器終端的配置

  1. Page.setViewport() 修改瀏覽器視窗大小
  2. Page.setUserAgent() 設(shè)置瀏覽器的 UserAgent 信息
  3. Page.emulateMedia() 更改頁面的CSS媒體類型,用于進行模擬媒體仿真。 可選值為 “screen”, “print”, “null”, 如果設(shè)置為 null 則表示禁用媒體仿真。
  4. Page.emulate() 模擬設(shè)備,參數(shù)設(shè)備對象,比如 iPhone, Mac, Android 等
page.setViewport({width:1920, height:1080}); //設(shè)置視窗大小為 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'); //設(shè)置打印機媒體樣式

除此之外我們還可以模擬非 PC 機設(shè)備, 比如下面這段代碼模擬 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 支持很多設(shè)備模擬仿真,比如Galaxy, iPhone, IPad 等,想要知道詳細設(shè)備支持,請戳這里 DeviceDescriptors.js.

6、鍵盤和鼠標

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

  1. keyboard.down(key[, options]) 觸發(fā) keydown 事件
  2. keyboard.press(key[, options]) 按下某個鍵,key 表示鍵的名稱,比如 ‘ArrowLeft' 向左鍵,詳細的鍵名映射請戳這里
  3. keyboard.sendCharacter(char) 輸入一個字符
  4. keyboard.type(text, options) 輸入一個字符串
  5. keyboard.up(key) 觸發(fā) 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]) 觸發(fā) mousedown 事件,options 可配置:

  1. options.button 按下了哪個鍵,可選值為[left, right, middle], 默認是 left, 表示鼠標左鍵
  2. options.clickCount 按下的次數(shù),單擊,雙擊或者其他次數(shù)
  3. delay 按鍵延時時間

mouse.move(x, y, [options]) 移動鼠標到指定位置, options.steps 表示移動的步長

mouse.up([options]) 觸發(fā) mouseup 事件

7、另外幾個有用的 API

Puppeteer 還提供幾個非常有用的 API, 比如:

7.1 Page.waitFor 系列 API

  1. page.waitFor(selectorOrFunctionOrTimeout[, options[, …args]]) 下面三個的綜合 API
  2. page.waitForFunction(pageFunction[, options[, …args]]) 等待 pageFunction 執(zhí)行完成之后
  3. page.waitForNavigation(options) 等待頁面基本元素加載完之后,比如同步的 HTML, CSS, JS 等代碼
  4. 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
 }
 });
});

其實上面的代碼就可以解決我們最上面的需求,抓取京東的產(chǎn)品,因為是異步加載的,所以使用這種方式。

7.2 page.getMetrics()

通過 page.getMetrics() 可以得到一些頁面性能數(shù)據(jù), 捕獲網(wǎng)站的時間線跟蹤,以幫助診斷性能問題。

  1. Timestamp 度量標準采樣的時間戳
  2. Documents 頁面文檔數(shù)
  3. Frames 頁面 frame 數(shù)
  4. JSEventListeners 頁面內(nèi)事件監(jiān)聽器數(shù)
  5. Nodes 頁面 DOM 節(jié)點數(shù)
  6. LayoutCount 頁面布局總數(shù)
  7. RecalcStyleCount 樣式重算數(shù)
  8. LayoutDuration 所有頁面布局的合并持續(xù)時間
  9. RecalcStyleDuration 所有頁面樣式重新計算的組合持續(xù)時間。
  10. ScriptDuration 所有腳本執(zhí)行的持續(xù)時間
  11. TaskDuration 所有瀏覽器任務(wù)時長
  12. JSHeapUsedSize JavaScript 占用堆大小
  13. JSHeapTotalSize JavaScript 堆總量

8、總結(jié)和源碼

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

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

下面貼上我們開始的需求實現(xiàn)源碼,僅供參考:

//延時函數(shù)
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一直沒有解決,發(fā)現(xiàn)截圖的高度最大只能是16384px, 超出部分被截掉了。
 await page.screenshot({path:filename, fullPage:true});
 page.close();
 }

 browser.close();
});

以上就是本文的全部內(nèi)容,希望對大家的學習有所幫助,也希望大家多多支持億速云。

向AI問一下細節(jié)

免責聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點不代表本網(wǎng)站立場,如果涉及侵權(quán)請聯(lián)系站長郵箱:is@yisu.com進行舉報,并提供相關(guān)證據(jù),一經(jīng)查實,將立刻刪除涉嫌侵權(quán)內(nèi)容。

AI