Zzzxb's Blog

你要静心学习那份等待时机的成熟的情绪,也要你一定保有这份等待之外的努力和坚持。

无头浏览器截图 Feb 27, 2026

1. Chromium / Chrome Headless 是什么?

2. Puppeteer 是什么?

Puppeteer 和 Chromium Headless 的关系

你可以把这两者理解为 “控制器”“被控制的工具” 的关系:

简单比喻:

Puppeteer 能做什么?

因为它直接控制了浏览器,所以几乎能在浏览器里手动做的事情,都能用 Puppeteer 自动化完成:

  1. 网页截图和生成PDF:自动化地将整个网页或特定元素保存为图片或PDF文件。
  2. 自动化测试:模拟用户交互(点击、输入、滚动、提交表单),用于测试单页应用(SPA)和现代网页功能。
  3. 网页爬虫:抓取动态渲染的网页内容。与传统的 HTTP 请求库(如 axios)相比,它能处理需要 JavaScript 执行后才能看到的数据,是爬取 React、Vue 等框架构建的网站的强大工具。
  4. 性能分析和监控:获取网页的时间线轨迹、计算资源消耗、监测加载性能等。
  5. 自动化表单提交UI自动化定时任务等。
  6. 测试浏览器扩展功能

使用 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维护) 非常活跃(增长最快) 极其成熟(最庞大) 作为基础能力被其他工具使用

如何选择?

  1. 如果你只需要控制Chrome/Chromium,做截图、简单爬虫或基础自动化 → Puppeteer 优势:专精Chrome,API简洁,Google官方维护
  2. 如果你需要测试现代Web应用,关注跨浏览器兼容,或开始新项目Playwright 优势:功能最强,开发体验好,多浏览器原生支持,是当前的技术趋势
  3. 如果你在企业环境中,需要多语言支持,或维护已有Selenium项目Selenium 优势:行业标准,生态成熟,云服务支持好,团队协作成本低
  4. 如果你只需要一个无界面的浏览器环境,不关心高级控制直接使用Chromium Headless 优势:最轻量,但需要自己处理通信协议

重要补充说明

发展趋势

目前 Playwright 凭借其优秀的跨浏览器支持、强大的自动等待机制和良好的开发体验,正在成为新项目的主流选择。而 Selenium 凭借其庞大的现有用户基础和全面的生态系统,在企业级市场依然稳固。Puppeteer 则在专注于Chrome生态的场景中保持其简洁高效的优势。