一、Puppeteer 简介

Puppeteer 是一个node库,他提供了一组用来操纵Chrome的API, 通俗来讲就是一个 headless chrome浏览器 (固然你也能够配置成有UI的,默认是没有的)。既然是浏览器,那么咱们手工能够在浏览器上作的事情 Puppeteer 都能胜任, 另外,Puppeteer 翻译成中文是”木偶”意思,因此听名字就知道,操纵起来很方便,你能够很方便的操纵她去实现:javascript

1) 生成网页截图或者 PDF 
2) 高级爬虫,能够爬取大量异步渲染内容的网页 
3) 模拟键盘输入、表单自动提交、登陆网页等,实现 UI 自动化测试 
4) 捕获站点的时间线,以便追踪你的网站,帮助分析网站性能问题php

若是你用过 PhantomJS 的话,你会发现她们有点相似,但Puppeteer是Chrome官方团队进行维护的,用俗话说就是”有娘家的人“,前景更好。css

二、运行环境

查看 Puppeteer 的官方 API 你会发现满屏的 async, await 之类,这些都是 ES7 的规范,因此你须要:html

  1. Nodejs 的版本不能低于 v7.6.0, 须要支持 async, await.
  2. 须要最新的 chrome driver, 这个你在经过 npm 安装 Puppeteer 的时候系统会自动下载的
npm install puppeteer --save 

三、基本用法

先开看看官方的入门的 DEMOjava

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(); })(); 

上面这段代码就实现了网页截图,先大概解读一下上面几行代码:node

  1. 先经过 puppeteer.launch() 建立一个浏览器实例 Browser 对象
  2. 而后经过 Browser 对象建立页面 Page 对象
  3. 而后 page.goto() 跳转到指定的页面
  4. 调用 page.screenshot() 对页面进行截图
  5. 关闭浏览器

是否是以为好简单? 反正我是以为比 PhantomJS 简单,至于跟 selenium-webdriver 比起来, 那更不用说了。下面就介绍一下 puppeteer 的经常使用的几个 API。linux

3.1 puppeteer.launch(options)

使用 puppeteer.launch() 运行 puppeteer,它会 return 一个 promise,使用 then 方法获取 browser 实例, 固然高版本的 的 nodejs 已经支持 await 特性了,因此上面的例子使用 await 关键字,这一点须要特殊说明一下,Puppeteer 几乎全部的操做都是 异步的, 为了使用大量的 then 使得代码的可读性下降,本文全部 demo 代码都是用 async, await 方式实现。这个 也是 Puppeteer 官方推荐的写法。对 async/await 一脸懵逼的同窗狠狠的戳这里git

options 参数详解
参数名称 参数类型 参数说明
ignoreHTTPSErrors boolean 在请求的过程当中是否忽略 Https 报错信息,默认为 false
headless boolean 是否以”无头”的模式运行 chrome, 也就是不显示 UI, 默认为 true
executablePath string 可执行文件的路劲,Puppeteer 默认是使用它自带的 chrome webdriver, 若是你想指定一个本身的 webdriver 路径,能够经过这个参数设置
slowMo number 使 Puppeteer 操做减速,单位是毫秒。若是你想看看 Puppeteer 的整个工做过程,这个参数将很是有用。
args Array(String) 传递给 chrome 实例的其余参数,好比你可使用”–ash-host-window-bounds=1024x768” 来设置浏览器窗口大小。更多参数参数列表能够参考这里
handleSIGINT boolean 是否容许经过进程信号控制 chrome 进程,也就是说是否可使用 CTRL+C 关闭并退出浏览器.
timeout number 等待 Chrome 实例启动的最长时间。默认为30000(30秒)。若是传入 0 的话则不限制时间
dumpio boolean 是否将浏览器进程stdout和stderr导入到process.stdout和process.stderr中。默认为false。
userDataDir string 设置用户数据目录,默认linux 是在 ~/.config 目录,window 默认在 C:\Users{USER}\AppData\Local\Google\Chrome\User Data, 其中 {USER} 表明当前登陆的用户名
env Object 指定对Chromium可见的环境变量。默认为process.env。
devtools boolean 是否为每一个选项卡自动打开DevTools面板, 这个选项只有当 headless 设置为 false 的时候有效

