Puppeteer 是 Chrome 开发团队在 2017 年发布的一个 Node.js 包,同时还有 Headless Chrome。用来模拟 Chrome 浏览器的运行。它提供了高级API来经过 DevTools 协议控制无头 Chrome 或 Chromium ,它也能够配置为使用完整(非无头)Chrome 或 Chromium。javascript
学习 Puppeteer 以前咱们先来了解一下 Chrome DevTool Protocol 和 Headless Chrome。java
总而言之 Headless Chrome 就是 Chrome 浏览器的无界面形态,能够在不打开浏览器的前提下,使用全部 Chrome 支持的特性运行你的程序。node
官方介绍:您能够在浏览器中手动执行的大多数操做均可以使用 Puppeteer 完成!示例:git
Puppeteer 中的 API 分层结构基本和浏览器保持一致,下面对常使用到的几个类介绍一下:es6
注意:在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 既能够对某个页面进行截图,也能够对页面中的某个元素进行截图: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')
:等待某个选择器对应的元素出现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')
:在输入框输入文本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 相关函数库请求在有些场景下颇有必要,拦截一下不必的请求提升性能,咱们能够在监听 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')
建立 webWorkerpage.on('workerdestroyed')
销毁 webWorkerPuppeteer 目前没有提供原生的用于处理 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(); })();
一个 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(); })();
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(); })();
在自动化测试中,常常会遇到对于文件的上传和下载的需求,那么在 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(); })();
在点击一个按钮跳转到新的 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;
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 共享内存