puppeteer前端利器

Puppeteer 是 Chrome 开发团队在 2017 年发布的一个 Node.js 包,同时还有 Headless Chrome。用来模拟 Chrome 浏览器的运行。它提供了高级API来经过 DevTools 协议控制无头 Chrome 或 Chromium ,它也能够配置为使用完整(非无头)Chrome 或 Chromium。javascript

image

学习 Puppeteer 以前咱们先来了解一下 Chrome DevTool Protocol 和 Headless Chrome。java

什么是 Chrome DevTool Protocol

  • CDP 基于 WebSocket,利用 WebSocket 实现与浏览器内核的快速数据通道。
  • CDP 分为多个域(DOM,Debugger,Network,Profiler,Console...),每一个域中都定义了相关的命令和事件(Commands and Events)。
  • 咱们能够基于 CDP 封装一些工具对 Chrome 浏览器进行调试及分析,好比咱们经常使用的 “Chrome 开发者工具” 就是基于 CDP 实现的。
  • 不少有用的工具都是基于 CDP 实现的,好比 Chrome 开发者工具chrome-remote-interfacePuppeteer 等。

什么是 Headless Chrome

  • 能够在无界面的环境中运行 Chrome。
  • 经过命令行或者程序语言操做 Chrome。
  • 无需人的干预,运行更稳定。
  • 在启动 Chrome 时添加参数 --headless,即可以 headless 模式启动 Chrome。
  • chrome 启动时能够加一些什么参数,你们能够点击这里查看。

总而言之 Headless Chrome 就是 Chrome 浏览器的无界面形态,能够在不打开浏览器的前提下,使用全部 Chrome 支持的特性运行你的程序。node

Puppeteer 是什么

  • Puppeteer 是 Node.js 工具引擎。
  • Puppeteer 提供了一系列 API,经过 Chrome DevTools Protocol 协议控制 Chromium/Chrome 浏览器的行为。
  • Puppeteer 默认状况下是以 headless 启动 Chrome 的,也能够经过参数控制启动有界面的 Chrome。
  • Puppeteer 默认绑定最新的 Chromium 版本,也能够本身设置不一样版本的绑定。
  • Puppeteer 让咱们不须要了解太多的底层 CDP 协议实现与浏览器的通讯。

Puppeteer 能作什么

官方介绍:您能够在浏览器中手动执行的大多数操做均可以使用 Puppeteer 完成!示例:git

  • 生成页面的屏幕截图和PDF。
  • 爬取 SPA 或 SSR 网站。
  • 自动化表单提交,UI测试,键盘输入等。
  • 建立最新的自动化测试环境。使用最新的JavaScript和浏览器功能,直接在最新版本的Chrome中运行测试。
  • 捕获站点的时间线跟踪,以帮助诊断性能问题。
  • 测试Chrome扩展程序。
  • ...

Puppeteer API 分层结构

Puppeteer 中的 API 分层结构基本和浏览器保持一致,下面对常使用到的几个类介绍一下:es6

image

  • 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: 页面发出的请求

Puppeteer 安装与环境

注意:在v1.18.1以前,Puppeteer至少须要Node v6.4.0。从v1.18.1到v2.1.0的版本依赖于Node 8.9.0+。从v3.0.0开始,Puppeteer开始依赖于Node 10.18.1+。若要使用 async / await,只有Node v7.6.0或更高版本才支持。

Puppeteer是一个node.js包,因此安装很简单:github

npm install puppeteer
// 或者
yarn add puppeteer
npm 在安装 puppeteer 的时候可能会报错!这是因为外网致使,使用淘宝镜像 cnpm 安装可解决。

安装Puppeteer时,它将下载 Chromium 的最新版本。从1.7.0版开始,官方发布了该 puppeteer-core 软件包,默认状况下不会下载任何浏览器,用于启动现有的浏览器或链接到远程浏览器。须要注意安装的 puppeteer-core 版本与打算链接的浏览器兼容。web

Puppeteer 使用

