本文首发于 vivo互联网技术 微信公众号 
链接:https://mp.weixin.qq.com/s/P-YdQPOQ9GZgjDEP7VG8ag
作者:Wang Zhenzheng

Puppeteer 是 Chrome开发团队2017年发布的一个 Node.js包,提供了一组用来操纵Chrome的API,通俗来说就是一个Headless Chrome浏览器,这Headless Chrome也可以配置成有UI的 。利用Puppeteer可以做到爬取页面数据,页面截屏或者生成PDF文件,前端自动化测试(模拟输入/点击/键盘行为)以及捕获站点的时间线,分析网站性能问题。

一、起因

虽说Puppeteer是Chrome开发团队2017年发布的一个 Node.js包,但是在团队日常工作中基本没有使用。前段时间在开发一个聊天工具的时候,需要引入emoji表情,但是业务方的需求是要使用Google emoji,那我们就需要在emojipedia上将这些图保存下来。这么多的图如果一张一张保存,那就枉为开发了。首先想到的是调用该页面的api接口,从接口中拿到对应的emoji地址然后遍历到本地文件。

尴尬的是这个页面是直出的,不是通过接口调用,那就需要我们换个思路,我们发现这些emoji的DOM是在一个class为emoji-grid的ul下,那么如果拿到该ul节点下的全部img的url,然后遍历到本地,是不是就做到将emoji表情保存下来。

依据这个思路,我们就想到使用Puppeteer,在介绍Puppeteer之前我们先将这段简单的捕获moji表情的代码放出来。

const puppeteer = require('puppeteer')
const request = require('request')
const fs = require('fs') async function getEmojiImage (url) {
// 返回解析为Promise的浏览器
const browser = await puppeteer.launch()
// 返回新的页面对象
const page = await browser.newPage()
// 页面对象访问对应的url地址
await page.goto(url, {
waitUntil: 'networkidle2'
})
// 等待3000ms,等待浏览器的加载
await page.waitFor(3000)
// 可以在page.evaluate的回调函数中访问浏览器对象,可以进行DOM操作
const emojis = await page.evaluate(() => {
let ol = document.getElementsByClassName('emoji-grid')[0]
let imgs = ol.getElementsByTagName('img')
let url = []
for (let i = 0; i < 97; i++) {
url.push(imgs[i].getAttribute('src'))
}
// 返回所有emoji的url地址数组
return url
})
// 定义一个存在的json
let json = []
for (let i = 0; i < emojis.length; i++) {
const name = emojis[i].slice(emojis[i].lastIndexOf('/') + 1)
// 将emoji写入本地文件中
request(emojis[i]).pipe(fs.createWriteStream('./' + (i < 10 ? '0' + i : i) + name))
json.push({
name,
url: `./a/a/${name}` // 你的url地址
})
console.log(`${name}----emoji写入成功`)
}
// 写入json文件
fs.writeFile('./google-emoji.json', JSON.stringify(json), function () {}) // 关闭无头浏览器
await browser.close()
} getEmojiImage('https://emojipedia.org/google/')

在了解Puppeteer之前,我们先来看下Headless Chrome。

二、Headless Chrome

Headless Chrome在Chrome59中发布,用于在headless环境中运行Chrome浏览器,也就是在非Chrome环境中运行Chrome。它将Chromium和Blink渲染引擎提供的所有现代Web平台功能引入命令行。

headless如何在终端中使用:我们尝试通过终端命令打开vivo 的官网

chrome  --headless --disable-gpu --remote-debugging-port=8080  https://vivo.com.cn

注意:在Mac上使用前,建议先绑定Chrome的别名

alias chrome="/Applications/Google\ Chrome.app/Contents/MacOS/Google\ Chrome"

此时,Headless Chrome已经成功运行了,浏览器输入 http://127.0.0.1:8080,你会看到如下的vivo界面:

除此之外,还可以以命令行的形式去执行以下常见的操作:

1、打印DOM:
chrome --headless --disable-gpu --dump-dom https://vivo.com.cn

2、创建一个PDF文件
chrome --headless --disable-gpu --print-to-pdf https://vivo.com.cn

3、截屏
chrome --headless --disable-gpu --screenshot https://vivo.com.cn
// 设置截屏的尺寸
chrome --headless --disable-gpu --screenshot --window-size=1280,1696 https://vivo.com.cn

那么,Puppeteer是什么?又可以做些什么?Puppeteer是一个node库,提供了一组用来操纵Chrome的API,通俗来说就是一个Headless Chrome浏览器,这Headless Chrome也可以配置成有UI的,默认是没有的。

