{T}

Puppeteer的实现

本节深入 Puppeteer 的实现,分析其自动下载、启动 Chrome 以及通过 CDP 控制浏览器的机制。

2024-2026 更新:Puppeteer v23+ 默认使用 Pipe 模式连接,v24 延续该默认;puppeteer-corepuppeteer 的区分更明确,且 v24 移除了 Browser.isConnected() 方法(改用 browser.connected 属性)。

Puppeteer 的模块结构

图表渲染中…

puppeteer vs puppeteer-core

包名说明自动下载 Chrome适用场景
puppeteer完整版✅ 是通用自动化
puppeteer-core精简版❌ 否使用已有 Chrome 安装
javascript
// puppeteer:自动下载 Chrome
const puppeteer = require('puppeteer');
const browser = await puppeteer.launch();  // 使用自动下载的 Chrome

// puppeteer-core:使用已有的 Chrome
const puppeteerCore = require('puppeteer-core');
const browser = await puppeteerCore.launch({
    executablePath: '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome',
});

Chrome 的自动下载

下载逻辑

npm install puppeteer 时,postinstall 脚本会自动下载对应版本的 Chrome:

图表渲染中…

Chrome 版本映射

Puppeteer 的每个版本都对应一个特定的 Chrome 版本:

javascript
// puppeteer/revisions.js
module.exports = {
    chrome: '131.0.6778.204',  // Puppeteer 对应的 Chrome 版本
};

缓存位置

平台缓存路径
macOS~/Library/Caches/puppeteer/
Linux~/.cache/puppeteer/
Windows%LOCALAPPDATA%\puppeteer\

跳过自动下载

bash
# 设置环境变量跳过下载
PUPPETEER_SKIP_DOWNLOAD=true npm install puppeteer

# 或在 .npmrc 中配置
puppeteer_skip_download=true

2024-2026 更新:Puppeteer 现在支持 PUPPETEER_CACHE_DIR 环境变量自定义缓存目录。

Chrome 的启动过程

launch() 方法的实现

javascript
// puppeteer-core/lib/cjs/puppeteer/node/BrowserLauncher.js
async launch(options = {}) {
    // 1. 解析 Chrome 路径
    const executablePath = options.executablePath || this.executablePath;

    // 2. 构建启动参数
    const chromeArguments = [];
    if (!options.ignoreDefaultArgs) {
        chromeArguments.push(...this.defaultArgs(options));
    }
    if (options.args) {
        chromeArguments.push(...options.args);
    }

    // 3. 启动 Chrome 子进程
    // 使用 --remote-debugging-pipe(默认)或 --remote-debugging-port
    const usePipe = options.pipe !== false;  // v23+ 默认使用 Pipe,v24 延续

    if (usePipe) {
        chromeArguments.push('--remote-debugging-pipe');
        const { 3: pipeWrite, 4: pipeRead } = spawnChrome(executablePath, chromeArguments);
        // 连接 Pipe
        const connection = new PipeConnection(pipeRead, pipeWrite);
        return Browser.create(connection, ...);
    } else {
        chromeArguments.push(`--remote-debugging-port=${port}`);
        const process = spawnChrome(executablePath, chromeArguments);
        // 等待调试端口就绪
        const connection = await WebSocketConnection.create(port);
        return Browser.create(connection, ...);
    }
}

默认启动参数

Puppeteer 启动 Chrome 时会添加大量默认参数,确保浏览器以"干净"的状态运行:

javascript
defaultArgs(options = {}) {
    const args = [
        '--disable-background-networking',
        '--disable-background-timer-throttling',
        '--disable-backgrounding-occluded-windows',
        '--disable-breakpad',
        '--disable-component-extensions-with-background-pages',
        '--disable-component-update',
        '--disable-default-apps',
        '--disable-dev-shm-usage',
        '--disable-extensions',
        '--disable-features=TranslateUI',
        '--disable-hang-monitor',
        '--disable-ipc-flooding-protection',
        '--disable-popup-blocking',
        '--disable-prompt-on-repost',
        '--disable-renderer-backgrounding',
        '--disable-sync',
        '--enable-features=NetworkService,NetworkServiceInProcess',
        '--force-color-profile=srgb',
        '--metrics-recording-only',
        '--no-first-run',
        '--no-default-browser-check',
        '--password-store=basic',
        '--use-mock-keychain',
    ];

    if (options.headless !== false) {
        // v22+ 无头模式与有头模式行为一致(完整 Chrome,无 UI)
        args.push('--headless');
    }

    return args;
}

Headless 模式

2024-2026 更新:Puppeteer v22+ 默认使用新版 Headless 模式(完整 Chrome 无 UI),它完全支持 CDP。v24 中 headless 选项的取值为 boolean | 'shell''new' 字符串已被移除,而 'shell' 对应旧版精简无头模式(仅 Shell Browser,用于极简场景)。

图表渲染中…

连接方式

图表渲染中…