Case1: 截图

咱们使用 Puppeteer 既能够对某个页面进行截图,也能够对页面中的某个元素进行截图:chrome

const puppeteer = require('puppeteer');

(async () => {
  const browser = await puppeteer.launch();
  const page = await browser.newPage();
  //设置可视区域大小,默认的页面大小为800x600分辨率
  await page.setViewport({width: 1920, height: 800});
  await page.goto('https://www.baidu.com/');
  //对整个页面截图
  await page.screenshot({
      path: './files/baidu_home.png',  //图片保存路径
      type: 'png',
      fullPage: true //边滚动边截图
      // clip: {x: 0, y: 0, width: 1920, height: 800}
  });
  //对页面某个元素截图
  let element = await page.$('#s_lg_img');
  await element.screenshot({
      path: './files/baidu_logo.png'
  });
  await page.close();
  await browser.close();
})();

咱们怎么去获取页面中的某个元素呢?docker

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

Case2: 模拟用户操做

const puppeteer = require('puppeteer');

(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://www.baidu.com/');
    //输入文本
    const inputElement = await page.$('#kw');
    await inputElement.type('hello word', {delay: 20});
    //点击搜索按钮
    let okButtonElement = await page.$('#su');
    //等待页面跳转完成,通常点击某个按钮须要跳转时,都须要等待 page.waitForNavigation() 执行完毕才表示跳转成功
    await Promise.all([
        okButtonElement.click(),
        page.waitForNavigation()  
    ]);
    await page.close();
    await browser.close();
})();

那么 ElementHandle 都提供了哪些操做元素的函数呢?npm

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

Case3: 植入 javascript 代码

Puppeteer 最强大的功能是,你能够在浏览器里执行任何你想要运行的 javascript 代码。下面代码是对百度首页新闻推荐爬取数据的例子。

const puppeteer = require('puppeteer');

(async () => {
    const browser = await puppeteer.launch();
    const page = await browser.newPage();
    await page.goto('https://www.baidu.com/');
    //经过 page.evaluate 在浏览器里执行代码
    const resultData = await page.evaluate(async () =>  {
      let data = {};
      const ListEle = [...document.querySelectorAll('#hotsearch-content-wrapper .hotsearch-item')];
      data = ListEle.map((ele) => {
        const urlEle = ele.querySelector('a.c-link');
        const titleEle = ele.querySelector('.title-content-title');
        return {
          href: urlEle.href,
          title: titleEle.innerText,
        };
      });
      return data;
    });
    console.log(resultData)
    await page.close();
    await browser.close();
})();

有哪些函数能够在浏览器环境中执行代码呢?

  • page.evaluate(pageFunction[, ...args]):在浏览器环境中执行函数
  • page.evaluateHandle(pageFunction[, ...args]):在浏览器环境中执行函数,返回 JsHandle 对象
  • page.$$eval(selector, pageFunction[, ...args]):把 selector 对应的全部元素传入到函数并在浏览器环境执行
  • page.$eval(selector, pageFunction[, ...args]):把 selector 对应的第一个元素传入到函数在浏览器环境执行
  • page.evaluateOnNewDocument(pageFunction[, ...args]):建立一个新的 Document 时在浏览器环境中执行,会在页面全部脚本执行以前执行
  • page.exposeFunction(name, puppeteerFunction):在 window 对象上注册一个函数,这个函数在 Node 环境中执行,有机会在浏览器环境中调用 Node.js 相关函数库

Case4: 请求拦截

请求在有些场景下颇有必要,拦截一下不必的请求提升性能,咱们能够在监听 Page 的 request 事件,并进行请求拦截,前提是要开启请求拦截 page.setRequestInterception(true)

const puppeteer = require('puppeteer');

