Puppeteer的入門教程和實踐

Puppeteer是谷歌官方出品的一個通過DevTools協(xié)議控制headless Chrome的Node庫抱婉”肷迹可以通過Puppeteer的提供的api直接控制Chrome模擬大部分用戶操作來進行UI Test或者作為爬蟲訪問頁面來收集數(shù)據(jù)。

在Puppeteer出現(xiàn)之前胚想,測試web應用程序通常依靠兩種方式:有頭瀏覽器,和構建在JavaScript引擎之上的無頭瀏覽器。無頭瀏覽器意味著沒有用戶界面暇屋,有頭瀏覽器是最終用戶與產(chǎn)品進行交互的窗口。

這就造成了一種必須權衡的局面:速度(無頭)與可靠性(有頭)洞辣。Puppeter的設計目標是消除這種權衡咐刨,能夠讓開發(fā)人員利用Chromium瀏覽器環(huán)境來運行測試昙衅,并讓他們能夠靈活地利用有頭與無頭瀏覽器來實現(xiàn)用戶場景的測試。

Puppeteer也非常適合開發(fā)人員定鸟,開發(fā)人員可以基于熟悉的測試框架集成Puppeteer來實現(xiàn)而涉、拓展自己的測試能力。

簡單來說使用Puppeteer联予,我們可以實現(xiàn):

  • 快速爬取網(wǎng)頁內(nèi)容
  • 自動完成表單數(shù)據(jù)提交
  • 實現(xiàn)在瀏覽器上的所有自動化操作
  • 跟蹤并記錄頁面加載性能
  • 瀏覽器屏幕截圖
  • 實現(xiàn)自動化測試
  • 從網(wǎng)頁生成PDF

環(huán)境和安裝

Puppeteer因為是一個npm的包啼县,所以安裝很簡單:

npm i puppeteer

或者

yarn add puppeteer

Puppeteer安裝時自帶一個最新版本的Chromium,可以通過設置環(huán)境變量或者npm config中的PUPPETEER_SKIP_CHROMIUM_DOWNLOAD跳過下載沸久。如果不下載的話季眷,啟動時可以通過puppeteer.launch([options])配置項中的executablePath指定Chromium的位置。

使用和例子

## 在Node.js文件中加載Puppeteer
const puppeteer = require('puppeteer');
## 使用launch() 方法用于創(chuàng)建瀏覽器的實例對象:
(async () => {
  const browser = await puppeteer.launch()
})() // 或者promis形式使用 puppeteer.launch().then(async browser => {})
## 調(diào)用browser對象的newPage() 方法返回頁面對象:

(async () => {
  const browser = await puppeteer.launch()
  const page = await browser.newPage()
})()
## 在頁面對象上調(diào)用goto() 方法用以訪問測試頁面:
(async () => {
  const browser = await puppeteer.launch()
  const page = await browser.newPage()
  await page.goto('https://website.com')
})()
## 使用page.$() 等同于使用Selectors API querySelector()獲取頁面元素, page.$$() 等同于querySelectorAll()獲取全部頁面滿足條件對象卷胯。
## 測試結束后可以使用close()方法關閉瀏覽器:
browser.close()

上述代碼通過puppeteer的launch方法生成了一個browser的實例子刮,對應于瀏覽器,launch方法可以傳入配置項窑睁,比較有用的是在本地調(diào)試時傳入{headless:false}可以關閉headless模式挺峡。

const browser = await puppeteer.launch({headless:false})

browser.newPage方法可以打開一個新選項卡并返回選項卡的實例page,通過page上的各種方法可以對頁面進行常用操作担钮。上述代碼就進行了截屏和打印pdf的操作沙郭。

一個很強大的方法是page.evaluate(pageFunction, ...args),可以向頁面注入我們的函數(shù)裳朋,這樣就有了無限可能

const puppeteer = require('puppeteer');

(async () => {
  const browser = await puppeteer.launch();
  const page = await browser.newPage();
  await page.goto('http://rennaiqian.com');

  // Get the "viewport" of the page, as reported by the page.
  const dimensions = await page.evaluate(() => {
    return {
      width: document.documentElement.clientWidth,
      height: document.documentElement.clientHeight,
      deviceScaleFactor: window.devicePixelRatio
    };
  });

  console.log('Dimensions:', dimensions);
  await browser.close();
})();

需要注意的是evaluate方法中是無法直接使用外部的變量的病线,需要作為參數(shù)傳入,想要獲得執(zhí)行的結果也需要return出來鲤嫡。

調(diào)試技巧

  1. 關掉無界面模式送挑,有時查看瀏覽器顯示的內(nèi)容是很有用的。使用以下命令可以啟動完整版瀏覽器:
const browser = await puppeteer.launch({headless: false})
  1. 減慢速度暖眼,slowMo選項以指定的毫秒減慢Puppeteer的操作惕耕。這是另一個看到發(fā)生了什么的方法:
const browser = await puppeteer.launch({
  headless:false,
  slowMo:250
});

3.捕獲console的輸出,通過監(jiān)聽console事件。在page.evaluate里調(diào)試代碼時這也很方便:

page.on('console', msg => console.log('PAGE LOG:', ...msg.args));
await page.evaluate(() => console.log(`url is ${location.href}`));

4.啟動詳細日志記錄诫肠,所有公共API調(diào)用和內(nèi)部協(xié)議流量都將通過puppeteer命名空間下的debug模塊進行記錄

# Basic verbose logging
 env DEBUG="puppeteer:*" node script.js

 # Debug output can be enabled/disabled by namespace
 env DEBUG="puppeteer:*,-puppeteer:protocol" node script.js # everything BUT protocol messages
 env DEBUG="puppeteer:session" node script.js # protocol session messages (protocol messages to targets)
 env DEBUG="puppeteer:mouse,puppeteer:keyboard" node script.js # only Mouse and Keyboard API calls

 # Protocol traffic can be rather noisy. This example filters out all Network domain messages
 env DEBUG="puppeteer:*" env DEBUG_COLORS=true node script.js 2>&1 | grep -v '"Network'

爬蟲實踐

很多網(wǎng)頁通過user-agent來判斷設備司澎,可以通過page.emulate(options)來進行模擬。options有兩個配置項栋豫,一個為userAgent挤安,另一個為viewport可以設置寬度(width)、高度(height)丧鸯、屏幕縮放(deviceScaleFactor)蛤铜、是否是移動端(isMobile)、有無touch事件(hasTouch)。

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.example.com');
  // other actions...
  await browser.close();
});

上述代碼則模擬了iPhone6訪問某網(wǎng)站围肥,其中devices是puppeteer內(nèi)置的一些常見設備的模擬參數(shù)剿干。

很多網(wǎng)頁需要登錄,有兩種解決方案:

  1. 讓puppeteer去輸入賬號密碼

常用方法:點擊可以使用page.click(selector[, options])方法穆刻,也可以選擇聚焦page.focus(selector)置尔。
輸入可以使用page.type(selector, text[, options])輸入指定的字符串,還可以在options中設置delay緩慢輸入更像真人一些氢伟。也可以使用keyboard.down(key[, options])來一個字符一個字符的輸入榜轿。

  1. 如果是通過cookie判斷登錄狀態(tài)的可以通過page.setCookie(...cookies),想要維持cookie可以定時訪問腐芍。
Tip:有些網(wǎng)站需要掃碼差导,但是相同域名的其他網(wǎng)頁卻有登錄试躏,就可以嘗試去可以登錄的網(wǎng)頁登錄完利用cookie訪問跳過掃碼猪勇。

簡單例子

const puppeteer = require('puppeteer');

(async () => {
  const browser = await puppeteer.launch({headless: false});
  const page = await browser.newPage();
  await page.goto('https://baidu.com');
  await page.type('#kw', 'puppeteer', {delay: 100});
  page.click('#su')
  await page.waitFor(1000);
  const targetLink = await page.evaluate(() => {
    return [...document.querySelectorAll('.result a')].filter(item => {
      return item.innerText && item.innerText.includes('Puppeteer的入門和實踐')
    }).toString()
  });
  await page.goto(targetLink);
  await page.waitFor(1000);
  browser.close();
})()

Page對象內(nèi)置函數(shù)

page.$() 提供對頁面上的選擇器API方法querySelector()的訪問權限

page.$$() 提供對頁面上的選擇器API方法querySelectorAll()的訪問權限

page.$eval() 接收2個或多個參數(shù)。第一個是選擇器颠蕴,第二個是函數(shù)泣刹。如果有更多參數(shù),則這些參數(shù)將作為附加參數(shù)傳遞給函數(shù)犀被。

const innerTextOfButton = await page.$eval('button#submit', el => el.innerText)

click() 對頁面元素進行點擊事件模擬

await page.click('button#submit')

content() 獲取HTML的頁面源代碼

emulate() 模擬設備信息椅您,可以通過配置Agent和viewport 模擬不同設備訪問網(wǎng)站。

const puppeteer = require('puppeteer');
const device = require('puppeteer/DeviceDescriptors')['iPhone X'];

puppeteer.launch().then(async browser => {
  const page = await browser.newPage()
  await page.emulate(device)

  //testing

  await browser.close()
})

evaluate() 在頁上下文中執(zhí)行用戶指定的函數(shù)寡键。在這個函數(shù)中掀泳,可以直接訪問document對象,因此可以調(diào)用任何DOM API:

const puppeteer = require('puppeteer');

(async () => {
  const browser = await puppeteer.launch()
  const page = await browser.newPage()
  await page.goto('https://website.com')

  const result = await page.evaluate(() => {
    return document.querySelectorAll('.footer-tags a').length
  })

  console.log(result)
})()

ocus() 選中當前元素
goBack() 后退頁面
goForward() 前進頁面
goto() 打開某一網(wǎng)址
hover() 懸停在某元素上
pdf() 生成當前頁面的PDF文檔
reload() 重新加載當前頁面
screenshot() 截圖當前頁面并存儲為PNG圖片
setViewPort() 默認的viewport 為800x600px西轩, 可以通過setViewport 來模擬不同窗口尺寸

waitFor() 等待某些條件成立员舵,內(nèi)置以下等待條件:

  • waitForFunction
  • waitForNavigation
  • waitForRequest
  • waitForResponse
  • waitForSelector
  • waitForXPath
await page.waitFor(waitForNameToBeFilled)
const waitForNameToBeFilled = () => page.$('input#name').value != ''
?著作權歸作者所有,轉載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市藕畔,隨后出現(xiàn)的幾起案子马僻,更是在濱河造成了極大的恐慌,老刑警劉巖注服,帶你破解...
    沈念sama閱讀 206,602評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件韭邓,死亡現(xiàn)場離奇詭異,居然都是意外死亡溶弟,警方通過查閱死者的電腦和手機女淑,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,442評論 2 382
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來辜御,“玉大人诗力,你說我怎么就攤上這事。” “怎么了苇本?”我有些...
    開封第一講書人閱讀 152,878評論 0 344
  • 文/不壞的土叔 我叫張陵袜茧,是天一觀的道長。 經(jīng)常有香客問我瓣窄,道長笛厦,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 55,306評論 1 279
  • 正文 為了忘掉前任俺夕,我火速辦了婚禮裳凸,結果婚禮上,老公的妹妹穿的比我還像新娘劝贸。我一直安慰自己姨谷,他們只是感情好,可當我...
    茶點故事閱讀 64,330評論 5 373
  • 文/花漫 我一把揭開白布映九。 她就那樣靜靜地躺著梦湘,像睡著了一般。 火紅的嫁衣襯著肌膚如雪件甥。 梳的紋絲不亂的頭發(fā)上捌议,一...
    開封第一講書人閱讀 49,071評論 1 285
  • 那天,我揣著相機與錄音引有,去河邊找鬼瓣颅。 笑死,一個胖子當著我的面吹牛譬正,可吹牛的內(nèi)容都是我干的宫补。 我是一名探鬼主播,決...
    沈念sama閱讀 38,382評論 3 400
  • 文/蒼蘭香墨 我猛地睜開眼曾我,長吁一口氣:“原來是場噩夢啊……” “哼粉怕!你這毒婦竟也來了?” 一聲冷哼從身側響起您单,我...
    開封第一講書人閱讀 37,006評論 0 259
  • 序言:老撾萬榮一對情侶失蹤斋荞,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后虐秦,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體平酿,經(jīng)...
    沈念sama閱讀 43,512評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 35,965評論 2 325
  • 正文 我和宋清朗相戀三年悦陋,在試婚紗的時候發(fā)現(xiàn)自己被綠了蜈彼。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 38,094評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡俺驶,死狀恐怖幸逆,靈堂內(nèi)的尸體忽然破棺而出棍辕,到底是詐尸還是另有隱情,我是刑警寧澤还绘,帶...
    沈念sama閱讀 33,732評論 4 323
  • 正文 年R本政府宣布楚昭,位于F島的核電站,受9級特大地震影響拍顷,放射性物質(zhì)發(fā)生泄漏抚太。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 39,283評論 3 307
  • 文/蒙蒙 一昔案、第九天 我趴在偏房一處隱蔽的房頂上張望尿贫。 院中可真熱鬧,春花似錦踏揣、人聲如沸庆亡。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,286評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽又谋。三九已至,卻和暖如春括享,著一層夾襖步出監(jiān)牢的瞬間搂根,已是汗流浹背珍促。 一陣腳步聲響...
    開封第一講書人閱讀 31,512評論 1 262
  • 我被黑心中介騙來泰國打工铃辖, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人猪叙。 一個月前我還...
    沈念sama閱讀 45,536評論 2 354
  • 正文 我出身青樓娇斩,卻偏偏與公主長得像,于是被迫代替她去往敵國和親穴翩。 傳聞我的和親對象是個殘疾皇子犬第,可洞房花燭夜當晚...
    茶點故事閱讀 42,828評論 2 345

推薦閱讀更多精彩內(nèi)容