Puppeteer初探

木偶也是有心的~😜

背景

目前负责的几十多个页面管理系统平台,产品发版迭代频繁,除了本业务线会修改逻辑代码,其他业务线也时有涉及(通用逻辑),受测试资源所限,不是每次改动都能做到全面回归验证,时常会在线上暴露出一些问题。

需求

期望有个自动化测试工具,能在发版进行主页面回归验证。因管理平台用户量级较小,主要兼容Chrome浏览器,前端页面使用Nodejs开发,顾经过选型,选择了Puppeteer,Puppeteer是Chrome官方出品,有后台背景,使用的也是JS脚本,在解决问题的同时,也可以提高对前端代码的认知水平。一举两得,何乐而不为之。😆

Puppeteer是什么?

Puppeteer,“木偶”,顾名思义,可以用它来操纵网页。是Chrome推出headless(无界面)模式之后,由官方出品的通过DevTools协议控制headless Chrome的Node库,可以通过Puppeteer的提供的API直接控制Chrome模拟大部分用户操作来进行自动化测试或者作为爬虫访问页面来收集数据。最大特点:操作Dom可以完全在内存中进行模拟既在V8引擎中处理而不用打开浏览器。

Headless Browser

Headless Browser(无头浏览器,在Chrome59中发布)是浏览器的无界面状态,可以在不打开浏览器GUI的情况下,使用浏览器支持的性能。
Chrome Headless相比于其他的浏览器,可以更便捷的运行web自动化,编写爬虫、截图等。通常是由编程或者命令行来控制的。可以加快UI自动化测试的执行时间,对于UI自动化测试,少了真实浏览器加载css,js以及渲染页面的工作。无头测试要比真实浏览器快的多。可以在无界面的服务器或CI上运行测试,减少了外界的干扰,使自动化测试更稳定。
终端命令使用(Mac使用,需要指定别名)

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

启动官网

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

1. --headless 无浏览器模式
2. --disable-gpu 禁用GPU加速
3. --remote-debugging-port=8080 指定端口
4. https://maimai.cn 指定网址
浏览器中输入http://127.0.0.1:8080,可以打开官网

打印DOM

chrome --headless --disable-gpu --dump-dom https://maimai.cn

DOM结构

<html>
  <head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
    <meta name="description" content="8000万人都在用的职场社交平台,基于“实名职业认证”和“人脉网络引擎”帮助职场人拓展人脉、交流合作、求职招聘,收获更多机遇。">
    <meta name="keywords" content="脉脉,脉脉官网,脉脉APP,maimai,招聘,找人,合作,商务,升职,加薪,找工作,职言,社交,交友">
    <meta name="baidu-site-verification" content="tTbhbyypfH">
    <meta name="renderer" content="webkit">
    <title>脉脉-成就职业梦想</title>
    <link rel="stylesheet" type="text/css" href="/static/styles/website/pc/index.css?v=24">
  </head>
</html>

创建PDF文件

chrome --headless --disable-gpu --print-to-pdf https://maimai.cn

截屏

chrome --headless --disable-gpu --window-size=1280,1696 --screenshot https://maimai.cn

1. --window-size=1280,1696 设置尺寸

Puppeteer能做什么?

- 生成网页截图或者 PDF
- 高级爬虫,可以爬取网页
- 模拟键盘输入、表单自动提交、登录网页等,实现 UI 自动化测试
- 捕获站点的时间线,以便追踪你的网站,帮助分析网站性能问题

安装

Puppeteer依赖Node,为了异步超级好用的async/await(ES6的规范),推荐使用7.6版本以上的Node。

#npm install puppeteer or #yarn add puppeteer

如何创建实例

创建demo.js文件

const puppeteer = require('puppeteer');

(async () => {
  const browser = await puppeteer.launch();
  const page = await browser.newPage();
  await page.goto('https://maimai.cn');
  await page.screenshot({path: 'maimai.png'});
  await page.pdf({path: 'maimai.pdf', format: 'A4'});
  await browser.close();
})();

1. puppeteer.launch() 创建一个浏览器实例 Browser 对象
2. Browser 对象创建页面 Page 对象
3. page.goto() 跳转到指定的页面
4. page.screenshot() 对页面进行截图
5. page.pdf() 创建pdf文件
6. browser.close()关闭浏览器

执行:

node demo.js
调试技巧

无界模式

const browser = await puppeteer.launch({headless: false});  //关闭无界模式,默认开启

指定浏览器路径,默认自带的chromium

const browser = await puppeteer.launch({executablePath: '/path/to/Chrome'}); //指定chrome,默认自带

减慢操作速度(毫秒)

const browser = await puppeteer.launch({
  headless: false,
  slowMo: 250 // 延迟250ms
});

开启devtools