Chrome 启动参数对照

启动参数说明Puppeteer 默认
--headless新版无头模式
--remote-debugging-pipePipe 模式连接✅ (v23+,v24 延续)
--remote-debugging-port=PORTWebSocket 模式连接
--no-first-run跳过首次运行提示
--disable-extensions禁用扩展
--disable-dev-shm-usage避免 /dev/shm 问题(Docker)
--disable-gpu禁用 GPU 加速仅 CI 环境
--window-size=WIDTH,HEIGHT设置窗口大小
--auto-open-devtools-for-tabs自动打开 DevTools
--disable-web-security禁用同源策略
--user-data-dir=DIR用户数据目录使用临时目录

使用 puppeteer-core 连接已有的 Chrome

2024-2026 更新:连接已有的 Chrome 是调试场景中常用的方式。

javascript
const puppeteer = require('puppeteer-core');

// 方式一:连接到已启动的 Chrome(WebSocket 模式)
const browser = await puppeteer.connect({
    browserURL: 'http://127.0.0.1:9222',
});

// 方式二:使用 WebSocket URL 连接
const browser = await puppeteer.connect({
    browserWSEndpoint: 'ws://127.0.0.1:9222/devtools/browser/ABC123',
});

// 方式三:使用 Pipe 连接
const browser = await puppeteer.connect({
    browserWSEndpoint: '...',
    transport: new PipeTransport(pipeRead, pipeWrite),
});

// 连接后即可正常使用
const page = await browser.newPage();
await page.goto('https://example.com');

通过 CDP 控制浏览器

2024-2026 更新:Puppeteer v23+ 使用 Pipe 模式通信(v24 延续),API 变化不大但底层通信更高效。

Puppeteer 的核心 API 与 CDP 对照

每个 Puppeteer API 最终都会转换为一个或多个 CDP 命令:

图表渲染中…

Puppeteer API 详解

1. page.goto(url)

javascript
// Puppeteer API
await page.goto('https://example.com');

// 底层 CDP 命令
await connection.send('Page.enable');
await connection.send('Page.navigate', { url: 'https://example.com' });
// 等待 Page.loadEventFired 事件

2. page.evaluate(expression)

javascript
// Puppeteer API
const title = await page.evaluate('document.title');
const href = await page.evaluate(() => location.href);

// 底层 CDP 命令(简单表达式)
await connection.send('Runtime.evaluate', {
    expression: 'document.title',
    returnByValue: true,
});

// 底层 CDP 命令(函数)
await connection.send('Runtime.callFunctionOn', {
    functionDeclaration: '() => location.href',
    returnByValue: true,
});

3. page.click(selector)

javascript
// Puppeteer API
await page.click('#submit-button');

// 底层 CDP 命令
// 1. 查找元素
const { nodeId } = await connection.send('DOM.querySelector', {
    nodeId: 1,
    selector: '#submit-button',
});

// 2. 获取元素位置
const { model } = await connection.send('DOM.getBoxModel', { nodeId });
const x = model.content[0][0] + (model.content[2][0] - model.content[0][0]) / 2;
const y = model.content[0][1] + (model.content[4][1] - model.content[0][1]) / 2;

// 3. 模拟鼠标按下
await connection.send('Input.dispatchMouseEvent', {
    type: 'mousePressed', x, y, button: 'left', clickCount: 1,
});

// 4. 模拟鼠标释放
await connection.send('Input.dispatchMouseEvent', {
    type: 'mouseReleased', x, y, button: 'left', clickCount: 1,
});

4. page.type(selector, text)

javascript
// Puppeteer API
await page.type('#search', 'hello world', { delay: 100 });

// 底层 CDP 命令(逐字符发送)
for (const char of 'hello world') {
    await connection.send('Input.dispatchKeyEvent', {
        type: 'keyDown',
        text: char,
    });
    await connection.send('Input.dispatchKeyEvent', {
        type: 'keyUp',
        text: char,
    });
    if (delay) await sleep(delay);
}

5. page.screenshot()

javascript
// Puppeteer API
const screenshot = await page.screenshot({ path: 'screenshot.png' });

// 底层 CDP 命令
const { data } = await connection.send('Page.captureScreenshot', {
    format: 'png',
    quality: 100,
});
const buffer = Buffer.from(data, 'base64');

6. page.$(selector) / page.waitForSelector(selector)

javascript
// Puppeteer API
const element = await page.$('.content');
await page.waitForSelector('.content');

// 底层 CDP 命令
const { nodeId } = await connection.send('DOM.querySelector', {
    nodeId: 1,
    selector: '.content',
});

// waitForSelector 实际上是轮询
while (true) {
    const { nodeId } = await connection.send('DOM.querySelector', {
        nodeId: 1,
        selector: '.content',
    });
    if (nodeId) break;
    await sleep(100);
}

CDP 连接的实现

PipeConnection(v23+ 默认,v24 延续)

