很早很早以前,前端就有了对 headless 浏览器的需求,最多的应用场景有两个javascript
也就有了不少杰出的实现,前端常用的莫过于 PhantomJS 和 selenium-webdriver,但两个库有一个共性——难用!环境安装复杂,API 调用不友好,1027 年 Chrome 团队连续放了两个大招 Headless Chrome 和对应的 NodeJS API Puppeteer,直接让 PhantomJS 和 Selenium IDE for Firefox 做者悬宣布不必继续维护其产品html
如同其 github 项目介绍:Puppeteer 是一个经过 DevTools Protocol 控制 headless chrome 的 high-level Node 库,也能够经过设置使用 非 headless Chrome前端
咱们手工能够在浏览器上作的事情 Puppeteer 都能胜任java
官方提供了一个 playground,能够快速体验一下。关于其具体使用不在赘述,官网的 demo 足矣让彻底不了解的同窗入门git
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(); })();
实现网页截图就这么简单,本身也实现了一个简单的爬取百度图片的搜索结果的 demo,代码不过 40 行,用过 selenium-webdriver 的同窗看了会流泪,接下来介绍几个好玩的特性github
虽然 Puppeteer API 足够简单,但若是是从 webdriver 流转过来的同窗会很不适应,主要是在 webdirver 中咱们操做网页更多的是从程序的视角,而在 Puppeteer 中网页浏览者的视角。举个简单的例子,咱们但愿对一个表单的 input 作输入web
webdriver 流程chrome
const input = await driver.findElement(By.id('kw')); await input.sendKeys('test');
Puppeteer 流程api
await page.focus('#kw'); await page.keyboard.sendCharacter('test');
在使用中能够多感觉一下区别,会发现 Puppeteer 的使用会天然不少浏览器
看官方的例子就能够看出来,几乎全部的操做都是异步的,若是坚持使用回调或者 Promise.then 写出来的代码会很是丑陋且难读,Puppeteer 官方推荐的也是使用高版本 Node 用 async/await 语法
const puppeteer = require('puppeteer'); (async () => { const browser = await puppeteer.launch(); const page = await browser.newPage(); await page.goto('https://news.ycombinator.com', {waitUntil: 'networkidle'}); await page.pdf({path: 'hn.pdf', format: 'A4'}); await browser.close(); })();
这是 UI 自动化测试最经常使用的功能了,Puppeteer 的处理也至关简单
这两个函数分别会在页面内执行 document.querySelector
和 document.querySelectorAll
,但返回值却不是 DOM 对象,如同 jQuery 的选择器,返回的是通过本身包装的 Promise<ElementHandle>
,ElementHandle 帮咱们封装了经常使用的 click
、boundingBox
等方法
咱们写爬虫爬取页面图片列表,感受能够经过 page.$$(selector)
获取到页面的元素列表,而后再去转成 DOM 对象,获取 src,而后并不行,想作对获取元素对应 DOM 属性的获取,须要用专门的 API
大概用法
const searchValue = await page.$eval('#search', el => el.value); const preloadHref = await page.$eval('link[rel=preload]', el => el.href); const html = await page.$eval('.main-container', e => e.outerHTML);
const divsCounts = await page.$$eval('div', divs => divs.length);
值得注意的是若是 pageFunction
返回的是 Promise,那么 page.$eval
会等待方法 resolve
若是咱们有一些及其个性的需求,没法经过 page.$() 或者 page.$eval() 实现,能够用大招——evaluate,有几个相关的 API
这几个函数很是相似,都是能够在页面环境执行咱们舒心的 JavaScript,区别主要在执行环境和返回值上
前两个函数都是在当前页面环境内执行,的主要区别在返回值上,第一个返回一个 Serializable 的 Promise,第二个返回值是前面提到的 ElementHandle 对象父类型 JSHandle 的 Promise
const result = await page.evaluate(() => { return Promise.resolve(8 * 7); }); console.log(result); // prints "56" const aWindowHandle = await page.evaluateHandle(() => Promise.resolve(window)); aWindowHandle; // Handle for the window object. 至关于把返回对象作了一层包裹
page.evaluateOnNewDocument(pageFunction, ...args)
是在 browser 环境中执行,执行时机是文档被建立完成可是 script 没有执行阶段,常常用于修改 JavaScript 环境
page.exposeFunction(name, puppeteerFunction)
用于在 window 对象注册一个函数,咱们能够添加一个 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)); // 注册 window.readfile 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(); });
Puppeteer 提供了几个有用的方法让咱们能够修改设备信息
await page.setViewport({ width: 1920, height: 1080 }); await page.setUserAgent('Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/61.0.3163.100 Safari/537.36');
page.emulateMedia(mediaType):能够用来修改页面访问的媒体类型,但仅仅支持
page.emulate(options):前面介绍的几个函数至关于这个函数的快捷方式,这个函数能够设置多个内容
puppeteer/DeviceDescriptors
还给咱们提供了几个大礼包
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(); });
// 直接输入、按键 page.keyboard.type('Hello World!'); page.keyboard.press('ArrowLeft'); // 按住不放 page.keyboard.down('Shift'); for (let i = 0; i < ' World'.length; i++) page.keyboard.press('ArrowLeft'); page.keyboard.up('Shift'); page.keyboard.press('Backspace'); page.keyboard.sendCharacter('嗨');
这几个 API 比较简单,不在展开介绍
Puppeteer 提供了对一些页面常见事件的监听,用法和 jQuery 很相似,经常使用的有
page.on('load', async () => { console.log('page loading done, start fetch...'); const srcs = await page.$$eval((img) => img.src); console.log(`get ${srcs.length} images, start download`); srcs.forEach(async (src) => { // sleep await page.waitFor(200); await srcToImg(src, mn); }); await browser.close(); });
经过 page.getMetrics()
能够获得一些页面性能数据
Timestamp
The timestamp when the metrics sample was taken.Documents
页面文档数Frames
页面 frame 数JSEventListeners
页面内事件监听器数Nodes
页面 DOM 节点数LayoutCount
页面 layout 数RecalcStyleCount
样式重算数LayoutDuration
页面 layout 时间RecalcStyleDuration
样式重算时长ScriptDuration
script 时间TaskDuration
全部浏览器任务时长JSHeapUsedSize
JavaScript 占用堆大小JSHeapTotalSize
JavaScript 堆总量{ Timestamp: 382305.912236, Documents: 5, Frames: 3, JSEventListeners: 129, Nodes: 8810, LayoutCount: 38, RecalcStyleCount: 56, LayoutDuration: 0.596341000346001, RecalcStyleDuration: 0.180430999898817, ScriptDuration: 1.24401400075294, TaskDuration: 2.21657899935963, JSHeapUsedSize: 15430816, JSHeapTotalSize: 23449600 }
本文知识介绍了部分经常使用的 API,所有的 API 能够在 github 上查看,因为 Puppeteer 尚未发布正式版,API 迭代比较迅速,在使用中遇到问题也能够在 issue 中反馈。
在 0.11 版本中只有 page.$eval
并无 page.$$eval
,使用的时候只能经过 page.evaluate
,经过你们的反馈,在 0.12 中已经添加了该功能,整体而言 Puppeteer 仍是一个十分值得期待的 Node headless API
Getting Started with Headless Chrome
Getting started with Puppeteer and Chrome Headless for Web Scraping