const browser = await puppeteer.launch({devtools: true});
Puppeteer API 分层结构

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

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 标签创建产生的
Worker: 具有单一执行上下文,以便于和 WebWorkers 交互

常用API

页面导航

page.goto:打开新页面
page.goBack :回退到上一个页面
page.goForward :前进到下一个页面
page.reload :重新加载页面
page.waitForNavigation:等待页面跳转
基本上所有的操作都是异步的,以上几个 API 都涉及到关于打开一个页面,什么情况下才能判断这个函数执行完毕呢,这些函数都提供了两个参数 waitUtil 和 timeout,waitUtil 表示直到什么出现就算执行完毕,timeout 表示如果超过这个时间还没有结束就抛出异常。

await page.goto('https://www.baidu.com', {
   timeout: 30 * 1000,
   waitUntil: [
       'load',              //等待 “load” 事件触发
       'domcontentloaded',  //等待 “domcontentloaded” 事件触发
       'networkidle0',      //在 500ms 内没有任何网络连接
       'networkidle2'       //在 500ms 内网络连接个数不超过 2 个
   ]
});

等待元素、请求、响应

page.waitForXPath:等待 xPath 对应的元素出现,返回对应的实例
page.waitForSelector :等待选择器对应的元素出现,返回对应的实例
page.waitForResponse :等待某个响应结束,返回 Response 实例
page.waitForRequest:等待某个请求出现,返回 Request 实例

await page.waitForXPath('//img');
await page.waitForSelector('#uniqueId');
await page.waitForResponse('https://d.youdata.netease.com/api/dash/hello');
await page.waitForRequest('https://d.youdata.netease.com/api/dash/hello');

自定义等待

page.waitForFunction:等待在页面中自定义函数的执行结果,返回实例
page.waitFor:设置等待时间,实在没办法的做法

获取元素

page.$('#uniqueId'):获取某个选择器对应的第一个元素
page.$$('div'):获取某个选择器对应的所有元素
page.$x('//img'):获取某个 xPath 对应的所有元素
page.waitForXPath('//img'):等待某个 xPath 对应的元素出现
page.waitForSelector('#uniqueId'):等待某个选择器对应的元素出现

ElementHandle

elementHandle.click():点击某个元素
elementHandle.tap():模拟手指触摸点击
elementHandle.focus():聚焦到某个元素
elementHandle.hover():鼠标 hover 到某个元素上
elementHandle.type('hello'):在输入框输入文本

好几个栗子~~ 🌰

login

(async () => {
    const browser = await puppeteer.launch({
        slowMo: 100,    //放慢速度
        headless: false,
        defaultViewport: {width: 1440, height: 780},
        ignoreHTTPSErrors: false, //忽略 https 报错
        args: ['--start-fullscreen'] //全屏打开页面
    });
    const page = await browser.newPage();
    await page.goto('https://maimai.cn/login/');
    await page.type('input[class=loginPhoneInput]', '136xxxxxxxx',{delay: 20}); //输入手机号
    await page.type('input[id=login_pw]', '123456', {delay: 20});//输入密码
    await page.tap('input[class=loginBtn]');//点击登录
    page.waitForNavigation() 
    console.log('登录成功');
    await page.waitFor(5*1000);
    await page.close();
    await browser.close();
})();

spider

(async () => {
    const browser = await puppeteer.launch({headless:false});
    const page = await browser.newPage();
    await page.goto('https://maimai.cn/',{waitUntil:'networkidle2'});//500毫秒内网络连接数不超过2个
    await page.waitFor('body > div > div.website-navbar > div > div.website-navbar__links > div.website-links');
    const result = await page.evaluate(() => {
        let data = []; // 初始化空数组来存储数据
        let elements = document.querySelectorAll('body > div > div.website-navbar > div > div.website-navbar__links > div.website-links > a'); // 获取selector下的所有<a>标签元素
        for (var element of elements){
            let title = element.innerText; // 获取标题
            let url = element.href;//获取网址
            data.push({title,url}); // 存入数组
        }
        return data;
    });
    console.log(result);
    await page.waitFor(3000);
    await browser.close();
})();

trace
在 DevTools 的 Performance 可以上传对应的 json 文件并查看分析结果

(async () => {
    const browser = await puppeteer.launch();
    const page = await browser.newPage();
    await page.tracing.start({path: './trace.json'});
    await page.goto('https://maimai.cn');
    await page.tracing.stop();
    browser.close();
})();
参考文献

DOM及页面渲染
更多api
中文文档
Puppeteer Github
结合项目来谈谈puppeteer

TBD............
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,839评论 6 482
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,543评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 153,116评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,371评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,384评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,111评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,416评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,053评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,558评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,007评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,117评论 1 334
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,756评论 4 324
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,324评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,315评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,539评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,578评论 2 355
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,877评论 2 345