很早很早以前,前端就有了对后端环境调用浏览器页面功能的需求,最多的应用场景有两个javascript
市场上出现过不少优秀的解决方案,在 Puppeteer 出现以前最经常使用的是 PhantomJS 和 selenium-webdriver,但两个库有个共同特色——环境安装复杂,API 调用不语义化。2017 年 Chrome 团队连续放了两个大招 Headless Chrome 和对应的 NodeJS API Puppeteer,直接让 PhantomJS 和 Selenium IDE for Firefox 做者宣布不必继续维护其产品html
如同其 github 项目介绍:Puppeteer 是一个经过 DevTools Protocol 控制 headless chrome 的 high-level Node 库,提供了高度封装、使用方便的 API 来模拟用户在页面的操做、对浏览器事件作出响应等前端
手动能够在浏览器上作的大部分行为 Puppeteer 都能经过 API 真实模拟,API 使用很是简单,看下官网对网页截图的示例java
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();
})();
复制代码
实现网页截图就这么简单,用过 selenium-webdriver 的同窗看了会流泪,官方提供了一个 playground,能够快速体验一下git
虽然 Puppeteer API 足够简单,但若是是从 webdriver 流转过来的同窗会很不适应,主要是在 webdirver 中操做网页更多的是从程序的视角,而在 Puppeteer 中网页浏览者的视角。举个简单的例子,对一个表单的 input 作输入github
使用 webdriver 流程web
const input = await driver.findElement(By.id('kw'));
await input.sendKeys('test');
复制代码
使用 Puppeteer 流程chrome
await page.focus('#kw');
await page.keyboard.type('test');
复制代码
甚至能够简化为一条语句:向 input 输入字符npm
await page.type('#kw', 'test');
复制代码
能够看到 Puppeteer 的使用流程几乎是在模拟人的操做,在使用过程当中能够感觉区别,会发现 Puppeteer 的使用天然不少后端
npm i puppeteer
复制代码
比起 PhantomJS 和 selenium-webdriver 实在简单了太多,安装 Puppeteer 时会下载最新版本的 Chromium,从 1.7 开始 Puppeteer 每次发布还会有一个 puppeteer-core 发布,相对于 puppeteer 有两个区别
PUPPETEER_* env
变量如同 Node.js 启动能够设置环境变量,puppeteer 也支持特定的环境变量
HTTP_PROXY
,HTTPS_PROXY
,NO_PROXY
- 定义用于下载和运行 Chromium 的 HTTP 代理设置。PUPPETEER_SKIP_CHROMIUM_DOWNLOAD
- 请勿在安装步骤中下载绑定的 Chromium。PUPPETEER_DOWNLOAD_HOST
- 覆盖用于下载 Chromium 的 URL 的主机部分。PUPPETEER_CHROMIUM_REVISION
- 在安装步骤中指定一个你喜欢 puppeteer 使用的特定版本的 Chromium。PUPPETEER_EXECUTABLE_PATH
- 指定一个 Chrome 或者 Chromium 的可执行路径,会被用于puppeteer.launch
。具体关于可执行路径参数的意义,可参考puppeteer.launch([options])
。
Puppeteer API 设计和浏览器层次相对应(浅色框体内容目前不在 Puppeteer 中实现)
Puppeteer
使用 DevTools 协议 与浏览器进行通讯Browser
实例能够拥有浏览器上下文BrowserContext
实例定义了一个浏览会话并可拥有多个页面Page
至少有一个框架:主框架。 可能还有其余框架由 iframe 或 框架标签 建立frame
至少有一个执行上下文 - 默认的执行上下文 - 框架的 JavaScript 被执行。 一个框架可能有额外的与 扩展 关联的执行上下文Worker
具备单一执行上下文,而且便于与 WebWorkers 进行交互API 很是丰富,看几个经常使用的功能
这是 UI 自动化测试最经常使用的功能了,Puppeteer 的处理也至关简单——使用选择器
这两个函数分别会在页面内执行 document.querySelector
和 document.querySelectorAll
,但返回值却不是 DOM 对象,如同 jQuery 的选择器,返回的是通过本身包装的 Promise,ElementHandle 封装了经常使用的 click 、boundingBox 等方法
const puppeteer = require('puppeteer');
puppeteer.launch().then(async browser => {
const page = await browser.newPage();
await page.goto('https://google.com');
const inputElement = await page.$('input[type=submit]');
await inputElement.click();
// ...
});
复制代码
经过 ElementHandle 并不能直接获取对应 DOM 元素的属性,须要使用专门的 API 操做
pageFunction 的代码会在浏览器实例中执行,因此能够用 Window 等 dom 对象;其返回值是整个方法的返回值
const searchValue = await page.$eval('#search', el => el.value);
const html = await page.$eval('.main-container', e => e.outerHTML);
const divsCounts = await page.$eval('div', divs => divs.length);
复制代码
page.evaluate(pageFunction [, ...args]) 是上述方法的抽象,能够在浏览器示例中执行任意方法
const result = await page.evaluate(x => {
return Promise.resolve(8 * x);
}, 7); // 7 会作为实参传入 pageFunction
console.log(result); // 输出 "56"
复制代码
前面提到的 ElementHandle 实例 能够做为参数传给 page.evaluate
const bodyHandle = await page.$('body');
const html = await page.evaluate(body => body.innerHTML, bodyHandle);
复制代码
Puppeteer 经过 page.keyboard
对象暴露操做键盘的接口
await page.keyboard.type('Hello World!', {delay: 100});
await page.keyboard.press('ArrowLeft');
await page.keyboard.down('Shift');
for (let i = 0; i < ' World'.length; i++)
await page.keyboard.press('ArrowLeft');
await page.keyboard.up('Shift');
await page.keyboard.press('Backspace');
// 结果字符串最终为 'Hello!'
复制代码
方法看名字就知道什么意思,type 和 sendCharacter 做用很是相似,区别是
keypress
和 input
事件,不会触发 keydown
或 keyup
事件keydown
, keypress
/input
和 keyup
事件特殊键名参考:github.com/puppeteer/p…
Puppeteer 经过 page.mouse
对象暴露操做键盘的接口
// 使用 ‘page.mouse’ 追踪 100x100 的矩形。
await page.mouse.move(0, 0);
await page.mouse.down();
await page.mouse.move(0, 100);
await page.mouse.move(100, 100);
await page.mouse.move(100, 0);
await page.mouse.move(0, 0);
await page.mouse.up();
复制代码
手机页面常用 tap 事件,用 page.mouse.click() 是不能触发的,须要使用专门的 tap API
几个页面跳转的 API 很是简单,options 的 waitUntil
参数用来指定知足什么条件认为页面跳转完成,若是值为事件数组,那么全部事件触发后才认为是跳转完成。事件包括:
load
- 页面的load事件触发时(默认值)domcontentloaded
- 页面的 DOMContentLoaded 事件触发时networkidle0
- 再也不有网络链接时触发(至少500毫秒后)networkidle2
- 只有2个网络链接时触发(至少500毫秒后)Puppeteer 提供了对一些页面常见事件的监听,用法和 jQuery 很相似
Puppeteer 提供了几个有用的方法用来修改设备信息
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)
:能够用来修改页面访问的媒体类型,但仅仅支持
page.emulate(options)
:前面介绍的几个函数至关于这个函数的快捷方式,这个函数能够设置多个内容
viewport
width
height
deviceScaleFactor
isMobile
hasTouch
isLandscape
userAgent
由于使用太频繁,Puppeteer 经过 puppeteer/DeviceDescriptors
提供了全套的设备模拟
const puppeteer = require('puppeteer');
const devices = require('puppeteer/DeviceDescriptors');
const iPhone = devices['iPhone XR'];
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();
});
复制代码
全部支持参考:github.com/puppeteer/p…
经过 page.getMetrics() 能够获得一些页面性能数据
{
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
}
复制代码
page.exposeFunction(name, puppeteerFunction)
用于在 window 对象注册一个函数,在自动化测试初始化测试环境时候颇有用,举个例子:给 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 默认运行 Chromium 的 headless mode。若是想要使用彻底版本的 Chromium 设置 'headless' option 便可。
const browser = await puppeteer.launch({headless: false});
复制代码
默认状况下,Puppeteer 下载并使用特定版本的 Chromium 以及其 API 保证开箱即用。 若是要将 Puppeteer 与不一样版本的 Chrome 或 Chromium 一块儿使用,在建立Browser
实例时传入 Chromium 可执行文件的路径便可:
const browser = await puppeteer.launch({executablePath: '/path/to/Chrome'});
复制代码
const path = require('path');
const puppeteer = require('puppeteer');
const iPhoneXR = puppeteer.devices['iPhone XR'];
(async () => {
const browser = await puppeteer.launch();
const page = await browser.newPage();
await page.emulate(iPhoneXR);
await page.goto('https://www.baidu.com', { waitUntil: ['load'] });
await page.screenshot({
path: path.join(__dirname, '../image', 'baidu.png'),
fullPage: true,
});
await browser.close();
})();
复制代码
const path = require('path');
const fs = require('fs');
const http = require('http');
const https = require('https');
const puppeteer = require('puppeteer');
const ora = require('ora');
// const devices = require('puppeteer/DeviceDescriptors');
const iPhoneXR = puppeteer.devices['iPhone XR'];
(async () => {
const browser = await puppeteer.launch();
const page = await browser.newPage();
await page.emulate(iPhoneXR);
await page.goto('https://iamge.baidu.com', { waitUntil: ['load'] });
await page.type('#image-search-input', 'dog');
await page.tap('#image-search-btn');
page.on('load', async () => {
const srcs = await page.$eval(
'.sfc-image-content-waterfall img',
images => images.map(img => img.src)
);
await browser.close();
let i = 0;
srcs.forEach(src => {
const request = src.trim().startsWith('https') ? https : http;
const dest = path.join(__dirname, `../images/${i++}.jpg`);
console.log(`正在下载 ${src}`);
request.get(src, res => {
res.pipe(fs.createWriteStream(dest));
});
});
});
})();
复制代码