强曰为道

与天地相似,故不违。知周乎万物,而道济天下,故不过。旁行而不流,乐天知命,故不忧.
文档目录

06 - Puppeteer 集成

06 - Puppeteer 集成

使用 Puppeteer 通过 CDP 协议直接控制 Chrome/Chromium,掌握高级操作与性能优化。


6.1 Puppeteer 简介

Puppeteer 是 Google Chrome 团队维护的 Node.js 库,通过 Chrome DevTools Protocol (CDP) 直接控制 Chrome 或 Chromium。

特征说明
维护方Google Chrome DevTools 团队
协议CDP (Chrome DevTools Protocol)
语言JavaScript / TypeScript
浏览器Chrome / Chromium (Firefox 实验性支持)
包大小~3MB (不含浏览器)
浏览器下载Puppeteer 自动下载匹配版本的 Chrome

架构

┌──────────────────────────────────────────┐
│            Node.js 应用                   │
│  ┌────────────────────────────────────┐  │
│  │        Puppeteer API               │  │
│  └──────────────┬─────────────────────┘  │
│                 │ WebSocket               │
│  ┌──────────────▼─────────────────────┐  │
│  │   Chrome DevTools Protocol (CDP)   │  │
│  └──────────────┬─────────────────────┘  │
│                 │                        │
│  ┌──────────────▼─────────────────────┐  │
│  │      Chrome / Chromium 进程        │  │
│  └────────────────────────────────────┘  │
└──────────────────────────────────────────┘

6.2 安装与启动

安装

# 创建项目
mkdir puppeteer-demo && cd puppeteer-demo
npm init -y

# 安装 Puppeteer (自动下载 Chrome)
npm install puppeteer

# 或安装 Puppeteer Core (不自动下载浏览器,需手动指定)
npm install puppeteer-core

第一个脚本

const puppeteer = require('puppeteer');

(async () => {
  // 启动浏览器
  const browser = await puppeteer.launch({
    headless: 'new',      // 新版无头模式
    args: ['--no-sandbox', '--disable-dev-shm-usage']
  });

  // 打开新页面
  const page = await browser.newPage();

  // 导航
  await page.goto('https://example.com', {
    waitUntil: 'networkidle2'  // 等待网络空闲
  });

  // 获取标题
  const title = await page.title();
  console.log(`标题: ${title}`);

  // 截图
  await page.screenshot({ path: '/tmp/example.png', fullPage: true });

  // 关闭浏览器
  await browser.close();
})();

6.3 Puppeteer 核心 API

6.3.1 浏览器启动选项

const browser = await puppeteer.launch({
  // 基础选项
  headless: 'new',              // true/false/'new'
  executablePath: '/usr/bin/google-chrome',  // 自定义 Chrome 路径
  slowMo: 100,                  // 每步延迟 (ms, 调试用)
  devtools: true,               // 自动打开 DevTools

  // 启动参数 (等同于 Chrome 启动参数)
  args: [
    '--no-sandbox',
    '--disable-dev-shm-usage',
    '--window-size=1920,1080',
    '--proxy-server=http://proxy:8080',
    '--disable-blink-features=AutomationControlled',
  ],

  // 默认视口
  defaultViewport: {
    width: 1920,
    height: 1080,
    deviceScaleFactor: 1,
    isMobile: false,
    hasTouch: false,
  },

  // 超时
  timeout: 30000,               // 启动超时 (ms)
  protocolTimeout: 60000,       // CDP 协议超时 (ms)
});

6.3.2 页面导航

const page = await browser.newPage();

// 基本导航
await page.goto('https://example.com');

// 带选项
await page.goto('https://example.com', {
  waitUntil: 'networkidle2',   // load / domcontentloaded / networkidle0 / networkidle2
  timeout: 30000,              // 导航超时
  referer: 'https://google.com' // Referer 头
});

// waitUntil 选项说明:
// 'load'            - load 事件触发
// 'domcontentloaded' - DOMContentLoaded 事件触发
// 'networkidle0'    - 0 个网络连接 (所有请求完成)
// 'networkidle2'    - ≤2 个网络连接 (推荐,SPA 友好)

// 刷新
await page.reload({ waitUntil: 'networkidle2' });

// 前进后退
await page.goBack();
await page.goForward();

6.3.3 元素定位与交互

// CSS 选择器
const button = await page.$('#submit-btn');          // 单个
const items = await page.$$('.list-item');            // 多个

// XPath
const [element] = await page.$$('//div[@id="app"]');

// 点击
await page.click('#submit-btn');
await page.click('.menu-item', { button: 'right' });   // 右键
await page.click('.item', { clickCount: 2 });           // 双击
await page.click('#btn', { delay: 100 });               // 延迟点击

// 输入文本
await page.type('#search', 'Hello World', { delay: 50 });

// 清空输入
await page.$eval('#search', el => el.value = '');

// 键盘操作
await page.keyboard.press('Enter');
await page.keyboard.press('Tab');
await page.keyboard.down('Shift');
await page.keyboard.press('KeyA');  // Shift+A
await page.keyboard.up('Shift');
await page.keyboard.type('Hello');