2024-2026 更新:Puppeteer v23+ 使用 Pipe 模式通信(v24 延续):

javascript
class PipeConnection {
    constructor(pipeRead, pipeWrite) {
        this._pipeRead = pipeRead;
        this._pipeWrite = pipeWrite;
        this._id = 0;
        this._callbacks = new Map();

        // 读取 CDP 消息
        this._readMessageLoop();
    }

    async _readMessageLoop() {
        while (true) {
            // 读取消息长度(4字节整数)
            const lengthBuffer = await readBytes(this._pipeRead, 4);
            const length = lengthBuffer.readInt32LE(0);

            // 读取消息内容
            const messageBuffer = await readBytes(this._pipeRead, length);
            const message = JSON.parse(messageBuffer.toString());

            this._handleMessage(message);
        }
    }

    send(method, params = {}) {
        const id = ++this._id;
        const message = JSON.stringify({ id, method, params });

        return new Promise((resolve, reject) => {
            this._callbacks.set(id, { resolve, reject });

            // 写入消息长度 + 内容
            const lengthBuffer = Buffer.alloc(4);
            lengthBuffer.writeInt32LE(message.length, 0);
            this._pipeWrite.write(lengthBuffer);
            this._pipeWrite.write(message);
        });
    }
}

WebSocketConnection(v22 及之前)

javascript
class WebSocketConnection {
    constructor(ws) {
        this._ws = ws;
        // ... 同 PipeConnection 的消息处理逻辑
    }

    send(method, params = {}) {
        const id = ++this._id;
        const message = JSON.stringify({ id, method, params });

        return new Promise((resolve, reject) => {
            this._callbacks.set(id, { resolve, reject });
            this._ws.send(message);  // 直接发送 JSON
        });
    }
}

消息格式对比

方面Pipe 模式WebSocket 模式
消息边界4字节长度前缀 + JSONWebSocket 协议自带消息边界
通信方式stdin/stdout 管道TCP + WebSocket
消息编码UTF-8 JSONUTF-8 JSON
连接建立Chrome 启动时直接创建HTTP → WebSocket 升级

CDP 完整命令对照表

Puppeteer APICDP 命令说明
puppeteer.launch()启动 Chrome 子进程--remote-debugging-pipe/port
browser.newPage()Target.createTarget创建新标签页
browser.close()Browser.close关闭浏览器
browser.version()Browser.getVersion获取浏览器版本
page.goto(url)Page.navigate导航
page.evaluate(fn)Runtime.evaluate / Runtime.callFunctionOn执行 JS
page.click(sel)DOM.querySelector + Input.dispatchMouseEvent点击
page.type(sel, text)Input.dispatchKeyEvent输入文字
page.screenshot()Page.captureScreenshot截图
page.$(sel)DOM.querySelector查找元素
page.setContent(html)Page.setDocumentContent设置页面内容
page.waitForSelector(sel)轮询 DOM.querySelector等待元素出现
page.cookies()Network.getCookies获取 Cookie
page.setCookie(cookie)Network.setCookie设置 Cookie
page.emulate(device)Emulation.setDeviceMetricsOverride设备模拟
page.setViewport(viewport)Emulation.setDeviceMetricsOverride设置视口
page.tracing.start()Tracing.start开始性能追踪
page.tracing.stop()Tracing.end结束性能追踪
page.coverage.startJSCoverage()Profiler.startPreciseCoverageJS 覆盖率
page.coverage.startCSSCoverage()CSS.startRuleUsageTrackingCSS 覆盖率
page.setRequestInterception(true)Fetch.enable请求拦截

Puppeteer 的请求拦截

2024-2026 更新:请求拦截使用 Fetch Domain 替代已废弃的 Network.requestIntercepted

javascript
// Puppeteer API
await page.setRequestInterception(true);
page.on('request', (request) => {
    if (request.url().includes('ads')) {
        request.abort();  // 拦截广告请求
    } else {
        request.continue();  // 其他请求继续
    }
});

// 底层 CDP 命令
await connection.send('Fetch.enable', {
    patterns: [{ urlPattern: '*' }],
});

connection.on('Fetch.requestPaused', async (params) => {
    if (params.request.url.includes('ads')) {
        await connection.send('Fetch.failRequest', {
            requestId: params.requestId,
            errorReason: 'BlockedByClient',
        });
    } else {
        await connection.send('Fetch.continueRequest', {
            requestId: params.requestId,
        });
    }
});

Playwright 的差异

2024-2026 新增:Playwright 是 Puppeteer 的替代方案,支持多浏览器:

方面PuppeteerPlaywright
支持浏览器Chrome/ChromiumChrome/Firefox/Safari(WebKit)
连接模式Pipe/WebSocketPipe/WebSocket
自动等待需手动 waitForSelector自动等待元素可操作
多页面单 Context多 BrowserContext(隔离)
语言支持JavaScriptJavaScript/Python/Java/.NET
维护者GoogleMicrosoft