Puppeteer 是 Chrome 开发团队在 2017 年发布的一个 Node.js 包,用来模拟 Chrome 浏览器的运行。我们团队从 Puppeteer 刚发布出来就开始成为忠实用户了(主要是因为 PhantomJs 坑太多了),本文主要在介绍 Puppeteer 的同时,结合我们平时的实践做一个分享。 学习 Puppeteer 之前我们先来了解一下 Chrome DevTool Protocol 什么是 Chrome DevTool Protocol- CDP 基于 WebSocket,利用 WebSocket 实现与浏览器内核的快速数据通道
- CDP 分为多个域(DOM,Debugger,Network,Profiler,Console...),每个域中都定义了相关的命令和事件(Commands and Events)
- 我们可以基于 CDP 封装一些工具对 Chrome 浏览器进行调试及分析,比如我们常用的 “Chrome 开发者工具” 就是基于 CDP 实现的
- 如果你以 remote-debugging-port 参数启动 Chrome,那么就可以看到所有 Tab 页面的开发者调试前端页面,还会在同一端口上还提供了 http 服务,主要提供以下几个接口:
GET /json/version
GET /json or /json/list
GET /json/protocol
GET /json/new?{url}
GET /json/activate/{targetId}
GET /json/close/{targetId}
GET /devtools/inspector.html
WebSocket /devtools/page/{targetId}
- 很多有用的工具都是基于 CDP 实现的,比如 Chrome 开发者工具,chrome-remote-interface,Puppeteer 等
什么是 Headless Chrome- 在无界面的环境中运行 Chrome
- 通过命令行或者程序语言操作 Chrome
- 无需人的干预,运行更稳定
- 在启动 Chrome 时添加参数 --headless,便可以 headless 模式启动 Chrome
alias chrome="/Applications/Google\ Chrome.app/Contents/MacOS/Google\ Chrome"
chrome --headless --remote-debugging-port=9222 --disable-gpu
chrome --headless --disable-gpu --dump-dom https://www.baidu.com
chrome --headless --disable-gpu --screenshot https://www.baidu.com
- chrome 启动时可以加一些什么参数,大家可以点击这里查看
Puppeteer 是什么- Puppeteer 是 Node.js 工具引擎
- Puppeteer 提供了一系列 API,通过 Chrome DevTools Protocol 协议控制 Chromium/Chrome 浏览器的行为
- Puppeteer 默认情况下是以 headless 启动 Chrome 的,也可以通过参数控制启动有界面的 Chrome
- Puppeteer 默认绑定最新的 Chromium 版本,也可以自己设置不同版本的绑定
- Puppeteer 让我们不需要了解太多的底层 CDP 协议实现与浏览器的通信
Puppeteer 能做什么官方称:“Most things that you can do manually in the browser can be done using Puppeteer”,那么具体可以做些什么呢? - 网页截图或者生成 PDF
- 爬取 SPA 或 SSR 网站
- UI 自动化测试,模拟表单提交,键盘输入,点击等行为
- 捕获网站的时间线,帮助诊断性能问题
- 创建一个最新的自动化测试环境,使用最新的 js 和最新的 Chrome 浏览器运行测试用例
- 测试 Chrome 扩展程序
- ...
Puppeteer API 分层结构Puppeteer 中的 API 分层结构基本和浏览器保持一致,下面对常使用到的几个类介绍一下: - Browser: 对应一个浏览器实例,一个 Browser 可以包含多个 BrowserContext
- BrowserContext: 对应浏览器一个上下文会话,就像我们打开一个普通的 Chrome 之后又打开一个隐身模式的浏览器一样,BrowserContext 具有独立的 Session(cookie 和 cache 独立不共享),一个 BrowserContext 可以包含多个 Page
- Page:表示一个 Tab 页面,通过 browserContext.newPage()/browser.newPage() 创建,browser.newPage() 创建页面时会使用默认的 BrowserContext,一个 Page 可以包含多个 Frame
- Frame: 一个框架,每个页面有一个主框架(page.MainFrame()),也可以多个子框架,主要由 iframe 标签创建产生的
- ExecutionContext: 是 javascript 的执行环境,每一个 Frame 都一个默认的 javascript 执行环境
- ElementHandle: 对应 DOM 的一个元素节点,通过该该实例可以实现对元素的点击,填写表单等行为,我们可以通过选择器,xPath 等来获取对应的元素
- JsHandle:对应 DOM 中的 javascript 对象,ElementHandle 继承于 JsHandle,由于我们无法直接操作 DOM 中对象,所以封装成 JsHandle 来实现相关功能
- CDPSession:可以直接与原生的 CDP 进行通信,通过 session.send 函数直接发消息,通过 session.on 接收消息,可以实现 Puppeteer API 中没有涉及的功能
- Coverage:获取 JavaScript 和 CSS 代码覆盖率
- Tracing:抓取性能数据进行分析
- Response: 页面收到的响应
- Request: 页面发出的请求
如何创建一个 Browser 实例puppeteer 提供了两种方法用于创建一个 Browser 实例: - puppeteer.connect: 连接一个已经存在的 Chrome 实例
- puppeteer.launch: 每次都启动一个 Chrome 实例
const puppeteer = require('puppeteer');
let request = require('request-promise-native');
(async () => {
const browser = await puppeteer.launch({
headless: false,
slowMo: 100,
args: [
'–no-sandbox',
'--window-size=1280,960'
],
});
const page = await browser.newPage();
await page.goto('https://www.baidu.com');
await page.close();
await browser.close();
})();
(async () => {
let version = await request({
uri: "http://127.0.0.1:9222/json/version",
json: true
});
let browser = await puppeteer.connect({
browserWSEndpoint: version.webSocketDebuggerUrl
});
const page = await browser.newPage();
await page.goto('https://www.baidu.com');
await page.close();
await browser.disconnect();
})();
这两种方式的对比: - puppeteer.launch 每次都要重新启动一个 Chrome 进程,启动平均耗时 100 到 150 ms,性能欠佳
- puppeteer.connect 可以实现对于同一个 Chrome 实例的共用,减少启动关闭浏览器的时间消耗
- puppeteer.launch 启动时参数可以动态修改
- 通过 puppeteer.connect 我们可以远程连接一个 Chrome 实例,部署在不同的机器上
- puppeteer.connect 多个页面共用一个 chrome 实例,偶尔会出现 Page Crash 现象,需要进行并发控制,并定时重启 Chrome 实例
如何等待加载?在实践中我们经常会遇到如何判断一个页面加载完成了,什么时机去截图,什么时机去点击某个按钮等问题,那我们到底如何去等待加载呢? 下面我们把等待加载的 API 分为三类进行介绍: 加载导航页面- page.goto:打开新页面
- page.goBack :回退到上一个页面
- page.goForward :前进到下一个页面
- page.reload :重新加载页面
- page.waitForNavigation:等待页面跳转
Pupeeteer 中的基本上所有的操作都是异步的,以上几个 API 都涉及到关于打开一个页面,什么情况下才能判断这个函数执行完毕呢,这些函数都提供了两个参数 waitUtil 和 timeout,waitUtil 表示直到什么出现就算执行完毕,timeout 表示如果超过这个时间还没有结束就抛出异常。 await page.goto('https://www.baidu.com', {
timeout: 30 * 1000,
waitUntil: [
'load',
'domcontentloaded',
'networkidle0',
'networkidle2'
]
});
以上 waitUtil 有四个事件,业务可以根据需求来设置其中一个或者多个触发才以为结束,networkidle0 和 networkidle2 中的 500ms 对时间性能要求高的用户来说,还是有点长的 等待元素、请求、响应- page.waitForXPath:等待 xPath 对应的元素出现,返回对应的 ElementHandle 实例
- page.waitForSelector :等待选择器对应的元素出现,返回对应的 ElementHandle 实例
- page.waitForResponse :等待某个响应结束,返回 Response 实例
- page.waitForRequest:等待某个请求出现,返回 Request 实例
await page.waitForXPath('
await page.waitForSelector('#uniqueId');
await page.waitForResponse('https:
await page.waitForRequest('https:
自定义等待如果上面提供的等待方式都不能满足我们的需求,puppeteer 还提供我们提供两个函数: - page.waitForFunction:等待在页面中自定义函数的执行结果,返回 JsHandle 实例
- page.waitFor:设置等待时间,实在没办法的做法
await page.goto(url, {
timeout: 120000,
waitUntil: 'networkidle2'
});
let renderdoneHandle = await page.waitForFunction('window.renderdone', {
polling: 120
});
const renderdone = await renderdoneHandle.jsonValue();
if (typeof renderdone === 'object') {
console.log(`加载页面失败:报表${renderdone.componentId}出错 -- ${renderdone.message}`);
}else{
console.log('页面加载成功');
}
两个独立的环境在使用 Puppeteer 时我们几乎一定会遇到在这两个环境之间交换数据:运行 Puppeteer 的 Node.js 环境和 Puppeteer 操作的页面 Page DOM,理解这两个环境很重要 - 首先 Puppeteer 提供了很多有用的函数去 Page DOM Environment 中执行代码,这个后面会介绍到
- 其次 Puppeteer 提供了 ElementHandle 和 JsHandle 将 Page DOM Environment 中元素和对象封装成对应的 Node.js 对象,这样可以直接这些对象的封装函数进行操作 Page DOM
10 个用例告诉你如何使用 puppeteer下面介绍 10 个关于使用 Puppeteer 的用例,并在介绍用例的时候会穿插的讲解一些 API,告诉大家如何使用 Puppeteer: Case1:截图我们使用 Puppeteer 既可以对某个页面进行截图,也可以对页面中的某个元素进行截图: (async () => {
const browser = await puppeteer.launch();
const page = await browser.newPage();
await page.setViewport({width: 1920, height: 800});
await page.goto('https://youdata.163.com');
await page.screenshot({
path: './files/capture.png',
type: 'png',
fullPage: true
});
let [element] = await page.$x('/html/body/section[4]/div/div[2]');
await element.screenshot({
path: './files/element.png'
});
await page.close();
await browser.close();
})();
我们怎么去获取页面中的某个元素呢? - page.$('#uniqueId'):获取某个选择器对应的第一个元素
- page.$$('div'):获取某个选择器对应的所有元素
- page.$x('//img'):获取某个 xPath 对应的所有元素
- page.waitForXPath('//img'):等待某个 xPath 对应的元素出现
- page.waitForSelector('#uniqueId'):等待某个选择器对应的元素出现
case2: 模拟用户登录(async () => {
const browser = await puppeteer.launch({
slowMo: 100,
headless: false,
defaultViewport: {width: 1440, height: 780},
ignoreHTTPSErrors: false,
args: ['--start-fullscreen']
});
const page = await browser.newPage();
await page.goto('https://demo.youdata.com');
const uniqueIdElement = await page.$('#uniqueId');
本文出处: https://www.toutiao.com/a6891511371465392654/
|