8.截图、PDF与媒体处理
Puppeteer 的截图与 PDF 生成功能可以说是最常用的特性之一。无论是做自动化测试时需要留存证据,还是搭建网页快照服务,这些功能都能派上大用场。本章会详细介绍如何精确控制截图范围、生成打印级 PDF,以及处理文件上传下载这些实际场景中的常见问题。
全页面与元素截图
截图是 Puppeteer 最直观的功能。打开一个页面,调用 screenshot 方法,就能得到一张完整的网页图片。这背后其实做了不少工作:等待页面加载完成、处理懒加载图片、计算页面尺寸等。
基础截图操作
最简单的截图只需要几行代码。启动浏览器,打开页面,调用 page.screenshot(),指定保存路径即可。默认情况下,截图会按照当前视口大小捕获可见区域的内容。
import puppeteer from 'puppeteer';
const browser = await puppeteer.launch();
const page = await browser.newPage();
await page.goto('https://news.ycombinator.com', {
waitUntil: 'networkidle2',
});
// 保存为 hn.png
await page.screenshot({
path: 'hn.png',
});
await browser.close();
waitUntil: 'networkidle2' 这个参数值得注意。它表示网络连接数少于 2 个时才认为页面加载完成,这对动态内容很有用。如果页面有持续的后台请求,可以用 networkidle0 更严格地等待。
全页面截图
默认截图只捕获当前视口的内容。要获取完整网页,需要设置 fullPage: true。这会滚动页面,拼接多张截图,最终生成一张长图。
await page.screenshot({
path: 'full-page.png',
fullPage: true,
});
全页面截图有个细节:Puppeteer 会尝试计算页面的完整高度,包括滚动区域。但有些页面使用了虚拟滚动技术(比如无限加载的列表),这种情况下截图可能无法捕获所有内容。这时候需要手动滚动页面,触发数据加载,再执行截图。
元素级截图
有时候只需要截取某个特定区域,比如一个图表或卡片。可以先定位元素,然后对元素句柄调用 screenshot 方法。
// 等待元素出现
const element = await page.waitForSelector('div.content');
// 只截取这个元素
await element.screenshot({
path: 'element.png',
});
元素截图会自动处理滚动。如果元素不在视口内,Puppeteer 会将其滚动到可见区域再截图。这个特性很贴心,省去了手动计算位置的麻烦。
截图参数精调
screenshot 方法提供了丰富的配置选项。可以控制图片格式、质量、裁剪区域等。
await page.screenshot({
path: 'optimized.png',
type: 'png', // 或 'jpeg'
quality: 80, // 仅对 jpeg 有效
fullPage: false,
clip: {
x: 100,
y: 100,
width: 800,
height: 600,
},
omitBackground: true, // 透明背景
encoding: 'binary', // 或 'base64'
});
omitBackground: true 在截取透明背景的页面时很有用,比如 SVG 图表或带有 background: transparent 的元素。注意这个选项只对 PNG 格式有效。
截图在内存中处理
有时候不需要保存文件,而是直接在内存中处理截图数据。可以省略 path 参数,方法会返回 Buffer 或 Base64 字符串。
// 获取 Buffer
const buffer = await page.screenshot();
// 上传到其他服务
await uploadToStorage(buffer);
// 获取 Base64
const base64 = await page.screenshot({ encoding: 'base64' });
// 嵌入 HTML 或发送给前端
console.log(`data:image/png;base64,${base64}`);
这种模式在服务端渲染场景中很常见:截取页面后,直接将图片数据返回给客户端,避免磁盘 IO。
处理截图时机
截图时机很关键。页面可能在加载完成后仍有动画或异步数据。可以使用等待策略确保内容就绪。
// 等待特定元素出现
await page.waitForSelector('.chart-loaded');
// 等待网络空闲
await page.waitForNetworkIdle();
// 等待自定义条件
await page.waitForFunction(() => {
return document.querySelectorAll('.item').length > 10;
});
await page.screenshot({ path: 'ready.png' });
对于动画,可以等待动画完成或强制等待固定时间。但要注意,硬编码的 setTimeout 会降低测试稳定性,优先使用明确的等待条件。
生成 PDF 文档
PDF 生成是另一个高频需求。Puppeteer 的 page.pdf() 方法能将网页转换为打印质量的 PDF 文件,支持分页、页眉页脚、背景图形等高级特性。
基础 PDF 生成
生成 PDF 的代码结构类似截图。访问页面后调用 page.pdf(),指定输出路径即可。
const browser = await puppeteer.launch();
const page = await browser.newPage();
await page.goto('https://news.ycombinator.com', {
waitUntil: 'networkidle2',
});
// 保存为 hn.pdf
await page.pdf({
path: 'hn.pdf',
});
await browser.close();
默认生成的 PDF 是 A4 大小,包含背景图形。字体加载会自动等待,确保文字渲染正确。
PDF 格式控制
可以通过参数精确控制 PDF 的页面布局。支持自定义尺寸、边距、方向等。
await page.pdf({
path: 'custom.pdf',
format: 'A4', // 或 'Letter', 'Legal' 等
width: '8.5in',
height: '11in',
landscape: false, // 横向或纵向
scale: 1, // 缩放比例
printBackground: true, // 打印背景图形
margin: {
top: '1in',
right: '1in',
bottom: '1in',
left: '1in',
},
});
format 和 width/height 二选一即可。如果需要非标准尺寸,直接用 width 和 height 更灵活。单位支持 in、cm、mm、px。
分页与范围控制
对于长页面,可以控制打印的页码范围。这在只需要部分内容时很有用。
await page.pdf({
path: 'partial.pdf',
pageRanges: '1-3,5', // 打印第 1-3 页和第 5 页
});
页码从 1 开始。这个参数遵循打印对话框的语法,支持连续范围和离散页码的组合。
页眉页脚定制
Puppeteer 支持在 PDF 中添加页眉和页脚,可以插入页码、日期、标题等信息。
await page.pdf({
path: 'with-header.pdf',
displayHeaderFooter: true,
headerTemplate: `
<div style="font-size: 10px; width: 100%; text-align: center;">
<span>文档标题</span>
</div>
`,
footerTemplate: `
<div style="font-size: 10px; width: 100%; text-align: center;">
<span>第 <span class="pageNumber"></span> 页,共 <span class="totalPages"></span> 页</span>
</div>
`,
margin: {
top: '80px', // 为页眉留出空间
bottom: '80px', // 为页脚留出空间
},
});
模板中可以使用特殊的 CSS 类:pageNumber、totalPages、date、title、url。这些会被 Puppeteer 自动替换为实际值。注意页眉页脚使用的字体和样式需要内联在 HTML 中,外部样式表不会生效。
PDF 生成注意事项
PDF 生成有一些需要注意的细节。首先是字体问题:Puppeteer 默认会等待网页字体加载完成,但如果使用了网络字体,确保网络稳定。
其次是页面内容的分页。CSS 的 page-break-after、page-break-before 属性可以控制元素是否在新页面开始。对于不希望被分割的元素,可以使用 page-break-inside: avoid。
/* 避免表格跨页 */
table {
page-break-inside: avoid;
}
/* 强制在新页面开始 */
.section {
page-break-before: always;
}
最后是性能。生成大型 PDF 可能消耗较多内存,建议在服务器环境中监控资源使用,必要时限制并发数。
处理文件上传下载
文件操作是自动化中的常见需求。Puppeteer 对上传提供了原生支持,但下载处理需要一些技巧。
文件上传
文件上传通过文件输入元素实现。定位到 <input type="file"> 元素,调用 uploadFile 方法即可。
const fileElement = await page.waitForSelector('input[type=file]');
await fileElement.uploadFile(['./path-to-local-file.pdf']);
uploadFile 接受文件路径数组,支持多文件上传。路径可以是相对路径或绝对路径。这个方法会模拟用户选择文件的行为,触发浏览器的 change 事件。
对于动态创建的文件上传场景,可以先在本地生成临时文件,上传后再清理。
import fs from 'fs/promises';
import path from 'path';
import os from 'os';
// 创建临时文件
const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'upload-'));
const tempFile = path.join(tempDir, 'test.txt');
await fs.writeFile(tempFile, 'test content');
// 上传
const input = await page.$('input[type=file]');
await input.uploadFile([tempFile]);
// 清理
await fs.unlink(tempFile);
await fs.rmdir(tempDir);
处理文件选择对话框
有些上传场景会触发系统的文件选择对话框。Puppeteer 可以拦截这个对话框,直接处理文件上传。
// 监听文件选择事件
page.on('filechooser', async (fileChooser) => {
await fileChooser.accept(['/path/to/file.pdf']);
});
// 点击触发上传的按钮
await page.click('#upload-button');
这种方式更贴近真实用户操作,避免了直接操作 DOM 元素可能带来的问题。
文件下载的挑战
Puppeteer 目前没有提供直接处理文件下载的 API。当点击下载链接时,浏览器会触发下载,但 Puppeteer 无法直接获取文件内容。这是因为下载由浏览器内核直接处理,不经过 JavaScript 上下文。
不过有几种变通方案可以实现下载功能。
方案一:拦截请求并获取响应
对于已知 URL 的下载链接,可以直接拦截请求,获取响应体。
await page.setRequestInterception(true);
page.on('request', (request) => {
if (request.url().includes('/download')) {
// 继续请求
request.continue();
} else {
request.continue();
}
});
page.on('response', async (response) => {
if (response.url().includes('/download')) {
const buffer = await response.buffer();
// 保存文件
await fs.writeFile('downloaded.pdf', buffer);
}
});
await page.click('#download-link');
这个方法的局限是只能处理通过 HTTP 请求直接获取的文件。对于通过 JavaScript 动态生成或需要特殊认证的文件,可能需要更复杂的处理。
方案二:修改下载行为
可以通过 Chrome DevTools Protocol 修改浏览器的下载行为,让下载不弹出保存对话框,而是保存到指定目录。
const client = await page.target().createCDPSession();
await client.send('Page.setDownloadBehavior', {
behavior: 'allow',
downloadPath: '/path/to/downloads',
});
这种方法需要启用 CDP 会话,并且只能在无头模式下可靠工作。下载完成后,需要监听文件系统变化来获取文件。
方案三:直接请求资源
最简单的方案是绕过点击操作,直接用 page.evaluate 获取下载链接,然后使用 Node.js 的 HTTP 客户端下载。
const downloadUrl = await page.evaluate(() => {
return document.querySelector('#download-link').href;
});
// 使用 fetch 或 axios 下载
const response = await fetch(downloadUrl);
const buffer = await response.buffer();
await fs.writeFile('file.pdf', buffer);
这种方法最可靠,但缺点是无法模拟真实的用户点击行为,可能绕过某些前端逻辑。
媒体与音频播放支持
Puppeteer 可以控制页面中的媒体元素,但本身不处理媒体文件的编解码。这部分主要涉及如何与页面中的音视频元素交互。
控制视频播放
可以通过 JavaScript 操作页面中的 <video> 元素。播放、暂停、跳转等操作都可以实现。
// 播放视频
await page.evaluate(() => {
const video = document.querySelector('video');
if (video) {
video.play();
}
});
// 等待视频加载
await page.waitForFunction(() => {
const video = document.querySelector('video');
return video && video.readyState >= 3; // HAVE_FUTURE_DATA
});
// 截图视频画面
const frame = await page.$('video');
await frame.screenshot({ path: 'video-frame.png' });
视频截图是个实用技巧。可以捕获视频的某一帧作为预览图。注意要等待视频元数据加载完成,否则可能截取到空白画面。
音频播放处理
音频元素的处理类似视频。可以控制播放状态,但无法直接获取音频数据。
// 播放音频
await page.evaluate(() => {
const audio = document.querySelector('audio');
if (audio) {
audio.play();
}
});
// 监听音频事件
page.evaluateOnNewDocument(() => {
window.addEventListener('audio-play', () => {
console.log('Audio started playing');
});
});
媒体权限处理
某些媒体功能需要用户授权,比如麦克风、摄像头。Puppeteer 可以模拟授权。
const context = browser.defaultBrowserContext();
await context.overridePermissions('https://example.com', ['microphone', 'camera']);
这在测试 WebRTC 应用时很有用。可以自动授予权限,避免手动点击授权对话框。
媒体流捕获
Puppeteer 可以捕获页面的媒体流,但这需要借助 Chrome 的实验性特性。可以通过启动参数启用。
const browser = await puppeteer.launch({
args: [
'--enable-experimental-web-platform-features',
'--enable-features=MediaCaptureFromCanvas',
],
});
然后可以使用 getDisplayMedia 或 getUserMedia API 捕获页面内容。不过这个功能还在演进中,不同版本的 Chrome 支持程度不同。
性能考虑
媒体处理通常比较消耗资源。在服务器环境中运行大量媒体相关的自动化任务时,需要注意内存和 CPU 使用。无头模式下的媒体解码可能依赖软件渲染,性能不如有头模式。
建议在长时间运行的媒体任务中,定期重启浏览器实例,释放资源。同时监控系统的资源使用情况,设置合理的超时时间。
总结与展望
本章介绍了 Puppeteer 在截图、PDF 生成、文件处理和媒体支持方面的核心能力。截图功能灵活强大,支持全页面和元素级捕获;PDF 生成质量高,适合打印和归档;文件上传有原生支持,下载需要变通方案;媒体控制可以模拟用户交互,但处理本身受浏览器能力限制。
这些功能组合起来,可以构建强大的自动化工作流。比如定期截取网站状态、生成报告 PDF、批量上传文件测试、监控媒体播放质量等。
下一章将探讨无头模式与性能优化。我们会深入研究不同无头模式的区别,如何配置动态分辨率,以及窗口管理的最佳实践。性能优化部分会分享减少资源消耗、提升执行速度的具体技巧,让 Puppeteer 在生产环境中运行得更稳定高效。