Puppeteer: 更友好的 Headless Chrome Node API
很早很早之前,前端就有了对 headless 浏览器的需求,最多的应用场景有两个
- UI 自动化测试:摆脱手工浏览点击页面确认功能模式
- 爬虫:解决页面内容异步加载等问题
也就有了很多杰出的实现,前端经常使用的莫过于 PhantomJS 和 selenium-webdriver,但两个库有一个共性——难用!环境安装复杂,API 调用不友好,1027 年 Chrome 团队连续放了两个大招 Headless Chrome 和对应的 NodeJS API Puppeteer,直接让 PhantomJS 和 Selenium IDE for Firefox 作者悬宣布没必要继续维护其产品
Puppeteer
如同其 github 项目介绍:Puppeteer 是一个通过 DevTools Protocol 控制 headless chrome 的 high-level Node 库,也可以通过设置使用 非 headless Chrome
我们手工可以在浏览器上做的事情 Puppeteer 都能胜任
- 生成网页截图或者 PDF
- 爬取大量异步渲染内容的网页,基本就是人肉爬虫
- 模拟键盘输入、表单自动提交、UI 自动化测试
官方提供了一个 playground,可以快速体验一下。关于其具体使用不在赘述,官网的 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();
})();
实现网页截图就这么简单,自己也实现了一个简单的爬取百度图片的搜索结果的 demo,代码不过 40 行,用过 selenium-webdriver 的同学看了会流泪,接下来介绍几个好玩的特性
哲学
虽然 Puppeteer API 足够简单,但如果是从 webdriver 流转过来的同学会很不适应,主要是在 webdirver 中我们操作网页更多的是从程序的视角,而在 Puppeteer 中网页浏览者的视角。举个简单的例子,我们希望对一个表单的 input 做输入
webdriver 流程
- 通过选择器找到页面 input 元素
- 给元素设置值
const input = await driver.findElement(By.id('kw'));
await input.sendKeys('test');
Puppeteer 流程
- 光标应该 focus 到元素上
- 键盘点击输入
await page.focus('#kw');
await page.keyboard.sendCharacter('test');
在使用中可以多感受一下区别,会发现 Puppeteer 的使用会自然很多
async/await
看官方的例子就可以看出来,几乎所有的操作都是异步的,如果坚持使用回调或者 Promise.then 写出来的代码会非常丑陋且难读,Puppeteer 官方推荐的也是使用高版本 Node 用 async/await 语法
const puppeteer = require('puppeteer');
(async () => {
const browser = await puppeteer.launch();
const page = await browser.newPage();
await page.goto('https://news.ycombinator.com', {waitUntil: 'networkidle'});
await page.pdf({path: 'hn.pdf', format: 'A4'});
await browser.close();
})();
查找元素
这是 UI 自动化测试最常用的功能了,Puppeteer 的处理也相当简单
- page.$(selector)
- page.$$(selector)
这两个函数分别会在页面内执行 document.querySelector 和 document.querySelectorAll,但返回值却不是 DOM 对象,如同 jQuery 的选择器,返回的是经过自己包装的 Promise<ElementHandle>,ElementHandle 帮我们封装了常用的 click 、boundingBox 等方法
获取 DOM 属性
我们写爬虫爬取页面图片列表,感觉可以通过 page.$$(selector) 获取到页面的元素列表,然后再去转成 DOM 对象,获取 src,然后并不行,想做对获取元素对应 DOM 属性的获取,需要用专门的 API
- page.$eval(selector, pageFunction[, ...args])
- page.$$eval(selector, pageFunction[, ...args])
大概用法
const searchValue = await page.$eval('#search', el => el.value);
const preloadHref = await page.$eval('link[rel=preload]', el => el.href);
const html = await page.$eval('.main-container', e => e.outerHTML);
const divsCounts = await page.$$eval('div', divs => divs.length);
值得注意的是如果 pageFunction 返回的是 Promise,那么 page.$eval 会等待方法 resolve
evaluate
如果我们有一些及其个性的需求,无法通过 page.$() 或者 page.$eval() 实现,可以用大招——evaluate,有几个相关的 API
- page.evaluate(pageFunction, …args)
- page.evaluateHandle(pageFunction, …args):
- page.evaluateOnNewDocument(pageFunction, ...args)
这几个函数非常类似,都是可以在页面环境执行我们舒心的 JavaScript,区别主要在执行环境和返回值上
前两个函数都是在当前页面环境内执行,的主要区别在返回值上,第一个返回一个 Serializable 的 Promise,第二个返回值是前面提到的 ElementHandle 对象父类型 JSHandle 的 Promise
const result = await page.evaluate(() => {
return Promise.resolve(8 * 7);
});
console.log(result); // prints "56"
const aWindowHandle = await page.evaluateHandle(() => Promise.resolve(window));
aWindowHandle; // Handle for the window object. 相当于把返回对象做了一层包裹
page.evaluateOnNewDocument(pageFunction, ...args) 是在 browser 环境中执行,执行时机是文档被创建完成但是 script 没有执行阶段,经常用于修改 JavaScript 环境
注册函数
page.exposeFunction(name, puppeteerFunction) 用于在 window 对象注册一个函数,我们可以添加一个 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));
// 注册 window.readfile
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();
});
修改终端
Puppeteer 提供了几个有用的方法让我们可以修改设备信息
- page.setViewport(viewport)
- page.setUserAgent(userAgent)
await page.setViewport({
width: 1920,
height: 1080
});
await page.setUserAgent('Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/61.0.3163.100 Safari/537.36');
page.emulateMedia(mediaType):可以用来修改页面访问的媒体类型,但仅仅支持
- screen
- null:禁用 media emulation
page.emulate(options):前面介绍的几个函数相当于这个函数的快捷方式,这个函数可以设置多个内容
- viewport
- width
- height
- deviceScaleFactor
- isMobile
- hasTouch
- isLandscape
- userAgent
puppeteer/DeviceDescriptors 还给我们提供了几个大礼包
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();
});
键盘
- keyboard.down
- keyboard.up
- keyboard.press
- keyboard.type
- keyboard.sendCharacter
// 直接输入、按键
page.keyboard.type('Hello World!');
page.keyboard.press('ArrowLeft');
// 按住不放
page.keyboard.down('Shift');
for (let i = 0; i < ' World'.length; i++)
page.keyboard.press('ArrowLeft');
page.keyboard.up('Shift');
page.keyboard.press('Backspace');
page.keyboard.sendCharacter('嗨');
鼠标 & 屏幕
- mouse.click(x, y, [options]): options 可以设置
- button
- clickCount
- mouse.move(x, y, [options]): options 可以设置
- steps
- mouse.down([options])
- mouse.up([options])
- touchscreen.tap(x, y)
页面跳转控制
这几个 API 比较简单,不在展开介绍
- page.goto(url, options)
- page.goback(options)
- page.goForward(options)
事件
Puppeteer 提供了对一些页面常见事件的监听,用法和 jQuery 很类似,常用的有
- console:调用 console API
- dialog:页面出现弹窗
- error:页面 crash
- load
- pageerror:页面内未捕获错误
page.on('load', async () => {
console.log('page loading done, start fetch...');
const srcs = await page.$$eval((img) => img.src);
console.log(`get ${srcs.length} images, start download`);
srcs.forEach(async (src) => {
// sleep
await page.waitFor(200);
await srcToImg(src, mn);
});
await browser.close();
});
性能
通过 page.getMetrics() 可以得到一些页面性能数据
TimestampThe timestamp when the metrics sample was taken.Documents页面文档数Frames页面 frame 数JSEventListeners页面内事件监听器数Nodes页面 DOM 节点数LayoutCount页面 layout 数RecalcStyleCount样式重算数LayoutDuration页面 layout 时间RecalcStyleDuration样式重算时长ScriptDurationscript 时间TaskDuration所有浏览器任务时长JSHeapUsedSizeJavaScript 占用堆大小JSHeapTotalSizeJavaScript 堆总量
{
Timestamp: 382305.912236,
Documents: 5,
Frames: 3,
JSEventListeners: 129,
Nodes: 8810,
LayoutCount: 38,
RecalcStyleCount: 56,
LayoutDuration: 0.596341000346001,
RecalcStyleDuration: 0.180430999898817,
ScriptDuration: 1.24401400075294,
TaskDuration: 2.21657899935963,
JSHeapUsedSize: 15430816,
JSHeapTotalSize: 23449600
}
最后
本文知识介绍了部分常用的 API,全部的 API 可以在 github 上查看,由于 Puppeteer 还没有发布正式版,API 迭代比较迅速,在使用中遇到问题也可以在 issue 中反馈。
在 0.11 版本中只有 page.$eval 并没有 page.$$eval,使用的时候只能通过 page.evaluate,通过大家的反馈,在 0.12 中已经添加了该功能,总体而言 Puppeteer 还是一个十分值得期待的 Node headless API
参考
Getting Started with Headless Chrome
Getting started with Puppeteer and Chrome Headless for Web Scraping
Puppeteer: 更友好的 Headless Chrome Node API的更多相关文章
- PuppeteerSharp: 更友好的 Headless Chrome C# API
前端就有了对 headless 浏览器的需求,最多的应用场景有两个 UI 自动化测试:摆脱手工浏览点击页面确认功能模式 爬虫:解决页面内容异步加载等问题 也就有了很多杰出的实现,前端经常使用的莫过于 ...
- Headless Chrome Node API
puppeteer Headless Chrome Node API https://github.com/GoogleChrome/puppeteer https://pptr.dev/ PWA h ...
- puppeteer,新款headless chrome!
puppeteer puppeteer是一种谷歌开发的Headless Chrome,因为puppeteer的出现,业内许多自动化测试库停止维护,比如PhantomJS,Selenium IDE fo ...
- puppeteer,新款headless chrome
puppeteer puppeteer是一种谷歌开发的Headless Chrome,因为puppeteer的出现,业内许多自动化测试库停止维护,比如PhantomJS,Selenium IDE fo ...
- Headless Chrome:服务端渲染JS站点的一个方案【上篇】【翻译】
原文链接:https://developers.google.com/web/tools/puppeteer/articles/ssr 注:由于英文水平有限,没有逐字翻译,可以选择直接阅读原文 tip ...
- Headless Chrome入门
原文地址:Getting Started with Headless Chrome By EricBidelman Engineer @ Google working on web tooling ...
- Headless Chrome:服务端渲染JS站点的一个方案【中篇】【翻译】
接上篇 防止重新渲染 其实说不对客户端代码做任何修改是忽悠人的.在我们的Express 应用中,通过Puppteer加载页面,提供给客户端响应,但是这个过程是有一些问题的. js脚本在服务端的Head ...
- Serverless 实战——使用 Rendertron 搭建 Headless Chrome 渲染解决方案
为什么需要 Rendertron? 传统的 Web 页面,通常是服务端渲染的,而随着 SPA(Single-Page Application) 尤其是 React.Vue.Angular 为代表的前端 ...
- Java 实现 HttpClients+jsoup,Jsoup,htmlunit,Headless Chrome 爬虫抓取数据
最近整理一下手头上搞过的一些爬虫,有HttpClients+jsoup,Jsoup,htmlunit,HeadlessChrome 一,HttpClients+jsoup,这是第一代比较low,很快就 ...
随机推荐
- java - day004 - 数组排序,插入,冒泡
// 判断string 是否相等不能用 == 使用 equal 方法 Arrays.sort(数组); 数组排序算法 对基本类型. 优化的快速排序算法 对引用类型, 优化的合并排序算法
- windows安装多个python及pip版本
windows安装多个python及pip版本 1.下载所需要的python2和python3安装包 2.一路next 3.设置环境变量 4.修改python安装目录下的可执行程序名称 5.在cmd中 ...
- HashMap 学习 (JDK8)
1.hashmap中hash函数的实现中,异或运算操作的结果是什么,为什么要做这样的异或运算 static final int hash(Object key) { int h; return (ke ...
- Redis数据结构之HperLogLog
一.HyperLogLog HyperLogLog是用来做基数统计的. 其可以非常省内存的去统计各种计数,比如注册ip数.每日访问IP数.页面实时UV(PV肯定字符串就搞定了).在线用户数等在对准确性 ...
- Selenium Webdriver点击事件失效问题
最近在用selenium webdriver时,遇到一个棘手的问题,点击事件有时候会失效,具体原因我也不知道是什么. 但是有两个解决办法,第一,在点击该标签前,先点击它的父标签,也就是点击两次. dr ...
- HDU 2243考研路茫茫——单词情结 (AC自动机+矩阵快速幂)
背单词,始终是复习英语的重要环节.在荒废了3年大学生涯后,Lele也终于要开始背单词了. 一天,Lele在某本单词书上看到了一个根据词根来背单词的方法.比如"ab",放在单词前一般 ...
- OO Unit 1 表达式求导
OO Unit 1 表达式求导 面向对象学习小结 前言 本博主要内容目录: 基于度量来分析⾃己的程序结构 缺点反思 重构想法 关于BUG 自己程序出现过的BUG 分析⾃己发现别人程序bug所采⽤的策略 ...
- Fiddler和app抓包
1:请在“运行”,即下面这个地方输入certmgr.msc并回车,打开证书管理. 打开后,请点击操作--查找证书,如下所示: 然后输入“fiddler”查找所有相关证书,如下所示: 可以看到,我们找到 ...
- 开始食用grpc(之一)
开始食用grpc(之一) 转载请注明出处:https://www.cnblogs.com/funnyzpc/p/9501353.html ``` 记一次和一锅们压马路,路过一咖啡厅(某巴克),随口 ...
- Handler Timer TimerTask ScheduledExecutor 循环任务解析
使用Handler执行循环任务 private Handler handler = new Handler(); private int mDelayTime = 1000; private Runn ...