// 选择下拉框
await page.select('#country', 'CN');

// 上传文件
const input = await page.$('input[type="file"]');
await input.uploadFile('/path/to/file.pdf');

// 悬停
await page.hover('.dropdown-toggle');

// 等待元素
await page.waitForSelector('#content', { visible: true, timeout: 10000 });
await page.waitForSelector('.loading', { hidden: true });  // 等待消失
await page.waitForFunction('document.querySelectorAll(".item").length > 5');
await page.waitForNetworkIdle();

6.3.4 数据提取

// 获取文本内容
const text = await page.$eval('.title', el => el.textContent);
const texts = await page.$$eval('.item', els => els.map(el => el.textContent));

// 获取属性
const href = await page.$eval('a', el => el.href);
const src = await page.$eval('img', el => el.src);

// 获取 HTML
const html = await page.content();                              // 整个页面
const innerHTML = await page.$eval('#app', el => el.innerHTML); // 元素内部

// 执行 JavaScript
const result = await page.evaluate(() => {
  return {
    title: document.title,
    url: window.location.href,
    cookies: document.cookie,
    links: [...document.querySelectorAll('a')].map(a => a.href),
  };
});

// 传递参数给 evaluate
const data = await page.evaluate((selector) => {
  return [...document.querySelectorAll(selector)].map(el => ({
    text: el.textContent.trim(),
    href: el.href,
  }));
}, 'a.nav-link');

console.log(JSON.stringify(data, null, 2));

6.4 高级操作

6.4.1 请求拦截

await page.setRequestInterception(true);

page.on('request', (request) => {
  const url = request.url();

  // 阻止图片加载
  if (request.resourceType() === 'image') {
    request.abort();
    return;
  }

  // 阻止广告
  if (url.includes('doubleclick.net') || url.includes('google-analytics.com')) {
    request.abort();
    return;
  }

  // 修改请求头
  request.continue({
    headers: {
      ...request.headers(),
      'X-Custom-Header': 'value',
    }
  });
});

// 监听响应
page.on('response', async (response) => {
  const url = response.url();
  if (url.includes('/api/data')) {
    const data = await response.json();
    console.log('API 响应:', data);
  }
});

6.4.2 网络模拟

// 模拟离线
await page.setOfflineMode(true);

// 模拟慢速网络
await page.emulateNetworkConditions({
  offline: false,
  downloadThroughput: 1.5 * 1024 * 1024 / 8,  // 1.5 Mbps
  uploadThroughput: 750 * 1024 / 8,              // 750 Kbps
  latency: 100                                    // 100ms 延迟
});

6.4.3 设备模拟

const puppeteer = require('puppeteer');
const devices = puppeteer.devices;

// 使用预定义设备
await page.emulate(devices['iPhone 14 Pro']);
await page.emulate(devices['iPad Pro 11']);

// 自定义设备
await page.emulate({
  viewport: { width: 375, height: 812, isMobile: true, hasTouch: true },
  userAgent: 'Mozilla/5.0 (iPhone; CPU iPhone OS 16_0 like Mac OS X)...'
});

6.4.4 PDF 生成

await page.goto('https://example.com/report', { waitUntil: 'networkidle2' });

await page.pdf({
  path: '/tmp/report.pdf',
  format: 'A4',
  landscape: false,
  printBackground: true,
  margin: { top: '20mm', right: '15mm', bottom: '20mm', left: '15mm' },
  displayHeaderFooter: true,
  headerTemplate: '<div style="font-size:8px;width:100%;text-align:center">报告</div>',
  footerTemplate: '<div style="font-size:8px;width:100%;text-align:center">' +
                  '<span class="pageNumber"></span>/<span class="totalPages"></span></div>',
});

6.4.5 Trace 与性能分析

// 开始记录 Trace
await page.tracing.start({
  path: '/tmp/trace.json',
  screenshots: true,
  categories: ['devtools.timeline', 'v8.execute', 'blink.console']
});

// 执行操作
await page.goto('https://example.com');
await page.click('#button');

// 停止并保存
const traceBuffer = await page.tracing.stop();
console.log('Trace 保存到: /tmp/trace.json');
// 可在 chrome://tracing 中加载分析

// 性能指标
const metrics = await page.metrics();
console.log('JS 堆大小:', metrics.JSHeapUsedSize);
console.log('DOM 节点数:', metrics.Nodes);
console.log('布局次数:', metrics.LayoutCount);

6.5 CDP 高级操作

Puppeteer 可以直接发送 CDP 命令,实现更底层的控制。

// 获取 CDP Session
const client = await page.target().createCDPSession();

// 获取页面性能指标
const perf = await client.send('Performance.getMetrics');
console.log(perf.metrics);

// 拦截并修改响应体
await client.send('Fetch.enable', {
  patterns: [{ urlPattern: '*/api/*', requestStage: 'Response' }]
});

client.on('Fetch.requestPaused', async (event) => {
  const { requestId } = event;
  const response = await client.send('Fetch.getResponseBody', { requestId });
  const body = JSON.parse(response.body);
  body.modified = true;

  await client.send('Fetch.fulfillRequest', {
    requestId,
    responseCode: 200,
    body: Buffer.from(JSON.stringify(body)).toString('base64'),
  });
});

