切换至深色主题
Puppeteer
重要通知
。
基本概况
Puppeteer是一个Node.js库,它提供了一个高级API来控制 Chrome/Chromium over the DevTools Protocol。
安装配置
代码示例
const puppeteer = require('puppeteer');
module.exports = async function () {
const browser = await puppeteer.launch();
const page = await browser.newPage();
await page.goto('https://example.com');
await page.screenshot({path: 'screenshot.png'});
await browser.close();
}表单交互
// 模拟登陆
await page.click('...');
await page.waitForSelecto('...');
// 输入百度账号密码然后登录
await page.type('#username',"用户提供的用户名");
await page.type('#password','用户提供的密码');
await page.click('button');
await page.waitForNavigation();
console.log(':white_check_mark: 登录成功');
//页面登录成功后,是否需要reload 根据实际情况来确定
await page.reload();键盘与鼠标
# 事件
mouse.click(x, y[, options])
mouse.down([options])
mouse.move(x, y[, options])
mouse.up([options])
# mouse.click
> 功能:单击鼠标
mouse.click(x, y, {
button: 'left' | 'right' | 'middle',
clickCount: 1, #默认持续单击1次
delay: 0
})
# mouse.down
> 功能:按下鼠标
# mouse.move
> 功能:
await page.mouse.move(0, 0);
await page.mouse.down();
await page.mouse.move(0, 100);
await page.mouse.move(100, 100);
await page.mouse.move(100, 0);
await page.mouse.move(0, 0);
await page.mouse.up();
# mouse.up
> 功能:释放鼠标
# 方法
> page.click(selector[, options]) # 在被选择元素上模拟点击
> page.type(selector, text[, options]) # 在被选择的输入框中输入
> page.hover(selector) # 模拟鼠标移动到被选择元素上
> page.select(selector, ...values) # 在被选择元素上模拟选择select选项
> page.tap(selector) # 在被选择元素上模拟触摸
# 案例
> await page.click('div.article_showall a')
# 事件
keyboard.down(key[, options])
keyboard.press(key[, options])
keyboard.sendCharacter(char)
keyboard.type(text[, options])
keyboard.up(key)
# keyboard.down
> 功能:按下键盘
await page.keyboard.down('Shift');
# keyboard.press
> 功能:按着键盘
await page.keyboard.press('Backspace');
# keyboard.sendCharacter
> 功能:输入文本
page.keyboard.sendCharacter('嗨');
# keyboard.type
> 功能:
await page.keyboard.type('World', {delay: 100});
# keyboard.up
> 功能:释放键盘
await page.keyboard.up('Shift');浏览器相关属性与方法
# 事件
browser.on('disconnected')
browser.on('targetchanged')
browser.on('targetcreated')
browser.on('targetdestroyed')
# 属性与方法
browser.browserContexts()
browser.close()
browser.createIncognitoBrowserContext()
browser.defaultBrowserContext()
browser.disconnect()
browser.newPage()
browser.pages()
browser.process()
browser.target()
browser.targets()
browser.userAgent()
browser.version()
browser.wsEndpoint()路由管理
page.goBack([options])
page.goForward([options])
page.goto(url[, options])
# 方法
page.goBack([options])
page.goForward([options])
page.goto(url[, options])
page.reload([options])
# 五个方法参数一样
> options <Object> 导航配置,可选值:timeout <number> 跳转等待时间,单位是毫秒, 默认是30秒, 传 0 表示无限等待。可以通过page.setDefaultNavigationTimeout(timeout)方法修改默认值。waitUntil <string|Array<string>> 满足什么条件认为页面跳转完成,默认是 load 事件触发时。指定事件数组,那么所有事件触发后才认为是跳转完成。事件包括:load - 页面的load事件触发时domcontentloaded - 页面的 DOMContentLoaded 事件触发时networkidle0 - 不再有网络连接时触发(至少500毫秒后)networkidle2 - 只有2个网络连接时触发(至少500毫秒后)
> 返回: <Promise<?Response>> Promise对象resolve后是主要的请求的响应. 如果有多个跳转, resolve后是最后一次跳转的响应. 如果不能向前,resolve后是null
# 代码示例单击, 单击跳转指定页面
await page.waitFor(1000);
await page.click("a[title='Node.js 常用工具']");
# 返回上一页
> page.goBack([options])
await page.waitFor(1000);
await RoutineHandle('page.goBack', async ()=> {
await page.goBack();
});
process.stdout.write(`${page.url()}\n`);
# 继续下一页
> page.goForward([options])
await page.waitFor(1000);
await RoutineHandle('page.goForward', async ()=> {
await page.goForward();
});
process.stdout.write(`${page.url()}\n`);
# 页面跳转
> page.goto(url[, options])
# 页面重加载
> page.reload([options])
/**
* 路由控制
*
*/
const {Goto} = require('../common.js.js');
const SaveDataFile = require(ROOT_PATH + 'api/savedatafile.js');
module.exports = async function() {
//console.log("|-----" + "路由控制" + "-----FILE_URL: " + __filename);
//业务模型
await init();
}
async function init() {
//模块系统
await Goto('https://music.163.com/#/discover/artist/signed/');
process.stdout.write(`${page.url()}\n\n\n`);
//获取指定frame
let initCount = 0;
let event_name = "frame";
console.log(`${event_name} | time is starting ...... ${initCount}`); initCount++;
let timingCount = await setInterval(()=> {console.log(`${event_name} | time is ${initCount} secoond. please wait a moment!`); initCount++;}, 1000);
const URL = page.url();
const frame = page.frames().find(frame => frame.name() === 'contentFrame');
await frame.addScriptTag({url: 'https://cdn.bootcss.com/jquery/3.2.0/jquery.min.js'});
await page.waitFor(1000);
let result = await frame.$eval('body', async (body)=> {
await new Promise((resolve)=> {
let scrollTop = 0;
let distance = 500;
let interval = setInterval(()=> {
height = body.scrollHeight;
window.scrollBy(0, distance);
scrollTop += distance;
if (scrollTop >= height) {
clearInterval(interval);
resolve();
}
}, 1000);
});
let resultJSON = {status: 200, msg: 'success', data: {list: []}};
let dom = $('#m-artist-box').children();
dom.each((dn, dx)=> {
let it = $(dx);
let item = {};
item['portrait'] = it.children('div').children('img').attr('src');
item['link'] = it.children('div').children('a').attr('href');
item['name'] = it.children('p').children('a').text();
item['home'] = it.children('p').children('a').attr('href');
resultJSON['data']['list'].push(item);
});
return resultJSON['data']['list'];
});
clearInterval(timingCount);
console.log(`${event_name} | timing has finished, this handle use up <<${initCount}>> seconds`);
let save_file_path = rootsrc + 'example.json';
let save_data = JSON.stringify(result);
try {
await SaveDataFile(save_file_path, save_data);
} catch(e) {
console.log(e);
}
}权限管理
事件
page.tap(selector)
# 单击与触屏事件
touchscreen.tap(x, y)网页内容(增删改查)
# 基础操作
<script>
const URL = "http://www.360doc.com/content/18/0521/18/8527076_755771471.shtml";
const title = "1950-2011年中国历年财政收入一览表";
await page.goto(URL, {waitUntil: 'networkidle2'});
// 单击页面按钮
const allResultsSelector = '.devsite-suggest-all-results';
await page.waitForSelector(allResultsSelector);
await page.click(allResultsSelector);
// 删除遮挡内容的顶部DOM片段
const deleteNode = await page.$('div.atfixednav');
await page.evaluate((node) => node.remove(), deleteNode);
let result = await page.content(); // 方法一:获取页面HTML
const text = await page.$eval('html', element => element.outerHTML); // 方法二:获取页面HTML
// 检测DOM元素是否加载
await page.waitFor(selectorOrFunctionOrTimeout[, options[, ...args]])
await page.waitFor('.sect > .sele');
// 解析网页内容
const resultsSelector = '.gsc-results .gs-title';
await page.waitForSelector(resultsSelector);
const result = await page.evaluate((resultsSelector) => {
const anchors = Array.from(document.querySelectorAll(resultsSelector));
return anchors.map((anchor) => {
const title = anchor.textContent.split('|')[0].trim();
return `${title} - ${anchor.href}`;
});
// 检索图片
const imgs = document.querySelectorAll('img');
imgs.forEach((value)=> {
arr.push(value.src);
});
}, resultsSelector);
</script>
# HTML文档 | await page.content();
page.target()
page.title()
page.type(selector, text[, options])
page.url()
page.viewport()
page.emulate(options)
page.setUserAgent(userAgent)
page.setViewport(viewport)
page.url()
page.content()
page.frames()
page.mainFrame()
page.metrics()
page.target()
page.title()
page.viewport()
page.metrics()
返回: <Promise<Object>> 包含指标数据的键值对:
Timestamp <number> 时间点(when the metrics sample was taken)
Documents <number> 页面的documents数量。
Frames <number> 页面的iframe数量。
JSEventListeners <number> 页面的js事件数量。
Nodes <number> 页面的dom节点数量。
LayoutCount <number> 整页面或部分页面的布局数量。
RecalcStyleCount <number> 页面样式重新计算数量。
LayoutDuration <number> 页面布局总时间。
RecalcStyleDuration <number> 页面样式重新计算总时间。
ScriptDuration <number> 页面js代码执行总时间。
TaskDuration <number> 页面任务执行总时间。
JSHeapUsedSize <number> 页面占用堆内存大小。
JSHeapTotalSize <number> 总的页面堆内存大小。网页请求Request
# page.on('request')必须放在page.goto之前,否则失效
# 代码示例
<script>
await page.setRequestInterception(true); // 开启请求拦截 | 如果请求拦截没有开启会立即抛出异常
page.on('request', async (request) => {
//
});
page.on('requestfinished', (request) => {
console.log(request);
});
page.on('requestfailed', (request) => {
console.log(request.url() + ' ' + request.failure().errorText);
});
const includeList = [''], // 过滤白名单
excludeList = ['data:image/', 'main.bin']; // 过滤黑名单
// 具体业务
if (request.isInterceptResolutionHandled()) return;
if (request.resourceType() === 'image') {
request.abort();
} else {
request.continue();
}
const headers = Object.assign({}, request.headers(), {
foo: 'bar', // set "foo" header
origin: undefined, // remove "origin" header
});
request.continue({ headers });
const { action, priority } = request.interceptResolutionState();
if (action === InterceptResolutionAction.AlreadyHandled) return;
if (action === InterceptResolutionAction.Continue) return;
request.respond({
status: 404,
contentType: 'text/plain',
body: 'Not Found!',
});
</script>
# 方法
> request.abort([errorCode]):中断请求
> request.continue([overrides]):
> request.failure():返回描述请求失败的对象
> request.frame():
> request.headers():该请求的 http 头对象。所有头都采用小写的命名方式
> request.isNavigationRequest():
> request.method():请求方法
> request.postData():请求提交的数据。
> request.redirectChain():
> request.resourceType():包含渲染引擎识别出的请求资源类型,包含:document,stylesheet,image,media,font,script,texttrack,xhr,fetch,eventsource,websocket,manifest,other。
> request.respond(response):当使用request.abort中断请求后,使用该方法来返回一个响应。[request.respond 不支持模拟响应 dataURL 请求。 对 dataURL请求使用 request.respond 并不会起任何作用。]
> request.response():
> request.url():请求的 URL。网页响应Response
# page.on('response')必须放在page.goto之前,否则失效
# 代码示例
<script>
page.on('response', async (response) => {
console.log(response);
});
// 具体业务
</script>
# 方法
> response.buffer():
> response.frame():
> response.fromCache():
> response.fromServiceWorker():
> response.headers():
> response.json():
> response.ok():
> response.remoteAddress():
> response.request():
> response.securityDetails():
> response.status():
> response.statusText():
> response.text():
> response.url():下拉页面至底部
# 滚动
<script>
let result = await page.evaluate(async ()=> {
let $ = window.$;
//下拉页面到底部
await new Promise((resolve)=> {
let scrollTop = 0;
let distance = 250;
let interval = window.setInterval(()=> {
height = document.body.scrollHeight;
window.scrollBy(0, distance);
scrollTop += distance;
if (scrollTop >= height) {
clearInterval(interval);
resolve();
}
}, 1000);
});
let items = null;
let backJSON = {type: 'array', msg: '提示消息', data: {}, list: []};
//解析数据
items = $('');
if (items.length >= 1) {
items.each((index, item)=> {
let it = $(item);
let name = null;
backJSON.dataList.push({name});
});
}
return backJSON;
});
</script>
# frame框架内部
<script>
//获取指定frame
const frame = page.frames().find(frame => frame.name() === 'contentFrame');
//获取HTML
let html = await frame.$eval('body', async (body)=> {
await new Promise((resolve)=> {
let scrollTop = 0;
let distance = 500;
let interval = setInterval(()=> {
height = body.scrollHeight;
window.scrollBy(0, distance);
scrollTop += distance;
if (scrollTop >= height) {
clearInterval(interval);
resolve();
}
}, 1000);
});
return body.innerHTML;
});
let save_file_path = rootsrc + 'name.html';
let save_data = html;
SaveDataFile(save_file_path, save_data);
</script>性能测试与自动化测试
在平时调试中我们通常用两种方法来看页面性能数据:
window.performance.timing 和 chrome的DevTools中的Performance工具。
经查阅得知,window.performance.timing是执行于网页上下文的,而DevTools是执行于浏览器上下文的。这边给出获取两种数据的代码
const puppeteer = require('puppeteer')
const test = (async () => {
const browser = await puppeteer.launch({headless: false})
const page = await browser.newPage()
await page.goto('https://www.jianshu.com')
// 获取 DevTools 中的数据
let devData = await page._client.send('Performance.getMetrics')
// 获取 window 中的数据
let windowData = JSON.parse(await page.evaluate(() => {
JSON.stringify(window.performance.timing
})
})
test()追加外部资源
await page.goto(pageLink, {waitUntil: 'networkidle2'});
# page.addScriptTag(options)
try {
//载入jQuery
await page.mainFrame().addScriptTag({url: 'https://cdn.bootcss.com/jquery/3.2.0/jquery.min.js'});
} catch(e) {
console.log(JSON.stringify(e));
}
page.addScriptTag(options)
page.addStyleTag(options)
# page.addStyleTag(options)api
Cookie
page.cookies([...urls])
page.deleteCookie(...cookies)DevTools
Chrome DevTools Protocol:https://chromedevtools.github.io/devtools-protocol/
await page._client.send('Network.emulateNetworkConditions', {
offline: false,
latency: 200, // ms
downloadThroughput: 780 * 1024 / 8, // 780 kb/s
uploadThroughput: 330 * 1024 / 8, // 330 kb/s
});iframe操作技巧
# 获取指定frame
const frame = page.frames().find(frame => frame.name() === 'contentFrame');
# 获取内容
const text = await frame.$eval('html', element => element.textContent);
console.log(text);
# 获取HTML
const html = await frame.$eval('html', element => element.outerHTML);
# 页面框架
page.frames() // 页面包含的frame数组, 总数为page.frames().length
let frame = page.mainFrame()
# 嵌入frame页面的url
frame.url()
# frame数量
frame.childFrames()
# 方法
frame.$(selector)
frame.$$(selector)
frame.$$eval(selector, pageFunction[, ...args])
frame.$eval(selector, pageFunction[, ...args])
frame.$x(expression)
frame.addScriptTag(options)
frame.addStyleTag(options)
frame.childFrames()
frame.click(selector[, options])
frame.content()
frame.evaluate(pageFunction[, ...args])
frame.evaluateHandle(pageFunction[, ...args])
frame.executionContext()
frame.focus(selector)
frame.goto(url[, options])
frame.hover(selector)
frame.isDetached()
frame.name()
frame.parentFrame()
frame.select(selector, ...values)
frame.setContent(html[, options])
frame.tap(selector)
frame.title()
frame.type(selector, text[, options])
frame.url()
frame.waitFor(selectorOrFunctionOrTimeout[, options[, ...args]])
frame.waitForFunction(pageFunction[, options[, ...args]])
frame.waitForNavigation([options])
frame.waitForSelector(selector[, options])
frame.waitForXPath(xpath[, options])基础API
浏览器 Browser
- 启动具有给定参数和选项(如果指定)的浏览器实例
const browser = await puppeteer.launch({
headless: true, // 无头模式, boolean | 'new', true-启用无头模式 false-启用有头模式 'new'-Chrome 112 推出的一种新的无头模式
args: [ // , string[],
// 传递给浏览器实例的参数
'--no-proxy-server',
'--no-sandbox',
'--disable-setuid-sandbox',
'--disable-dev-shm-usage',
],
debuggingPort: , // , number,
devtools: false, // , boolean,
userDataDir: , // , string,
});
browser.close();键盘 Keyboard
鼠标 Mouse
触摸屏 Touchscreen
框架 Frame
延时、等待时间与超时
// 等待
page.waitFor(selectorOrFunctionOrTimeout[, options[, ...args]]) # 延迟下一步操作
page.waitForFileChooser([options])
page.waitForFunction(pageFunction[, options[, ...args]])
page.waitForNavigation([options]) # 延迟跳转时间
page.waitForRequest(urlOrPredicate[, options])
page.waitForResponse(urlOrPredicate[, options])
page.waitForSelector(selector[, options])
page.waitForXPath(xpath[, options])
// page.waitFor(selectorOrFunctionOrTimeout[, options[, ...args]])
selector: 选择器, 检测DOM元素加载
Function: 函数, 等待函数执行完成
Timeout: 延迟时间, 延迟等待时间
网页 Page
const page = await browser.newPage();
page.viewport({
width: ,
height: ,
deviceScaleFactor: 1,
isMobile: false,
hasTouch: false,
isLandScape: false
});
page.close();URL
const url = "";
page.goto(url, {
timeout: 10000, // 页面超时容错时间
waitUntil: [ // 等待条件
'load', // window.onload 事件被触发时继续。
'domcontentloaded', // Domcontentloaded 事件触发时继续。
'networkidle0', // 在 500ms 内没有网络连接时(全部的request结束)则继续。
'networkidle2' // 500ms 内有不超过 2 个网络连接时就算成功(还有两个以下的request)则继续。
]
});
导出 PDF
await page.pdf({
path: STATIC_PATH + `${Date.now()}.pdf`,
width: 1920,
printBackground: true,
displayHeaderFooter: true,
format: 'A4'
});导出图片
- 截图整个页面范围
const image = page.screenshot({
captureBeyondViewport: true,
clip: {
width: ,
height: ,
scale: 1,
x: ,
y:
},
encoding: 'binary', // 'base64' | 'binary'
fromSurface: false,
fullPage: false,
omitBackground: false, // 隐藏网页背景, 即transparent
optimizeForSpeed: ,
path: , // 保存图片磁盘路径,如果不存在,则返回流数据, '/home/meta/server/oss/'
quality: 100,
type: 'png', // 图片格式, 默认为'png', 取值'png' | 'jpeg' | 'webp'
});
// 方案一:返回图片数据结构
const base64Image = await image.toString('base64');
// 方案二:返回图片数据结构
ctx.body = image;- 截图指定DOM结构范围
/**
* 图片生成模型
* @param {*} data 数据
* @param {*} headers 请求头
* @returns
*/
async function imageModel(data = {}, headers = {}) {
const { userInfo, recordInfo, activityID, activityDetail, homeLink, shareLink } = data || {};
const { sessionid: session_id, token } = headers || {};
if (
!userInfo ||
!recordInfo ||
!activityID ||
!activityDetail ||
!homeLink ||
!shareLink
) {
return null;
}
if (
!session_id ||
!token
) {
return null;
}
// 渲染网页
const page = await browser.newPage();
await page.goto(homeLink, { waitUntil: 'networkidle2' });
// 添加 localStorage 缓存
await page.evaluate((token, session_id, activityID, activityDetail = {}, userInfo = {}, recordInfo = {}) => {
localStorage.clear();
localStorage.setItem('token', token);
localStorage.setItem('session_id', session_id);
localStorage.setItem('user_info', JSON.stringify(userInfo));
localStorage.setItem('activity_id', activityID);
localStorage.setItem('activity_detail', JSON.stringify(activityDetail));
localStorage.setItem('record_detail', JSON.stringify(recordInfo));
}, token, session_id, activityID, activityDetail, userInfo, recordInfo);
// 基于以上操作实现的localStorage缓存再次加载网页
await page.goto(shareLink, { waitUntil: 'networkidle2', timeout: 600000 });
// 移除 蒙层
const masklayer = "div.van-overlay.vant-dialog-mask";
await page.waitForSelector(masklayer);
const deleteMask = await page.$(masklayer);
await page.evaluate((node) => node.remove(), deleteMask);
// 移除 "打卡成功"弹窗
const modallayer = "div.van-dialog.component-failed-dialog";
await page.waitForSelector(modallayer);
const deleteModal = await page.$(modallayer);
await page.evaluate((node) => node.remove(), deleteModal);
const scrollViewClsName = "body > div#app > section > div.scroll-container > div.scroll-view";
await page.waitForSelector(scrollViewClsName);
const scrollViewDOM = await page.$(scrollViewClsName);
// 有效视口范围 截屏
const options = { encoding: 'binary' };
const image = await scrollViewDOM.screenshot(options);
return image;
}
DOM 操作
选择器
const element = await page.waitForSelector('div > .class-name');
await element.click();
await element.dispose();- page.$(selector):此方法是page.mainFrame().$(selector) 的简写,在页面内执行 document.querySelector。如果没有元素匹配指定选择器,返回值是 null。
- page.$$(selector):此方法是page.mainFrame().$$(selector) 的简写。在页面内执行 document.querySelectorAll。如果没有元素匹配指定选择器,返回值是 []。
- page.$$eval(selector, pageFunction[, ...args]):此方法在页面内执行 Array.from(document.querySelectorAll(selector)),然后把匹配到的元素数组作为第一个参数传给 pageFunction。
- page.$eval(selector, pageFunction[, ...args]):
执行JS
Puppeteer 允许在页面上下文中执行 JavaScript 函数。
const three = await page.evaluate(() => {
return 1 + 2;
});
console.log(three);- page.evaluate 传参
const result = await page.evaluate((name, age) => {
return {name, age};
}, 'Eshen', 19);
console.log(result);拦截请求 HTTPRequest
拦截响应 HTTPResponse
权限与依赖数据
cookie
sessionStorage
localStorage
headers请求头
经典案例
使用Nodejs和Puppeteer从HTML中导出PDF
<button onClick={()=> {this.savePDF()}}>generate PDF in puppeteer</button>
<script>
const puppeteer = require('puppeteer')
(async () => {
const brower = await puppeteer.launch()
const page = await brower.newPage()
await page.goto('https://github.com/linpenghui958', { waitUntil: 'networkidle0'})
const pdf = await page.pdf({ format: 'A4'})
await brower.close()
return pdf
})();
const result = await printPdf()
res.set({'Content-Type': 'application/pdf', 'Content-Length': result.length})
res.send(result)
getPDF() {
return axios.get('//localhost:3001/puppeteer', {
responseType: 'arraybuffer',
headers: {
'Accept': 'application/pdf'
}
})
}
savePDF() {
this.getPDF()
.then(res => {
const blob = new Blob([res.data], { type: 'application/pdf'})
const link = document.createElement('a')
link.href = window.URL.createObjectURL(blob)
link.download = 'test.pdf'
link.click()
})
.catch(e => console.log(e))
}
</script>