三、Puppeteer

Puppeteer可以做些什么呢?我们从文章开始的一个demo中可以发现,Puppeteer可以爬取页面数据。除此之外,结合Headless Chrome的一些命令行,Puppeteer可以做到一下几点:

  • 爬取页面数据
  • 页面截屏或者生成PDF文件
  • 前端自动化测试(模拟输入/点击/键盘行为)
  • 捕获站点的时间线,分析网站性能问题

1、初探

这是Puppeteer官方提供的一张API分层结构图

(图片来源于网络

从图上我们可以发现,Puppeteer是通过使用Chrome DevTools Protocol(CDP)协议与浏览器进行通信,而Browser对应一个浏览器实例,可以拥有浏览器上下文,一个Browser可以包含多个BrowserContext。Page表示一个Tab页面,一个BrowserContext可以包含多个Page。每个页面都有一个主的Frame,ExecutionContext是Frame提供的一个JavasSript执行环境。

2、Browser

一切的起源都是从Browser开始的,我们先来梳理下Browser实例以后发生了什么。

首先,通过puppeteer.launch()创建一个Browser实例

const browser = await puppeteer.launch({
// --remote-debugging-port=3333会启一个端口,在浏览器中访问http://127.0.0.1:3333/可以查看
args: ['--remote-debugging-port=3333']
})
console.log(browser.wsEndpoint())

通过打印的browser.wsEndpoint(),我们看到输出一个如下的链接:

ws://127.0.0.1:57546/devtools/browser/5d6ee624-6b5e-4b8c-b284-5e4800eac853

这就是devTool用于连接调试页面的连接了,这个websocket连接遵循CDP协议,我们看下这里面具体有什么。

{"id":46,"method":"CSS.getMatchedStylesForNode","params":{"nodeId":5}}
{"id":47,"method":"CSS.getComputedStyleForNode","params":{"nodeId":5}}

每条信息的格式是有一个递增的id值,然后有method和params参数。这些消息指挥者被调试页面做出各种各样的动作。换而言之,任何一个实现了CDP的程序都可以用来调试页面,chrome 这个协议等于是开放了用程序控制页面动作的接口。比如我们可以这样子模拟一个alert到页面。

{"id":190,"method":"Runtime.compileScript","params":{"expression":"alert()","sourceURL":"","persistScript":false,"executionContextId":3}}

这种直接操作太不友好,而Puppeteer正是实现了遵循CDP的Node顶层API,使我们可以调用简单方便的操作对应的指令。

3、Page

browser.newPage()为Browser中浏览器上下文的方法。我们看下newPage()的代码实现。

/**
* @param {?string} contextId
* @return {!Promise<!Puppeteer.Page>}
*/
async _createPageInContext(contextId) {
const {targetId} = await this._connection.send('Target.createTarget', {url: 'about:blank', browserContextId: contextId || undefined});
const target = await this._targets.get(targetId);
assert(await target._initializedPromise, 'Failed to create target for page');
const page = await target.page();
return page;
}

this._connection.send('Target.createTarget',{})使用CDP中的Target.createTarget创建页面了页面,同样,在我们其他API时也是在使用CDP中的方法,例如page.goto()实际上是执行的是client.send('Page.navigate', {});。而在Page中的一些操作,如点击/模拟输入,则是调用的DomWorld实例,DomWorld通过FrameManager管理,Page对象主要使用三种manager来管理常见操作:

  • FrameManager:页面行为管理。如跳转goto,点击clcik,模拟输入type,等待加载waitFor等
  • NetworkManager:网络行为管理。如设置每个请求忽略缓存setCacheEnabled,请求拦截setRequestInterception等
  • EmulationManager:模拟行为管理。只有一个方法,emulateViewport,模拟设备与视口尺寸

四、应用

除了文章开始的抓取emoji表情外,我们尝试将Puppeteer应用在一个前端自动化测试的场景中,我们在后台管理系统开发测试中,经常会碰到表单的提交,对于表单中不同字段的校验需要模拟不同的场景,人工的点击效率低,而且每次都需要重复表单输入,比较繁琐。

基于该场景,我们使用Puppeteer实现自动填写-保存-打印接口返回数据-截图。

STEP 1

创建一个Browser类的实例,并通过参数设置初始化它(更多设置参数参考官网API)

const browser = await puppeteer.launch({
devtools: true, //是否为每个选项卡自动打开DevTools面板
headless: false, //是否以无头模式运行浏览器。默认是true,除非devtools选项是true
defaultViewport: { width: 1000, height: 1200 }, //为每个页面设置一个默认视口大小
ignoreHTTPSErrors: true //是否在导航期间忽略 HTTPS 错误
})

STEP 2

创建一个 Page 实例,导航到一个url

const page = await browser.newPage()
await page.goto(url, {
waitUntil: 'networkidle0'
})

waitUntil参数是来确定满足什么条件才认为页面跳转完成。包括以下事件:

  • load - 页面的load事件触发时
  • domcontentloaded - 页面的DOMContentLoaded事件触发时
  • networkidle0 - 不再有网络连接时触发(至少500毫秒后)
  • networkidle2 - 只有2个网络连接时触发(至少500毫秒后)

该处用到的是不再有网络连接认为页面跳转完成。值得注意的是,后台管理系统会有token的校验,此处有两种解决方案,一种是等待页面自动跳转到登陆处,模拟登陆操作然后返回;一种是直接在cookie里设置token信息。我们采用第二种,代码如下:

const cookies = [
{
name: 'token',
value: 'system tokens', //你系统自己的token
domain: 'domain' //需要种在哪个domain下
}
]
await page.setCookie(...cookies)

STEP 3

模拟页面输入操作和点击事件,我们代码就只列举两个,不一一展开了。

// 操作input输入 132 ,delay参数表示输入延迟
await page.type('.el-form-item:nth-child(1) input', '132', { delay: 20 })
// 操作点击
await page.click('.el-form-item:nth-child(2) .el-form-item__content label:nth-child(1)')

STEP 4

监测页面是否有API响应,响应后将响应数据打印在控制台。

page.on('response', response => {
const req = response.request()
console.log(`Response的请求地址:${req.url()},请求方式是:${req.method()}, 请求返回的状态${response.status()},`)
let message = response.text()
message.then(function (result) {
console.log(`返回的数据:${result}`)
})
})

STEP 5

将操作后的页面信息截图保存

// 截取url中的路径标示,作为保存图片的命名,防止保存后覆盖
const testName = decodeURIComponent(url.split('#/')[1]).replace(/\//g, '-')
await page.screenshot({
path: `${testName}.png`,
fullPage: true
})

STEP 6

关闭Browser—await browser.close()

至此,我们完成了一个表单的自动化校验和测试。我们看下效果:

1.前端校验通过,请求到服务端接口的数据

2.如果前端校验没通过,直接截图生成

五、拓展

  1. 模拟线上环境点检操作走查
  2. 定时爬去周报日报数据,生成截图发送给相关人员查看

六、参考

  1. https://developers.google.com/web/updates/2017/04/headless-chrome

  2. https://peter.sh/experiments/chromium-command-line-switches/

  3. https://zhaoqize.github.io/puppeteer-api-zh_CN/#/

更多内容敬请关注 vivo 互联网技术 微信公众号

注:转载文章请先与微信号:Labs2020 联系。

Puppeteer 入门与实战的更多相关文章

  1. 赞一个 kindle电子书有最新的计算机图书可买了【Docker技术入门与实战】

    最近对docker这个比较感兴趣,找一个比较完整的书籍看看,在z.cn上找到了电子书,jd dangdang看来要加油啊 Docker技术入门与实战 [Kindle电子书] ~ 杨保华 戴王剑 曹亚仑 ...

  2. docker-9 supervisord 参考docker从入门到实战

    参考docker从入门到实战 使用 Supervisor 来管理进程 Docker 容器在启动的时候开启单个进程,比如,一个 ssh 或者 apache 的 daemon 服务.但我们经常需要在一个机 ...

  3. webpack入门和实战(一):webpack配置及技巧

    一.全面理解webpack 1.什么是 webpack? webpack是近期最火的一款模块加载器兼打包工具,它能把各种资源,例如JS(含JSX).coffee.样式(含less/sass).图片等都 ...

  4. CMake快速入门教程-实战

    http://www.ibm.com/developerworks/cn/linux/l-cn-cmake/ http://blog.csdn.net/dbzhang800/article/detai ...

  5. Sping Boot入门到实战之入门篇(三):Spring Boot属性配置

    该篇为Sping Boot入门到实战系列入门篇的第三篇.介绍Spring Boot的属性配置.   传统的Spring Web应用自定义属性一般是通过添加一个demo.properties配置文件(文 ...

  6. Sping Boot入门到实战之入门篇(二):第一个Spring Boot应用

    该篇为Spring Boot入门到实战系列入门篇的第二篇.介绍创建Spring Boot应用的几种方法. Spring Boot应用可以通过如下三种方法创建: 通过 https://start.spr ...

  7. Sping Boot入门到实战之入门篇(一):Spring Boot简介

    该篇为Spring Boot入门到实战系列入门篇的第一篇.对Spring Boot做一个大致的介绍. 传统的基于Spring的Java Web应用,需要配置web.xml, applicationCo ...

  8. Sping Boot入门到实战之入门篇(四):Spring Boot自动化配置

    该篇为Sping Boot入门到实战系列入门篇的第四篇.介绍Spring Boot自动化配置的基本原理与实现.   Spring Boot之所以受开发者欢迎, 其中最重要的一个因素就是其自动化配置特性 ...

  9. Sping Boot入门到实战之实战篇(一):实现自定义Spring Boot Starter——阿里云消息队列服务Starter

    在 Sping Boot入门到实战之入门篇(四):Spring Boot自动化配置 这篇中,我们知道Spring Boot自动化配置的实现,主要由如下几部分完成: @EnableAutoConfigu ...

  10. BDD敏捷开发入门与实战

    BDD敏捷开发入门与实战 1.BDD的来由 2003年,Dan North首先提出了BDD的概念,并在随后开发出了JBehave框架.在Dan North博客上介绍BDD的文章中,说到了BDD的想法是 ...

随机推荐

  1. 用友U8与MES系统API接口对接案例分析

    企业数字化转型:轻易云数据集成平台助力 U8 ERP+MES 系统集成 为什么选择数字化转型? 领导层对企业资源规划(ERP)的深刻理解促使了数字化转型的启动. 采用精确的"N+5" ...

  2. 介绍一个我开源的项目:一键部署 VictoriaMetrics 群集

    作者:张富春(ahfuzhang),转载时请注明作者和引用链接,谢谢! cnblogs博客 zhihu Github 公众号:一本正经的瞎扯 我实在是非常喜欢这个强大的 metrics 监控组件 Vi ...

  3. [算法考研笔记]mm算法随笔[成绩划分][回溯0-1][得分][字段和][聪明小偷][股票买卖]

    mm算法随笔 学习笔记(回溯算法) 回溯 <---->递归1.递归的下面就是回溯的过程 回溯法是一个 纯暴力的 搜索.有的时候暴力求解都没有办法,用回溯可以解决. 回溯法解决的问题: 组合 ...

  4. 吉特日化MES & HttpClient基础连接已经关闭: 连接被意外关闭

    在吉特日化MES调用某公司AGV平台下发任务的时候,使用HttpClient 进行POST请求,出现如下异常: HttpClient基础连接已经关闭: 连接被意外关闭  , 之前已经使用HTTPCli ...

  5. 吉特日化MES & 标签的设计与选择

    吉特日化MES & 标签的设计与选择:如今条码和二维码在生活中随处可见,作为一种能够快速精准读取识别的手段,条码和二维码在生产车间也应用的较为广泛.如果说条码和二维码哪一种更好,我更加倾向于使 ...

  6. DI入门案例

    1.基于IoC管理bean 2.Service中使用new形式创建的Dao对象是否保留?(不保留) 3.Service中需要的Dao对象如何进入到Service中?(提供方法) 4.Service与D ...

  7. Java在指定路径下执行cmd命令的方法

    目前状态:毕业设计ing 背景: 做毕设时,由于需要将python的运行效果展示出来,所以使用了Java写了一个前端的界面.但是在使用Java对python的脚本进行调用时就尴尬了,出错-- 这里也许 ...

  8. 理解 Paimon changelog producer

    介绍 目的 Chaneglog producer 的主要目的是为了在 Paimon 表上产生流读的 changelog, 所以如果只是批读的表是可以不用设置 Chaneglog producer 的. ...

  9. C++ Qt开发:自定义Dialog对话框组件

    Qt 是一个跨平台C++图形界面开发库,利用Qt可以快速开发跨平台窗体应用程序,在Qt中我们可以通过拖拽的方式将不同组件放到指定的位置,实现图形化开发极大的方便了开发效率,本章将重点介绍自定义Dial ...

  10. python学习笔记:python的字符串拼接效率分析

    问题的起因是因为在做LeetCode5714题的时候,对于字符串拼接使用了 ans = ans+s[i] 提交后超时了,改成 ans+=s[i] 就可以通过了,而且用c++好像也有这个问题,在此记录一 ...