(async () => {
    const browser = await puppeteer.launch();
    const page = await browser.newPage();
    const blockTypes = new Set(['image', 'media', 'font']);
    await page.setRequestInterception(true); //开启请求拦截
    page.on('request', request => {
        const type = request.resourceType();
        const shouldBlock = blockTypes.has(type);
        if(shouldBlock){
            //直接阻止请求
            return request.abort();
        }else{
            //对请求重写
            return request.continue({
                //能够对 url,method,postData,headers 进行覆盖
                headers: Object.assign({}, request.headers(), {
                    'puppeteer-test': 'true'
                })
            });
        }
    });
    await page.goto('https://www.baidu.com/');
    await page.close();
    await browser.close();
})();

那 page 页面上都提供了哪些事件呢?

  • page.on('close') 页面关闭
  • page.on('console') console API 被调用
  • page.on('error') 页面出错
  • page.on('load') 页面加载完
  • page.on('request') 收到请求
  • page.on('requestfailed') 请求失败
  • page.on('requestfinished') 请求成功
  • page.on('response') 收到响应
  • page.on('workercreated') 建立 webWorker
  • page.on('workerdestroyed') 销毁 webWorker

Case5: 获取 WebSocket 响应

Puppeteer 目前没有提供原生的用于处理 WebSocket 的 API 接口,可是咱们能够经过更底层的 Chrome DevTool Protocol (CDP) 协议得到

const puppeteer = require('puppeteer');

(async () => {
    const browser = await puppeteer.launch();
    const page = await browser.newPage();
    //建立 CDP 会话
    let cdpSession = await page.target().createCDPSession();
    //开启网络调试,监听 Chrome DevTools Protocol 中 Network 相关事件
    await cdpSession.send('Network.enable');
    //监听 webSocketFrameReceived 事件,获取对应的数据
    cdpSession.on('Network.webSocketFrameReceived', frame => {
        let payloadData = frame.response.payloadData;
        if(payloadData.includes('push:query')){
            //解析payloadData,拿到服务端推送的数据
            let res = JSON.parse(payloadData.match(/\{.*\}/)[0]);
            if(res.code !== 200){
                console.log(`调用websocket接口出错:code=${res.code},message=${res.message}`);
            }else{
                console.log('获取到websocket接口数据:', res.result);
            }
        }
    });
    await page.goto('https://netease.youdata.163.com/dash/142161/reportExport?pid=700209493');
    await page.waitForFunction('window.renderdone', {polling: 20});
    await page.close();
    await browser.close();
})();

Case6: 如何抓取 iframe 中的元素

一个 Frame 包含了一个执行上下文(Execution Context),咱们不能跨 Frame 执行函数,一个页面中能够有多个 Frame,主要是经过 iframe 标签嵌入的生成的。其中在页面上的大部分函数实际上是 page.mainFrame().xx 的一个简写,Frame 是树状结构,咱们能够经过 frame.childFrames() 遍历到全部的 Frame,若是想在其它 Frame 中执行函数必须获取到对应的 Frame 才能进行相应的处理

如下是在登陆 188 邮箱时,其登陆窗口实际上是嵌入的一个 iframe,如下代码时咱们在获取 iframe 并进行登陆

const puppeteer = require('puppeteer');

(async () => {
    const browser = await puppeteer.launch({headless: false, slowMo: 50});
    const page = await browser.newPage();
    await page.goto('https://www.188.com');
    
    for (const frame of page.mainFrame().childFrames()){
        //根据 url 找到登陆页面对应的 iframe
        if (frame.url().includes('passport.188.com')){
            await frame.type('.dlemail', 'admin@admin.com');
            await frame.type('.dlpwd', '123456');
            await Promise.all([
                frame.click('#dologin'),
                page.waitForNavigation()
            ]);
            break;
        }
    }
    await page.close();
    await browser.close();
})();

Case7: 页面性能分析

Puppeteer 提供了对页面性能分析的工具,目前功能仍是比较弱的,只能获取到一个页面性能执行的数据,如何分析须要咱们本身根据数据进行分析,听说在 2.0 版本会作大的改版: - 一个浏览器同一时间只能 trace 一次 - 在 devTools 的 Performance 能够上传对应的 json 文件并查看分析结果 - 咱们能够写脚原本解析 trace.json 中的数据作自动化分析 - 经过 tracing 咱们获取页面加载速度以及脚本的执行性能