3.2 Browser 对象

当 Puppeteer 链接到一个 Chrome 实例的时候就会建立一个 Browser 对象,有如下两种方式:github

Puppeteer.launch 和 Puppeteer.connect.web

下面这个 DEMO 实现断开链接以后从新链接浏览器实例

const puppeteer = require('puppeteer'); puppeteer.launch().then(async browser => { // 保存 Endpoint,这样就能够从新链接 Chromium const browserWSEndpoint = browser.wsEndpoint(); // 从Chromium 断开链接 browser.disconnect(); // 使用endpoint 从新和 Chromiunm 创建链接 const browser2 = await puppeteer.connect({browserWSEndpoint}); // Close Chromium await browser2.close(); }); 
Browser 对象 API
方法名称 返回值 说明
browser.close() Promise 关闭浏览器
browser.disconnect() void 断开浏览器链接
browser.newPage() Promise(Page) 建立一个 Page 实例
browser.pages() Promise(Array(Page)) 获取全部打开的 Page 实例
browser.targets() Array(Target) 获取全部活动的 targets
browser.version() Promise(String) 获取浏览器的版本
browser.wsEndpoint() String 返回浏览器实例的 socket 链接 URL, 能够经过这个 URL 重链接 chrome 实例

好了,Puppeteer 的API 就不一一介绍了,官方提供的详细的 API, 戳这里

四、Puppeteer 实战

了解 API 以后咱们就能够来一些实战了,在此以前,咱们先了解一下 Puppeteer 的设计原理,简单来讲 Puppeteer 跟 webdriver 以及 PhantomJS 最大的 的不一样就是它是站在用户浏览的角度,而 webdriver 和 PhantomJS 最初设计就是用来作自动化测试的,因此它是站在机器浏览的角度来设计的,因此它们 使用的是不一样的设计哲学。举个栗子,加入我须要打开京东的首页并进行一次产品搜索,分别看看使用 Puppeteer 和 webdriver 的实现流程:

Puppeteer 的实现流程:

  1. 打开京东首页
  2. 将光标 focus 到搜索输入框
  3. 键盘点击输入文字
  4. 点击搜索按钮

webdriver 的实现流程:

  1. 打开京东首页
  2. 找到输入框的 input 元素
  3. 设置 input 的值为要搜索文字
  4. 触发搜索按钮的单机事件

我的感受 Puppeteer 设计哲学更符合任何的操做习惯,更天然一些。

下面咱们就用一个简单的需求实现来进行 Puppeteer 的入门学习。这个简单的需求就是:

在京东商城抓取10个手机商品,并把商品的详情页截图。

首先咱们来梳理一下操做流程

  1. 打开京东首页
  2. 输入“手机”关键字并搜索
  3. 获取前10个商品的 A 标签,并获取 href 属性值,获取商品详情连接
  4. 分别打开10个商品的详情页,截取网页图片

要实现上面的功能须要用到查找元素,获取属性,键盘事件等,那接下来咱们就一个一个的讲解一下。

4.1 获取元素

Page 对象提供了2个 API 来获取页面元素

(1). Page.$(selector) 获取单个元素,底层是调用的是 document.querySelector() , 因此选择器的 selector 格式遵循 css 选择器规范

let inputElement = await page.$("#search", input => input); //下面写法等价 let inputElement = await page.$('#search'); 

(2). Page.$$(selector) 获取一组元素,底层调用的是 document.querySelectorAll(). 返回 Promise(Array(ElemetHandle)) 元素数组.

const links = await page.$$("a"); //下面写法等价 const links = await page.$$("a", links => links); 

最终返回的都是 ElemetHandle 对象

4.2 获取元素属性

Puppeteer 获取元素属性跟咱们平时写前段的js的逻辑有点不同,按照一般的逻辑,应该是现获取元素,而后在获取元素的属性。可是上面咱们知道 获取元素的 API 最终返回的都是 ElemetHandle 对象,而你去查看 ElemetHandle 的 API 你会发现,它并无获取元素属性的 API.

事实上 Puppeteer 专门提供了一套获取属性的 API, Page.$eval() 和 Page.$$eval()

(1). Page.$$eval(selector, pageFunction[, …args]), 获取单个元素的属性,这里的选择器 selector 跟上面 Page.$(selector) 是同样的。

const value = await page.$eval('input[name=search]', input => input.value); const href = await page.$eval('#a", ele => ele.href); const content = await page.$eval('.content', ele => ele.outerHTML); 

4.3 执行自定义的 JS 脚本

Puppeteer 的 Page 对象提供了一系列 evaluate 方法,你能够经过他们来执行一些自定义的 js 代码,主要提供了下面三个 API

(1). page.evaluate(pageFunction, …args) 返回一个可序列化的普通对象,pageFunction 表示要在页面执行的函数, args 表示传入给 pageFunction 的参数, 下面的 pageFunction 和 args 表示一样的意思。

const result = await page.evaluate(() => { return Promise.resolve(8 * 7); }); console.log(result); // prints "56" 

这个方法颇有用,好比咱们在获取页面的截图的时候,默认是只截图当前浏览器窗口的尺寸大小,默认值是800x600,那若是咱们须要获取整个网页的完整 截图是没办法办到的。Page.screenshot() 方法提供了能够设置截图区域大小的参数,那么咱们只要在页面加载完了以后获取页面的宽度和高度就能够解决 这个问题了。

(async () => { const browser = await puppeteer.launch({headless:true}); const page = await browser.newPage(); await page.goto('https://jr.dayi35.com'); await page.setViewport({width:1920, height:1080}); const documentSize = await page.evaluate(() => { return { width: document.documentElement.clientWidth, height : document.body.clientHeight, } }) await page.screenshot({path:"example.png", clip : {x:0, y:0, width:1920, height:documentSize.height}}); await browser.close(); })(); 

(2). Page.evaluateHandle(pageFunction, …args) 在 Page 上下文执行一个 pageFunction, 返回 JSHandle 实体

const aWindowHandle = await page.evaluateHandle(() => Promise.resolve(window)); aWindowHandle; // Handle for the window object. const aHandle = await page.evaluateHandle('document'); // Handle for the 'document'. 

从上面的代码能够看出,page.evaluateHandle() 方法也是经过 Promise.resolve 方法直接把 Promise 的最终处理结果返回, 只不过把最后返回的对象封装成了 JSHandle 对象。本质上跟 evaluate 没有什么区别。

下面这段代码实现获取页面的动态(包括js动态插入的元素) HTML 代码.

const aHandle = await page.evaluateHandle(() => document.body); const resultHandle = await page.evaluateHandle(body => body.innerHTML, aHandle); console.log(await resultHandle.jsonValue()); await resultHandle.dispose(); 

(3). page.evaluateOnNewDocument(pageFunction, …args), 在文档页面载入前调用 pageFunction, 若是页面中有 iframe 或者 frame, 则函数调用 的上下文环境将变成子页面的,即iframe 或者 frame, 因为是在页面加载前调用,这个函数通常是用来初始化 javascript 环境的,好比重置或者 初始化一些全局变量。

4.4 Page.exposeFunction

除此上面三个 API 以外,还有一相似的很是有用的 API, 那就是 Page.exposeFunction,这个 API 用来在页面注册全局函数,很是有用:

由于有时候须要在页面处理一些操做的时候须要用到一些函数,虽然能够经过 Page.evaluate() API 在页面定义函数,好比:

const docSize = await page.evaluate(()=> { function getPageSize() { return { width: document.documentElement.clientWidth, height : document.body.clientHeight, } } return getPageSize(); }); 

可是这样的函数不是全局的,须要在每一个 evaluate 中去从新定义,没法作到代码复用,在一个就是 nodejs 有不少工具包能够很轻松的实现很复杂的功能 好比要实现 md5 加密函数,这个用纯 js 去实现就不太方便了,而用 nodejs 倒是几行代码的事情。

下面代码实现给 Page 上下文的 window 对象添加 md5 函数:

const puppeteer = require('puppeteer'); const crypto = require('crypto'); puppeteer.launch().then(async browser => { const page = await browser.newPage(); page.on('console', msg => console.log(msg.text)); await page.exposeFunction('md5', text => crypto.createHash('md5').update(text).digest('hex') ); await page.evaluate(async () => { // use window.md5 to compute hashes const myString = 'PUPPETEER'; const myHash = await window.md5(myString); console.log(`md5 of ${myString} is ${myHash}`); }); await browser.close(); }); 

能够看出,Page.exposeFunction API 使用起来是很方便的,也很是有用,在好比给 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)); 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(); }); 

五、Page.emulate 修改模拟器(客户端)运行配置

Puppeteer 提供了一些 API 供咱们修改浏览器终端的配置

  1. Page.setViewport() 修改浏览器视窗大小
  2. Page.setUserAgent() 设置浏览器的 UserAgent 信息
  3. Page.emulateMedia() 更改页面的CSS媒体类型,用于进行模拟媒体仿真。 可选值为 “screen”, “print”, “null”, 若是设置为 null 则表示禁用媒体仿真。
  4. Page.emulate() 模拟设备,参数设备对象,好比 iPhone, Mac, Android 等
page.setViewport({width:1920, height:1080}); //设置视窗大小为 1920x1080 page.setUserAgent('Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/60.0.3112.90 Safari/537.36'); page.emulateMedia('print'); //设置打印机媒体样式 

除此以外咱们还能够模拟非 PC 机设备, 好比下面这段代码模拟 iPhone 6 访问google:

const puppeteer = require('puppeteer'); const devices = require('puppeteer/DeviceDescriptors'); const iPhone = devices['iPhone 6']; 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(); }); 

Puppeteer 支持不少设备模拟仿真,好比Galaxy, iPhone, IPad 等,想要知道详细设备支持,请戳这里 DeviceDescriptors.js.

六、键盘和鼠标

键盘和鼠标的API比较简单,键盘的几个API以下:

  • keyboard.down(key[, options]) 触发 keydown 事件
  • keyboard.press(key[, options]) 按下某个键,key 表示键的名称,好比 ‘ArrowLeft’ 向左键,详细的键名映射请戳这里
  • keyboard.sendCharacter(char) 输入一个字符
  • keyboard.type(text, options) 输入一个字符串
  • keyboard.up(key) 触发 keyup 事件
page.keyboard.press("Shift"); //按下 Shift 键 page.keyboard.sendCharacter('嗨'); page.keyboard.type('Hello'); // 一次输入完成 page.keyboard.type('World', {delay: 100}); // 像用户同样慢慢输入 

鼠标操做:

  • mouse.click(x, y, [options]) 移动鼠标指针到指定的位置,而后按下鼠标,这个其实 mouse.move 和 mouse.down 或 mouse.up 的快捷操做
  • mouse.down([options]) 触发 mousedown 事件,options 可配置:
    • options.button 按下了哪一个键,可选值为[left, right, middle], 默认是 left, 表示鼠标左键
    • options.clickCount 按下的次数,单击,双击或者其余次数
    • delay 按键延时时间
  • mouse.move(x, y, [options]) 移动鼠标到指定位置, options.steps 表示移动的步长
  • mouse.up([options]) 触发 mouseup 事件

七、另外几个有用的 API

Puppeteer 还提供几个很是有用的 API, 好比:

7.1 Page.waitFor 系列 API

  • page.waitFor(selectorOrFunctionOrTimeout[, options[, …args]]) 下面三个的综合 API
  • page.waitForFunction(pageFunction[, options[, …args]]) 等待 pageFunction 执行完成以后
  • page.waitForNavigation(options) 等待页面基本元素加载完以后,好比同步的 HTML, CSS, JS 等代码
  • page.waitForSelector(selector[, options]) 等待某个选择器的元素加载以后,这个元素能够是异步加载的,这个 API 很是有用,你懂的。

好比我想获取某个经过 js 异步加载的元素,那么直接获取确定是获取不到的。这个时候就可使用 page.waitForSelector 来解决:

await page.waitForSelector('.gl-item'); //等待元素加载以后,不然获取不到异步加载的元素 const links = await page.$$eval('.gl-item > .gl-i-wrap > .p-img > a', links => { return links.map(a => { return { href: a.href.trim(), name: a.title } }); }); 

其实上面的代码就能够解决咱们最上面的需求,抓取京东的产品,由于是异步加载的,因此使用这种方式。

7.2 page.getMetrics()

经过 page.getMetrics() 能够获得一些页面性能数据, 捕获网站的时间线跟踪,以帮助诊断性能问题。

  • Timestamp 度量标准采样的时间戳
  • Documents 页面文档数
  • Frames 页面 frame 数
  • JSEventListeners 页面内事件监听器数
  • Nodes 页面 DOM 节点数
  • LayoutCount 页面布局总数
  • RecalcStyleCount 样式重算数
  • LayoutDuration 全部页面布局的合并持续时间
  • RecalcStyleDuration 全部页面样式从新计算的组合持续时间。
  • ScriptDuration 全部脚本执行的持续时间
  • TaskDuration 全部浏览器任务时长
  • JSHeapUsedSize JavaScript 占用堆大小
  • JSHeapTotalSize JavaScript 堆总量

八、总结和源码

本文经过一个实际需求来学习了 Puppeteer 的一些基本的经常使用的 API, API 的版本是 v0.13.0-alpha. 最新邦本的 API 请参考 Puppeteer 官方API.

总的来讲,Puppeteer 真是一款不错的 headless 工具,操做简单,功能强大。用来作UI自动化测试,和一些小工具都是很不错的。

下面贴上咱们开始的需求实现源码,仅供参考:

//延时函数 function sleep(delay) { return new Promise((resolve, reject) => { setTimeout(() => { try { resolve(1) } catch (e) { reject(0) } }, delay) }) } const puppeteer = require('puppeteer'); puppeteer.launch({ ignoreHTTPSErrors:true, headless:false,slowMo:250, timeout:0}).then(async browser => { let page = await browser.newPage(); await page.setJavaScriptEnabled(true); await page.goto("https://www.jd.com/"); const searchInput = await page.$("#key"); await searchInput.focus(); //定位到搜索框 await page.keyboard.type("手机"); const searchBtn = await page.$(".button"); await searchBtn.click(); await page.waitForSelector('.gl-item'); //等待元素加载以后,不然获取不异步加载的元素 const links = await page.$$eval('.gl-item > .gl-i-wrap > .p-img > a', links => { return links.map(a => { return { href: a.href.trim(), title: a.title } }); }); page.close(); const aTags = links.splice(0, 10); for (var i = 1; i < aTags.length; i++) { page = await browser.newPage() page.setJavaScriptEnabled(true); await page.setViewport({width:1920, height:1080}); var a = aTags[i]; await page.goto(a.href, {timeout:0}); //防止页面太长,加载超时 //注入代码,慢慢把滚动条滑到最底部,保证全部的元素被所有加载 let scrollEnable = true; let scrollStep = 500; //每次滚动的步长 while (scrollEnable) { scrollEnable = await page.evaluate((scrollStep) => { let scrollTop = document.scrollingElement.scrollTop; document.scrollingElement.scrollTop = scrollTop + scrollStep; return document.body.clientHeight > scrollTop + 1080 ? true : false }, scrollStep); await sleep(100); } await page.waitForSelector("#footer-2014", {timeout:0}); //判断是否到达底部了 let filename = "images/items-"+i+".png"; //这里有个Puppeteer的bug一直没有解决,发现截图的高度最大只能是16384px, 超出部分被截掉了。 await page.screenshot({path:filename, fullPage:true}); page.close(); } browser.close(); }); 

《THE END》