无头浏览器截图 Feb 27, 2026
- 1. Chromium / Chrome Headless 是什么?
- 2. Puppeteer 是什么?
- Puppeteer 和 Chromium Headless 的关系
- Puppeteer 能做什么?
- 使用 Safari 命令行(最简单)
1. Chromium / Chrome Headless 是什么?
- Chromium 是 Google 主导开发的开源网页浏览器项目,是 Chrome 浏览器的基础。
- Headless(无头模式) 指的是在没有图形用户界面(没有窗口、没有菜单栏等)的情况下运行浏览器。它就像一个“隐形”的浏览器,可以执行所有常规浏览器的操作(加载页面、运行JavaScript、渲染CSS等),但你看不到它。
- 作用:这使得浏览器可以完全通过命令行或程序脚本来控制,非常适合在服务器、测试环境或自动化脚本中使用,因为它节省资源且可以在没有显示设备的系统上运行。
2. Puppeteer 是什么?
- Puppeteer 是一个由 Google Chrome 团队 开发和维护的 Node.js 库。
- 核心功能:它提供了一个高级的 API,通过 DevTools 协议 来控制和与 Chrome 或 Chromium(通常是 Headless 模式) 浏览器进行通信。
- 名字的由来:Puppeteer 意为“操纵木偶的人”,它让你像操纵木偶一样,通过代码来操纵浏览器。
Puppeteer 和 Chromium Headless 的关系
你可以把这两者理解为 “控制器” 和 “被控制的工具” 的关系:
- Puppeteer 是“控制器”/“驱动程序”:它是一套用 JavaScript 写好的命令和接口。你编写 Puppeteer 脚本(Node.js 程序),来发出指令。
- Chromium Headless 是“被控制的工具”:它是一个实际执行操作(导航、点击、渲染)的浏览器引擎。Puppeteer 脚本会启动一个 Chromium/Chrome 进程(通常是 Headless 模式),并通过协议向其发送指令。
- 默认捆绑:为了简化使用,Puppeteer 在安装时会自动下载一个与它版本兼容的 Chromium 版本(这是开源的版本)。你也可以通过配置让它连接到你系统上已安装的完整版 Chrome 浏览器。
简单比喻:
- Chromium Headless 就像一辆没有方向盘的汽车引擎,它有动力,但无法直接操控。
- Puppeteer 就像你为这辆引擎加装的一套完整的遥控系统(包括方向盘、油门、刹车的遥控器)。你通过遥控系统(Puppeteer API)来精确控制引擎(Chromium)的行为。
Puppeteer 能做什么?
因为它直接控制了浏览器,所以几乎能在浏览器里手动做的事情,都能用 Puppeteer 自动化完成:
- 网页截图和生成PDF:自动化地将整个网页或特定元素保存为图片或PDF文件。
- 自动化测试:模拟用户交互(点击、输入、滚动、提交表单),用于测试单页应用(SPA)和现代网页功能。
- 网页爬虫:抓取动态渲染的网页内容。与传统的 HTTP 请求库(如 axios)相比,它能处理需要 JavaScript 执行后才能看到的数据,是爬取 React、Vue 等框架构建的网站的强大工具。
- 性能分析和监控:获取网页的时间线轨迹、计算资源消耗、监测加载性能等。
- 自动化表单提交、UI自动化、定时任务等。
- 测试浏览器扩展功能
使用 Safari 命令行(最简单)
# 使用 Safari 的 screenshot 命令
/usr/bin/screencapture screenshot.png
# 或者使用自动化工具
screencapture -i screenshot.png # 交互式选择区域
screencapture -R 0,0,800,600 screenshot.png # 指定区域
使用 Chrome/Chromium headless
# 如果你有 Chrome
/Applications/Google\ Chrome.app/Contents/MacOS/Google\ Chrome \
--headless --disable-gpu --screenshot=screenshot.png https://example.com
# 或者使用 chromium(brew 安装)
brew install chromium
chromium --headless --disable-gpu --screenshot=screenshot.png https://example.com
使用 puppeteer(Node.js)
# 创建 screenshot.js
cat > screenshot.js << 'EOF'
const puppeteer = require('puppeteer');
(async () => {
const browser = await puppeteer.launch();
const page = await browser.newPage();
await page.setViewport({ width: 1920, height: 1080 });
await page.goto('https://example.com');
await page.screenshot({ path: 'screenshot.png' });
await
puppeteer 高级版(Node.js)
const puppeteer = require('puppeteer');
const fs = require('fs');
const path = require('path');
// ====================== 可配置参数 ======================
const CONFIG = {
// 截图相关配置
screenshot: {
width: 1920, // 截图宽度(像素)
height: 800, // 截图高度(像素)
type: 'png', // 截图格式:png 或 jpeg
quality: undefined, // jpeg格式的质量,0-100,png格式请设为undefined
},
// 目标元素位置配置
elementPosition: {
topMargin: 120, // 元素距离截图顶部的边距(像素)
// 目标位置模式:'top'=最上方,'center'=居中,'custom'=自定义位置
mode: 'top',
// 如果mode为'custom',这里指定百分比(0=顶部,100=底部)
customPositionPercent: 10,
},
// 浏览器配置
browser: {
headless: 'new', // 无头模式:'new'或true为无头,false为有界面
windowWidth: 1920, // 浏览器窗口宽度
windowHeight: 800, // 浏览器窗口高度
userAgent: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
},
// 搜索配置
search: {
url: 'https://www.baidu.com/s?wd=通天购',
targetText: '从巨舰到米粒,万物皆可购,有坑我来避。我是「通天购」,您的专属消费穹顶——让每次选择都清晰自信。',
},
// 输出配置
output: {
directory: 'baidu_screenshots', // 输出目录
filenamePrefix: '顶部截图', // 文件名前缀
saveLogFile: true, // 是否保存日志文件
saveFullPageScreenshot: false, // 是否保存完整页面截图(用于参考)
},
// 行为配置
behavior: {
waitAfterLoad: 2000, // 页面加载后等待时间(毫秒)
waitAfterScroll: 1000, // 滚动后等待时间(毫秒)
highlightElement: true, // 是否高亮目标元素
highlightColor: 'red', // 高亮颜色:red, blue, green, yellow
addMarkerLabel: true, // 是否添加标记标签
timeout: 30000, // 页面加载超时时间(毫秒)
},
};
// ====================== 主程序 ======================
(async () => {
// 创建输出目录
fs.mkdirSync(CONFIG.output.directory, { recursive: true });
// 初始化浏览器
const browser = await puppeteer.launch({
headless: CONFIG.browser.headless,
args: [
'--no-sandbox',
'--disable-setuid-sandbox',
'--disable-blink-features=AutomationControlled',
`--window-size=${CONFIG.browser.windowWidth},${CONFIG.browser.windowHeight}`
]
});
const page = await browser.newPage();
// 设置浏览器参数
await page.setUserAgent(CONFIG.browser.userAgent);
await page.setViewport({
width: CONFIG.browser.windowWidth,
height: CONFIG.browser.windowHeight
});
console.log(`🌐 访问: ${CONFIG.search.url}`);
console.log(`🔍 查找: "${CONFIG.search.targetText.substring(0, 30)}..."`);
console.log(`🎯 目标位置: ${CONFIG.elementPosition.mode}模式`);
try {
// 访问页面
await page.goto(CONFIG.search.url, {
waitUntil: 'networkidle2',
timeout: CONFIG.behavior.timeout
});
console.log('✅ 页面加载完成');
// 等待页面完全渲染
await new Promise(resolve => setTimeout(resolve, CONFIG.behavior.waitAfterLoad));
// 查找目标元素并获取其精确位置
const elementInfo = await page.evaluate((searchText) => {
// 查找包含文本的元素
const walker = document.createTreeWalker(
document.body,
NodeFilter.SHOW_TEXT,
null,
false
);
let targetElement = null;
let node;
while (node = walker.nextNode()) {
if (node.textContent.includes(searchText)) {
targetElement = node.parentElement;
break;
}
}
if (!targetElement) {
// 备用查找方法
const allElements = document.querySelectorAll('*');
for (const element of allElements) {
if (element.textContent && element.textContent.includes(searchText)) {
targetElement = element;
break;
}
}
}
if (targetElement) {
// 获取元素位置信息
const rect = targetElement.getBoundingClientRect();
const scrollY = window.scrollY;
return {
found: true,
top: rect.top + scrollY,
left: rect.left,
width: rect.width,
height: rect.height,
elementTop: rect.top + scrollY,
elementText: targetElement.textContent.substring(0, 100),
tagName: targetElement.tagName,
className: targetElement.className,
fullText: targetElement.textContent
};
}
return { found: false };
}, CONFIG.search.targetText);
if (!elementInfo.found) {
console.log('❌ 未找到目标文本');
await browser.close();
return;
}
console.log('\n🎯 找到目标元素信息:');
console.log(` 元素顶部位置: Y=${Math.round(elementInfo.elementTop)}px`);
console.log(` 元素尺寸: ${Math.round(elementInfo.width)}×${Math.round(elementInfo.height)}px`);
console.log(` 元素类型: ${elementInfo.tagName}`);
console.log(` 类名: ${elementInfo.className || '(无)'}`);
console.log(` 示例文本: ${elementInfo.elementText}...`);
// 根据配置模式计算滚动位置
let scrollToY;
switch (CONFIG.elementPosition.mode) {
case 'top':
// 顶部模式:元素距截图顶部指定边距
scrollToY = Math.max(0, elementInfo.elementTop - CONFIG.elementPosition.topMargin);
console.log(`📐 模式: 顶部 (距顶部${CONFIG.elementPosition.topMargin}px)`);
break;
case 'center':
// 居中模式:元素位于截图中央
const centerOffset = (CONFIG.screenshot.height / 2) - (elementInfo.height / 2);
scrollToY = Math.max(0, elementInfo.elementTop - centerOffset);
console.log(`📐 模式: 居中`);
break;
case 'custom':
// 自定义模式:元素位于指定百分比位置
const customPixelPosition = (CONFIG.screenshot.height * CONFIG.elementPosition.customPositionPercent) / 100;
scrollToY = Math.max(0, elementInfo.elementTop - customPixelPosition + CONFIG.elementPosition.topMargin);
console.log(`📐 模式: 自定义 (${CONFIG.elementPosition.customPositionPercent}%位置)`);
break;
default:
scrollToY = Math.max(0, elementInfo.elementTop - CONFIG.elementPosition.topMargin);
console.log(`📐 模式: 默认顶部`);
}
console.log(`\n📐 计算截图参数:`);
console.log(` 元素顶部Y: ${Math.round(elementInfo.elementTop)}px`);
console.log(` 目标截图高度: ${CONFIG.screenshot.height}px`);
console.log(` 滚动到位置: ${Math.round(scrollToY)}px`);
console.log(` 截图区域: Y=${Math.round(scrollToY)} 到 Y=${Math.round(scrollToY + CONFIG.screenshot.height)}`);
// 滚动到计算位置
await page.evaluate((y) => {
window.scrollTo({ top: y, behavior: 'smooth' });
}, scrollToY);
// 等待滚动完成
await new Promise(resolve => setTimeout(resolve, CONFIG.behavior.waitAfterScroll));
// 获取当前滚动位置(验证)
const currentScrollY = await page.evaluate(() => window.scrollY);
console.log(` 实际滚动位置: ${Math.round(currentScrollY)}px`);
// 获取当前视口内元素位置(验证)
const elementInViewport = await page.evaluate((searchText) => {
const allElements = document.querySelectorAll('*');
for (const element of allElements) {
if (element.textContent && element.textContent.includes(searchText)) {
const rect = element.getBoundingClientRect();
return {
top: rect.top,
bottom: rect.bottom,
height: rect.height
};
}
}
return null;
}, CONFIG.search.targetText);
if (elementInViewport) {
const percentPosition = (elementInViewport.top / CONFIG.screenshot.height * 100).toFixed(1);
console.log(` 元素在截图内位置: 顶部${Math.round(elementInViewport.top)}px (${percentPosition}%)`);
}
// 高亮目标元素(如果启用)
if (CONFIG.behavior.highlightElement) {
await page.evaluate(({searchText, highlightColor, addMarkerLabel}) => {
const allElements = document.querySelectorAll('*');
for (const element of allElements) {
if (element.textContent && element.textContent.includes(searchText)) {
// 设置高亮颜色
const colorMap = {
red: '255, 0, 0',
blue: '0, 0, 255',
green: '0, 255, 0',
yellow: '255, 255, 0'
};
const rgb = colorMap[highlightColor] || '255, 0, 0';
element.style.cssText += `; border: 3px solid ${highlightColor} !important; background-color: rgba(${rgb}, 0.1) !important; padding: 5px !important;`;
// 添加标记标签(如果启用)
if (addMarkerLabel) {
const marker = document.createElement('div');
marker.style.cssText = 'position: absolute; left: 0; top: -25px; background: red; color: white; padding: 2px 8px; font-size: 12px; border-radius: 3px; z-index: 10000;';
marker.textContent = '目标文本';
marker.id = 'target-marker';
try {
element.parentNode.insertBefore(marker, element);
} catch (e) {
document.body.appendChild(marker);
}
}
break;
}
}
}, {
searchText: CONFIG.search.targetText,
highlightColor: CONFIG.behavior.highlightColor,
addMarkerLabel: CONFIG.behavior.addMarkerLabel
});
// 等待高亮显示
await new Promise(resolve => setTimeout(resolve, 500));
}
// 生成时间戳和文件名
const timestamp = new Date().toISOString()
.replace(/[:.]/g, '-')
.replace('T', '_')
.substring(0, 19);
const screenshotPath = path.join(
CONFIG.output.directory,
`${CONFIG.output.filenamePrefix}_${timestamp}.${CONFIG.screenshot.type}`
);
console.log(`\n📸 正在截图: ${CONFIG.screenshot.width} × ${CONFIG.screenshot.height} (${CONFIG.screenshot.type})`);
// 截图参数
const screenshotOptions = {
path: screenshotPath,
clip: {
x: 0,
y: currentScrollY,
width: CONFIG.screenshot.width,
height: CONFIG.screenshot.height
},
type: CONFIG.screenshot.type
};
// 如果格式是jpeg且设置了quality,添加quality参数
if (CONFIG.screenshot.type === 'jpeg' && CONFIG.screenshot.quality !== undefined) {
screenshotOptions.quality = CONFIG.screenshot.quality;
}
try {
await page.screenshot(screenshotOptions);
console.log(`✅ 截图已保存: ${screenshotPath}`);
} catch (error) {
console.log(`截图失败: ${error.message},尝试备用方法...`);
// 备用方法:调整浏览器窗口大小后截图
await page.setViewport({
width: CONFIG.screenshot.width,
height: CONFIG.screenshot.height
});
await page.evaluate((y) => {
window.scrollTo(0, y);
}, scrollToY);
await new Promise(resolve => setTimeout(resolve, 500));
// 重新准备备用截图参数
const fallbackOptions = {
path: screenshotPath,
type: CONFIG.screenshot.type
};
if (CONFIG.screenshot.type === 'jpeg' && CONFIG.screenshot.quality !== undefined) {
fallbackOptions.quality = CONFIG.screenshot.quality;
}
await page.screenshot(fallbackOptions);
console.log(`✅ 备用截图已保存: ${screenshotPath}`);
// 恢复视口
await page.setViewport({
width: CONFIG.browser.windowWidth,
height: CONFIG.browser.windowHeight
});
}
// 保存完整页面截图(如果启用)
if (CONFIG.output.saveFullPageScreenshot) {
const fullScreenshotPath = path.join(
CONFIG.output.directory,
`全页参考_${timestamp}.${CONFIG.screenshot.type}`
);
const fullPageOptions = {
path: fullScreenshotPath,
fullPage: true,
type: CONFIG.screenshot.type
};
if (CONFIG.screenshot.type === 'jpeg' && CONFIG.screenshot.quality !== undefined) {
fullPageOptions.quality = CONFIG.screenshot.quality;
}
await page.screenshot(fullPageOptions);
console.log(`📄 全页参考图: ${fullScreenshotPath}`);
}
// 验证截图文件
try {
const stats = fs.statSync(screenshotPath);
const fileSizeKB = (stats.size / 1024).toFixed(0);
console.log(`📊 文件大小: ${fileSizeKB} KB`);
console.log(`📁 完整路径: ${path.resolve(screenshotPath)}`);
} catch (e) {
console.log('⚠️ 无法验证文件');
}
// 保存日志文件(如果启用)
if (CONFIG.output.saveLogFile) {
const logPath = path.join(CONFIG.output.directory, `截图信息_${timestamp}.txt`);
const logContent = `
截图信息 - ${new Date().toISOString()}
========================================
配置信息:
截图尺寸: ${CONFIG.screenshot.width} × ${CONFIG.screenshot.height}
目标位置模式: ${CONFIG.elementPosition.mode}
顶部边距: ${CONFIG.elementPosition.topMargin}px
----------------------------------------
搜索信息:
URL: ${CONFIG.search.url}
目标文本: ${CONFIG.search.targetText.substring(0, 50)}...
----------------------------------------
元素信息:
元素顶部: ${Math.round(elementInfo.elementTop)}px
元素类型: ${elementInfo.tagName}
类名: ${elementInfo.className || '(无)'}
完整文本: ${elementInfo.fullText}
----------------------------------------
执行结果:
计算滚动位置: ${Math.round(scrollToY)}px
实际滚动位置: ${Math.round(currentScrollY)}px
截图内元素位置: ${elementInViewport ? `${Math.round(elementInViewport.top)}px` : '未知'}
截图文件: ${screenshotPath}
----------------------------------------
`;
fs.writeFileSync(logPath, logContent);
console.log(`📝 日志文件已保存: ${logPath}`);
}
console.log(`\n🎯 截图完成:`);
console.log(` 目标: ${CONFIG.output.filenamePrefix}`);
console.log(` 尺寸: ${CONFIG.screenshot.width} × ${CONFIG.screenshot.height}`);
console.log(` 格式: ${CONFIG.screenshot.type}`);
console.log(` 高亮: ${CONFIG.behavior.highlightElement ? '是' : '否'}`);
} catch (error) {
console.error('❌ 发生错误:', error.message);
console.error('堆栈:', error.stack);
} finally {
await browser.close();
console.log('\n👋 任务完成');
}
})();
总结
| 特性维度 | Puppeteer | Playwright | Selenium | Chromium Headless |
|---|---|---|---|---|
| 本质 | Node.js 库/API | 多语言框架 | 跨平台框架 | 浏览器的运行模式 |
| 开发者 | Google Chrome 团队 | 微软(原Puppeteer团队) | 开源社区(Apache 2.0) | Chromium项目 |
| 诞生背景 | 为 Chrome 自动化而生 | Puppeteer团队为多浏览器和更强功能而创建 | 最早的Web自动化标准之一 | Chrome 59+ 引入的无界面模式 |
| 支持浏览器 | Chrome/Chromium为主(可通过插件有限支持Firefox) | Chromium、Firefox、WebKit(三核原生支持) | 几乎全部:Chrome, Firefox, Safari, Edge, IE等 | 仅限 Chromium/Chrome |
| 支持语言 | Node.js (JavaScript/TypeScript) | 多语言:JS/TS, Python, Java, .NET | 全语言:Java, Python, C#, JS, Ruby等 | 不直接提供API,需通过其他工具控制 |
| 架构特点 | 直接通过DevTools协议控制Chrome | 为每个浏览器内核深度优化,提供统一API | WebDriver标准 + 各浏览器驱动 | 浏览器本身的运行状态 |
| 安装配置 | 简单(自带Chromium) | 简单(一键安装多浏览器) | 复杂(需单独配驱动和浏览器) | 作为浏览器功能存在 |
| 执行速度 | 快(协议直连) | 快(优化多核) | 较慢(WebDriver协议开销) | 取决于控制它的工具 |
| 功能特性 | - 完善的Chrome控制 - 截图/PDF - 请求拦截 | - 跨浏览器一致性 - 自动等待机制优秀 - 网络模拟强大 - 移动端模拟 - 录制代码生成 | - 行业标准 - 云测试平台集成好 - 企业级功能丰富 - 并行测试成熟 | - 无界面运行 - 节省资源 - 服务器友好 |
| Headless支持 | 默认Headless,可切换有界面 | 默认Headless,可切换有界面 | 需额外配置参数 | 就是它本身 |
| 主要场景 | - 单浏览器自动化 - 网页截图/PDF - 简单的爬虫和测试 | - 现代Web应用测试 - 跨浏览器测试 - E2E测试 - 复杂爬虫 | - 企业级测试 - 跨浏览器兼容测试 - 多语言团队 - 传统项目维护 | - 服务器端渲染 - 自动化任务 - 资源受限环境 |
| 社区生态 | 活跃(Google维护) | 非常活跃(增长最快) | 极其成熟(最庞大) | 作为基础能力被其他工具使用 |
如何选择?
- 如果你只需要控制Chrome/Chromium,做截图、简单爬虫或基础自动化 → Puppeteer 优势:专精Chrome,API简洁,Google官方维护
- 如果你需要测试现代Web应用,关注跨浏览器兼容,或开始新项目 → Playwright 优势:功能最强,开发体验好,多浏览器原生支持,是当前的技术趋势
- 如果你在企业环境中,需要多语言支持,或维护已有Selenium项目 → Selenium 优势:行业标准,生态成熟,云服务支持好,团队协作成本低
- 如果你只需要一个无界面的浏览器环境,不关心高级控制 → 直接使用Chromium Headless 优势:最轻量,但需要自己处理通信协议
重要补充说明
- Headless模式是所有现代浏览器的标配功能,不仅Chromium有,Firefox和Safari也都有自己的Headless模式。
- Puppeteer和Playwright都默认使用Headless模式,因为它们主要面向自动化场景。
- Selenium 4后也加强了对原生Chrome DevTools协议的支持,提高了与Chrome的通信效率。
- Playwright可视为Puppeteer的“全能升级版”,特别适合需要测试多浏览器或需要更稳定自动化的场景。
发展趋势
目前 Playwright 凭借其优秀的跨浏览器支持、强大的自动等待机制和良好的开发体验,正在成为新项目的主流选择。而 Selenium 凭借其庞大的现有用户基础和全面的生态系统,在企业级市场依然稳固。Puppeteer 则在专注于Chrome生态的场景中保持其简洁高效的优势。