const puppeteer = require('puppeteer');

(async () => {
    const browser = await puppeteer.launch();
    const page = await browser.newPage();
    await page.tracing.start({path: './files/trace.json'});
    await page.goto('https://www.google.com');
    await page.tracing.stop();
    /*
        continue analysis from 'trace.json'
    */
    browser.close();
})();

Case8: 文件的上传和下载

在自动化测试中,常常会遇到对于文件的上传和下载的需求,那么在 Puppeteer 中如何实现呢?

const puppeteer = require('puppeteer');

(async () => {
    const browser = await puppeteer.launch();
    const page = await browser.newPage();
    //经过 CDP 会话设置下载路径
    const cdp = await page.target().createCDPSession();
    await cdp.send('Page.setDownloadBehavior', {
        behavior: 'allow', //容许全部下载请求
        downloadPath: 'path/to/download'  //设置下载路径
    });
    //点击按钮触发下载
    await (await page.waitForSelector('#someButton')).click();
    //等待文件出现,轮训判断文件是否出现
    await waitForFile('path/to/download/filename');

    //上传时对应的 inputElement 必须是<input>元素
    let inputElement = await page.waitForXPath('//input[@type="file"]');
    await inputElement.uploadFile('/path/to/file');
    browser.close();
})();

Case9: 跳转新 tab 页处理

在点击一个按钮跳转到新的 Tab 页时会新开一个页面,这个时候咱们如何获取改页面对应的 Page 实例呢?能够经过监听 Browser 上的 targetcreated 事件来实现,表示有新的页面建立:

let page = await browser.newPage();
await page.goto(url);
let btn = await page.waitForSelector('#btn');
//在点击按钮以前,事先定义一个 Promise,用于返回新 tab 的 Page 对象
const newPagePromise = new Promise(res => 
  browser.once('targetcreated', 
    target => res(target.page())
  )
);
await btn.click();
//点击按钮后,等待新tab对象
let newPage = await newPagePromise;

Case10: 模拟不一样的设备

Puppeteer 提供了模拟不一样设备的功能,其中 puppeteer.devices 对象上定义不少设备的配置信息,这些配置信息主要包含 viewport 和 userAgent,而后经过函数 page.emulate 实现不一样设备的模拟

const puppeteer = require('puppeteer');
const iPhone = puppeteer.devices['iPhone 6'];
puppeteer.launch().then(async browser => {
  const page = await browser.newPage();
  await page.emulate(iPhone);
  await page.goto('https://www.baidu.com');
  await browser.close();
});

性能和优化

  • 关于共享内存:
Chrome 默认使用 /dev/shm 共享内存,可是 docker 默认/dev/shm 只有64MB,显然是不够使用的,提供两种方式来解决:
- 启动 docker 时添加参数 --shm-size=1gb 来增大 /dev/shm 共享内存,可是 swarm 目前不支持 shm-size 参数
- 启动 Chrome 添加参数 - disable-dev-shm-usage,禁止使用 /dev/shm 共享内存
  • 尽可能使用同一个浏览器实例,这样能够实现缓存共用
  • 经过请求拦截不必加载的资源
  • 像咱们本身打开 Chrome 同样,tab 页多必然会卡,因此必须有效控制 tab 页个数
  • 一个 Chrome 实例启动时间长了不免会出现内存泄漏,页面奔溃等现象,因此定时重启 Chrome 实例是有必要的
  • 为了加快性能,关闭不必的配置,好比:-no-sandbox(沙箱功能),--disable-extensions(扩展程序)等
  • 尽可能避免使用 page.waifFor(1000),让程序本身决定效果会更好
  • 由于和 Chrome 实例链接时使用的 Websocket,会存在 Websocket sticky session 问题.

参考文献

相关文章
相关标签/搜索