// 注入 CSS
await client.send('Page.addScriptToEvaluateOnNewDocument', {
  source: `
    const style = document.createElement('style');
    style.textContent = '* { border: 1px solid red !important; }';
    document.documentElement.prepend(style);
  `
});

6.6 连接已有的 Chrome 实例

// 方法 1: 连接到远程调试端口
// 先启动 Chrome: google-chrome --remote-debugging-port=9222
const browser = await puppeteer.connect({
  browserURL: 'http://127.0.0.1:9222',
  defaultViewport: null  // 使用浏览器实际视口
});

// 方法 2: 连接到 WebSocket
const browser = await puppeteer.connect({
  browserWSEndpoint: 'ws://127.0.0.1:9222/devtools/browser/xxxx'
});

// 方法 3: 复用浏览器上下文
const browser = await puppeteer.launch();
const context = browser.defaultBrowserContext();
await context.overridePermissions('https://example.com', ['notifications']);

// 使用后断开连接 (不关闭浏览器)
browser.disconnect();

6.7 Puppeteer vs Selenium

对比维度PuppeteerSelenium
协议CDP (WebSocket)WebDriver (HTTP)
语言JS / TS onlyJava, Python, C#, JS, Ruby
浏览器支持Chrome / Chromium onlyChrome, Firefox, Safari, Edge
安装npm install puppeteer (自动下载 Chrome)需手动安装驱动
学习曲线中等
API 风格async/await, Promise同步 / 异步
请求拦截✅ 原生支持❌ 需第三方库
设备模拟✅ 内置设备列表⚠️ 需手动配置
PDF 生成✅ 原生支持⚠️ 需 CDP 扩展
Trace/性能✅ 原生支持
自动等待❌ 需手动等待❌ 需手动等待
跨浏览器
生态成熟度中等最成熟
企业级支持社区Selenium Grid

选择建议

选择 Puppeteer:
  ✅ 只需控制 Chrome/Chromium
  ✅ Node.js / TypeScript 技术栈
  ✅ 需要请求拦截、Trace、PDF 等 CDP 高级能力
  ✅ 爬虫、截图、性能分析

选择 Selenium:
  ✅ 需要跨浏览器测试 (Chrome + Firefox + Safari)
  ✅ 企业级 E2E 测试
  ✅ 需要 Selenium Grid 分布式测试
  ✅ 非 JS 语言 (Java, Python, C#)

6.8 完整示例 — 动态页面爬虫

const puppeteer = require('puppeteer');

(async () => {
  const browser = await puppeteer.launch({
    headless: 'new',
    args: ['--no-sandbox', '--disable-dev-shm-usage']
  });

  const page = await browser.newPage();

  // 拦截图片和字体,加速加载
  await page.setRequestInterception(true);
  page.on('request', (req) => {
    if (['image', 'font', 'stylesheet'].includes(req.resourceType())) {
      req.abort();
    } else {
      req.continue();
    }
  });

  // 设置 UA
  await page.setUserAgent(
    'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 ' +
    '(KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36'
  );

  // 导航
  await page.goto('https://news.ycombinator.com', {
    waitUntil: 'networkidle2'
  });

  // 提取数据
  const articles = await page.$$eval('.titleline > a', (links) => {
    return links.slice(0, 10).map(a => ({
      title: a.textContent.trim(),
      url: a.href,
    }));
  });

  console.log('前 10 条新闻:');
  articles.forEach((a, i) => console.log(`${i + 1}. ${a.title} - ${a.url}`));

  await browser.close();
})();

6.9 要点回顾

要点说明
CDP 协议Puppeteer 通过 WebSocket 使用 CDP 直接控制 Chrome
async/await所有 API 都是异步的,使用 async/await 或 Promise
自动下载浏览器npm install puppeteer 自动下载匹配的 Chrome
请求拦截setRequestInterception(true) 实现请求拦截与修改
等待策略waitForSelector / waitForFunction / waitForNetworkIdle
puppeteer-core不自动下载浏览器,适合连接已有 Chrome 实例

6.10 注意事项

⚠️ Puppeteer 不等于 Playwright: 虽然都用 CDP,但 Playwright 支持多浏览器且有自动等待,Puppeteer 仅支持 Chrome。

⚠️ Node.js 版本要求: Puppeteer 21+ 需要 Node.js 18+。

⚠️ 浏览器版本锁定: 生产环境应锁定 puppeteer 版本以确保 Chrome 版本一致。

⚠️ 内存泄漏: 长时间运行时注意关闭不需要的 page 对象,避免内存泄漏。


6.11 扩展阅读

资源链接
Puppeteer 官方文档https://pptr.dev/
Puppeteer GitHubhttps://github.com/puppeteer/puppeteer
Chrome DevTools Protocolhttps://chromedevtools.github.io/devtools-protocol/
CDP 参考 (JSON 格式)https://vanilla.aslushnikov.com/
Puppeteer 设备列表https://github.com/puppeteer/puppeteer/blob/main/packages/puppeteer-core/src/common/Device.ts