Go 浏览器自动化大比拼:chromedp vs rod

目录

引子

在现代软件开发中,自动化浏览器操作已经不仅仅是测试的需求,而是成为了数据抓取、智能代理和自动化工具的重要组成部分。过去,开发者常常依赖 Selenium 这类传统方案,通过浏览器驱动直接操作浏览器。这种方式虽然成熟,但存在启动慢、资源占用高、跨平台配置复杂等问题,越来越难以满足现代化场景的需求。

对于 Python 用户来说,官方推荐使用 Playwright ,它提供了完整的 API,自动管理浏览器二进制,无需依赖 Node,就能实现可靠且高效的浏览器自动化。而对于 Go 开发者来说,虽然也存在官方的 playwright-go 封装,但其本质仍然是调用 Node 版 Playwright,依赖外部进程和 Node 环境,因此在实际工程中并不适合。

相比之下,Go 社区提供了 chromedprod 这两种基于 Chrome DevTools Protocol (CDP) 的原生方案。它们直接通过 CDP 与浏览器通信,无需额外依赖,性能优异,能够实现类似 Playwright 的浏览器控制能力,更加贴近现代化的自动化需求。对于希望在 Go 中构建高效、工程化的浏览器工具或智能代理系统的开发者而言,学习和掌握 chromedprod 无疑是更值得投入的方向。

Chrome DevTools

Chrome DevTools 提供了一套内置于 Google Chrome 中的 Web 开发和调试工具,可用来对网站进行迭代、调试和分析。

Chrome DevTools 主要由四部分组成:

  • Frontend:调试器前端,默认由 Chromium 内核层集成,DevTools Frontend 是一个 Web 应用程序;
  • Backend:调试器后端,一般是 Chromium、V8 或 Node.js;
  • Protocol:调试协议,调试器前后端将遵守该协议进行通信。 它分为代表被检查实体的语义方面的域。 每个域定义类型、命令(从前端发送到后端的消息)和事件(从后端发送到前端的消息)。该协议基于 json rpc 2.0 运行;
  • Message Channels:调试消息通道,消息通道是调试前后端间发送协议消息的一种方式。包括:Embedder Channel、WebSocket Channel、Chrome Extensions Channel、USB/ADB Channel。

本质上 Chrome DevTools 就是一个 Web 应用程序,它通过使用 Chrome DevTools Protocol 与后端进行交互,达到调试目的。

关于 Chrome 开发者工具的详细使用可以看官方文档

说到现在,还有人不知道Chrome DevTools具体是什么,其实大家都用过!Chrome DevTools 完全是现代浏览器内置的,而且不仅仅是 Chrome,几乎所有主流浏览器都有类似机制。

咱们打开底层是Chrome的浏览器,按 F12Ctrl+Shift+I 就能打开Chrome DevTools。

它提供了:

  • 元素检查(DOM / CSS)
  • 控制台(Console)
  • 网络监控(Network)
  • 性能分析(Performance / Lighthouse)
  • JavaScript 调试
  • 存储 / Cookie 管理

也就是说,DevTools 本身就是浏览器内置的调试环境,无需额外安装任何插件。

浏览器 DevTools 内置 CDP 支持
Chrome / Edge ✅ 内置 ✅ 完整支持
Firefox ✅ 内置 ✅ 部分兼容(Remote Debug Protocol)
Safari ✅ 内置 ✅ Safari Web Inspector Protocol
Opera / Brave ✅ 内置 ✅ 基于 Chromium CDP

💡 结论:现代浏览器几乎都内置 DevTools,CDP 是 Chrome 系列(Chromium 内核)提供的标准协议。

Chrome DevTools Protocol (CDP)

Chrome DevTools Protocol(简称 CDP)是 Chrome 浏览器提供的一套调试协议 ,它允许外部程序通过 WebSocket 连接与浏览器进行通信,实现对浏览器的远程调试和自动化控制。

CDP 最初是为 Chrome DevTools 开发者工具设计的,后来成为浏览器自动化 领域的核心协议。无论是 Puppeteer、Playwright 还是 Selenium 4,底层都依赖 CDP 实现强大的自动化能力。

协议原文:https://chromedevtools.github.io/devtools-protocol/

CDP 本质就是一组 JSON 格式的数据封装协议,JSON 是轻量的文本交换协议,可以被任何平台任何语言进行解析。

特性 说明
双向通信 既可以发送命令控制浏览器,也可以接收浏览器事件
WebSocket 传输 基于 WebSocket,实时、全双工
JSON 格式 命令和响应都是 JSON
功能丰富 涵盖页面、网络、DOM、JavaScript、性能、安全等

CDP 与 Chrome 的关系

复制代码
Chrome 开发者工具(DevTools UI)
           ↓
        CDP (WebSocket)
           ↓
  Chrome 浏览器内核 (Blink + V8)

Chrome DevTools 本身就是通过 CDP 与浏览器内核通信的。CDP 暴露的能力与 DevTools 在界面上操作的能力完全一致。

CDP完整架构图:

python 复制代码
┌───────────────────────────────────────────────┐
│           应用层(Application Layer)        │
│  高级 API / Agent 控制浏览器                  │
│  Playwright、Puppeteer、自定义 Agent         │
│  操作示例:导航、点击、输入、提取数据        │
│                     ▲                         │
│                     │ 调用 / 响应               │
└─────────────────────┼─────────────────────────┘
                      │
                      ▼
┌───────────────────────────────────────────────┐
│           传输层(Transport Layer)           │
│       WebSocket / HTTP Endpoint               │
│       ws://localhost:9222                     │
│  ↕ 双向通信:发送命令 ←→ 接收事件             │
└─────────────────────┼─────────────────────────┘
                      │
                      ▼
┌───────────────────────────────────────────────┐
│           会话层(Session Layer)             │
│  管理多个 Target 会话,分发 Domain           │
│                                               │
│  ┌───────────┐ ┌───────────┐ ┌───────────┐ │
│  │ Target 1  │ │ Target 2  │ │ Target N  │ │
│  └─────┬─────┘ └─────┬─────┘ └─────┬─────┘ │
│        │             │             │         │
│        └─────────────┴─────────────┘         │
└───────────────┼──────────────────────────────┘
                │
                ▼
┌───────────────────────────────────────────────┐
│           协议层(Protocol Layer)            │
│  每个 Target 监听多个 Domain                   │
│                                               │
│  ┌───────────┐ ┌───────────┐ ┌───────────┐ │
│  │ Page      │ │ DOM       │ │ Network   │ │
│  │ Domain    │ │ Domain    │ │ Domain    │ │
│  └───────────┘ └───────────┘ └───────────┘ │
│  ┌───────────┐                                │
│  │ Runtime   │                                │
│  │ Domain    │                                │
│  └───────────┘                                │
│  Commands / Events (JSON-RPC)                 │
│  ↕ 双向流:Command → 浏览器, Event → Agent   │
└─────────────────────┼─────────────────────────┘
                      │
                      ▼
┌───────────────────────────────────────────────┐
│           浏览器内核(Chrome / Chromium)     │
│  Blink 渲染引擎 + V8 JS 引擎 + Network + Storage │
│  执行 CDP 命令,渲染页面,处理 JS 和网络请求   │
└───────────────────────────────────────────────┘

各层职责:

层级 职责 代表组件 / 示例
应用层 提供高级 API 供开发者或 Agent 使用 Playwright、Puppeteer、自定义 Agent
传输层 建立 WebSocket 连接,传输 JSON 命令和事件 ws://localhost:9222
会话层 管理多个 Target 会话,分发 Domain Target.attachToTarget
协议层 定义各种 Domain 和对应的 Command / Event Page、DOM、Network、Runtime
内核层 实际执行命令和渲染页面 Blink + V8 + Network Stack + Storage

如何直观的看到CDP信息呢?我们可以在设置-》实验里面找到Protocol Monitor然后勾选,

任意打开一个网页就可以录制到DevTools与后台的交互信息:

CDP 的每个 Method(${domain}.${command})包含 请求(Request)响应(Response) 两个阶段:

  1. Request(请求)

    • 指定要执行的操作(command)
    • 可以包含操作所需的参数(params)
    • 通过 JSON-RPC 消息发送给浏览器
  2. Response(响应)

    • 浏览器返回执行结果

    • 包含 resulterror

      • result 表示操作成功并返回数据
      • error 表示操作失败,并包含错误信息

这里需要注意:CDP 是异步事件驱动的,某些 Method 可能还会触发额外的 Event(事件),这些事件可能在 Response 返回前后发送,用于告知状态变化或结果更新。

Domain 分类

Domain 功能描述 常用命令
Page 页面导航、截图、生成 PDF Page.navigate, Page.reload, Page.captureScreenshot
DOM DOM 树操作、元素查找 DOM.querySelector, DOM.getDocument, DOM.setAttributeValue
Runtime JavaScript 执行 Runtime.evaluate, Runtime.callFunctionOn, Runtime.getProperties
Network 网络请求监控与拦截 Network.enable, Network.getResponseBody, Network.setRequestInterception
Input 模拟用户输入 Input.dispatchMouseEvent, Input.dispatchKeyEvent
Console 控制台消息操作 Console.enable, Console.clearMessages
Performance 性能分析 Performance.enable, Performance.getMetrics
Security 安全相关 Security.enable, Security.setIgnoreCertificateErrors

💡 小提示:Network Domain 不仅可以拦截请求,还可以监控响应和资源加载。Page Domain 也可以用于生成 PDF 或打印页面。

Domain 使用示例:

json 复制代码
// Page Domain - 导航到 URL
{
  "id": 1,
  "method": "Page.navigate",
  "params": {
    "url": "https://example.com"
  }
}

// DOM Domain - 查询元素
{
  "id": 2,
  "method": "DOM.querySelector",
  "params": {
    "nodeId": 1,
    "selector": "#submit-button"
  }
}

// Runtime Domain - 执行 JavaScript
{
  "id": 3,
  "method": "Runtime.evaluate",
  "params": {
    "expression": "document.title",
    "returnByValue": true
  }
}

// Network Domain - 启用网络监控
{
  "id": 4,
  "method": "Network.enable",
  "params": {
    "maxTotalBufferSize": 10000000,
    "maxResourceBufferSize": 5000000
  }
}

💡 注意:

  1. 每个 request 都会返回 response,包括 resulterror
  2. Runtime.evaluate 的 returnByValue: true 表示返回值直接序列化,而非对象引用。
  3. 某些操作可能触发额外 Event,在 response 前后发送。

targetId 解析

  • targetId 是 CDP 中用于唯一标识一个可调试目标的字符串。
  • Chrome 会为每个 Target 自动生成唯一 ID(通常为 UUID 格式,用于区分 page、iframe、worker 等)。

Target 的类型:

类型 说明 示例
page 普通网页标签页 你浏览的网页
iframe 嵌入的框架 页面中的 <iframe>
service_worker 服务工作线程 PWA 的后台脚本
shared_worker 共享工作线程 多页面共享的 JS 线程
background_page 扩展后台页 Chrome 扩展的后台
browser 浏览器实例 整个浏览器

💡 小提示:Target 类型 browser 通常只存在一个,是浏览器启动上下文。

targetId 生命周期:

text 复制代码
┌───────────────────────────────────────────────┐
│                Target 生命周期                │
├───────────────────────────────────────────────┤
│ 创建 Target                                   │
│    │                                         │
│    ▼                                         │
│ Chrome 生成唯一 targetId                      │
│    │                                         │
│    ▼                                         │
│ 分配给 Target(page / iframe / worker)      │
│    │                                         │
│    ▼                                         │
│ 可通过 CDP 发现和操作                        │
│    │                                         │
│    ▼                                         │
│ 监听事件或执行命令                            │
│    │                                         │
│    ▼                                         │
│ Target 被销毁(关闭标签页 / iframe 卸载等) │
│    │                                         │
│    ▼                                         │
│ targetId 失效                                │
└───────────────────────────────────────────────┘

💡 小提示:

  • 可以通过 Target.getTargetsTarget.setDiscoverTargets 获取或监听 Target。
  • Target 被关闭或卸载后,ID 失效,需要重新获取新的 Target。

targetId的作用层级:

1️⃣ CDP Method(${domain}.${command})的 Request/Response

  • 作用层级:操作具体浏览器能力(页面导航、DOM 查询、JS 执行等)。

  • 流程

    1. Agent 发送一个 Request(包含 domain.command + params)
    2. 浏览器执行命令
    3. 返回 Response(包含 result 或 error)
    4. 有些命令还会触发 Event(异步事件通知)
  • 特点 :面向 操作能力,不管具体哪个 tab、iframe 或 worker,只要发送 request,就会有 response 或 event。

2️⃣ targetId 的概念

  • 作用层级:标识一个可调试目标(Target)------比如一个 tab、iframe、worker、扩展后台页等)。

  • 目标:告诉 CDP "我要操作的是哪个目标"。

  • 生成:Chrome 启动或新建 Target 时生成唯一字符串(UUID-like)。

  • 用途

    • 通过 targetId 可以在同一个浏览器实例中同时操作多个 tab 或 worker。
    • 当发送 CDP Method 时,你需要 指定 targetId 才能让浏览器知道这是对哪个 Target 执行操作。

3️⃣ Request/Response 和 targetId 的关系

把两者联系起来可以这样理解:

复制代码
Agent 发送 CDP Method Request
┌───────────────────────────┐
│ method: Page.navigate      │
│ params: { url: "xxx" }     │
│ targetId: "ABC123"         │  <-- 指定操作目标
└───────────────────────────┘
            │
            ▼
Chrome 浏览器收到命令,根据 targetId 找到对应的 Target(tab/iframe/worker)
            │
            ▼
执行命令(导航、JS 执行等)
            │
            ▼
返回 Response / 触发 Event
  • targetId 只是告诉浏览器 "哪个目标" 执行这个 Method。
  • Method 的 Request/Response 流程 描述的是 命令和事件的交互方式
  • 没有 targetId,就无法区分操作哪个 tab 或 worker,在多 Target 场景下就会混乱。

💡 简单比喻

  • targetId → "桌号",你想点餐必须告诉服务员桌号。
  • Method Request → "菜品 + 份量",你告诉服务员你要什么菜。
  • Response / Event → "上菜或异常反馈",服务员告诉你菜上来了或者出错了。

实在理解不了的话,就把targetId 当作是 一个唯一标识符,指向一个可调试的目标(Target),可以是:

  • 标签页(page)

  • iframe

  • Worker(service/ shared)

  • 扩展后台页(background_page)

  • 它就像数据库里每条记录的 主键 ID,用来标识你要操作的资源。

就像你写CRUD一样:

概念 targetId / CDP 数据库 CRUD 类比说明
资源标识 targetId 主键 ID 指定要操作的具体目标/记录
创建 打开新页面/Worker → 新 targetId INSERT 新增资源,浏览器生成唯一 targetId
读取 Runtime.evaluate、DOM.querySelector 等 SELECT 查询资源内容
更新 Page.navigate、DOM.setAttributeValue UPDATE 修改资源状态或属性
删除 关闭标签页 / 卸载 iframe DELETE 释放资源,targetId 失效

💡 小结:

targetId 就是资源的句柄,所有 CDP 命令(Method Request)都需要指定 targetId,才能对对应资源执行操作,就像数据库 CRUD 操作都需要指定 ID 才能对某条记录执行。

chromedp vs rod

虽然 CDP 提供了非常强大且底层的浏览器控制能力,但它本身的协议较为复杂:每一个操作都需要构造 JSON-RPC 请求、处理响应、监听异步事件,并管理 targetId 等生命周期。这对于大多数日常自动化任务来说,既繁琐又容易出错。

如果我们的目标只是高效地操作浏览器、进行自动化任务或数据抓取 ,完全没有必要直接与 CDP 打交道。幸运的是,在 Go 语言生态中,有两个成熟的库可以帮助我们更轻松地完成这些工作:chromedpRod

  • 它们都基于 CDP,但对开发者屏蔽了底层的复杂细节。
  • 你无需手动管理 JSON-RPC 消息,也无需处理 targetId 的生命周期。
  • 开发者可以用更直观、更 Go 风格的 API 来操作浏览器,实现页面导航、元素操作、JavaScript 执行、截图等功能。

换句话说,如果 CDP 是"手工驾驶的跑车",那么 chromedp 和 Rod 就是"自动驾驶模式",让我们专注于业务逻辑,而不用纠结底层协议。

好的,我帮你把 chromedp 和 Rod 的详细介绍 写出来,并加上一个 详细对比表格,内容可直接放到博客中。排版清晰,强调功能和适用场景。

简单对比

在 Go 生态中,如果我们想绕过 CDP 的底层复杂性,高效操作浏览器,chromedpRod 是目前最常用的两个库。下面分别详细介绍它们的特点与使用方式。

chromedp 是 Go 官方团队开发的高性能浏览器自动化库,完全基于 Chrome DevTools Protocol(CDP),但对开发者屏蔽了底层的复杂性。

核心特点

特性 说明
Go 原生 API 提供 Go 风格的上下文管理和任务组合,无需 Node.js 或浏览器驱动
上下文管理 利用 context.Context 控制浏览器生命周期和超时
高性能 直接通过 CDP 与浏览器通信,无额外进程开销
任务组合 支持将多个操作组合成一个任务序列(Task List)
异步事件支持 可以监听浏览器事件,如页面加载、请求完成、控制台输出

示例代码

go 复制代码
package main

import (
    "context"
    "fmt"
    "github.com/chromedp/chromedp"
)

func main() {
    ctx, cancel := chromedp.NewContext(context.Background())
    defer cancel()

    var title string
    chromedp.Run(ctx,
        chromedp.Navigate("https://example.com"),
        chromedp.Title(&title),
    )
    fmt.Println("页面标题:", title)
}

💡 适用场景

  • 高性能抓取网页数据
  • 自动化测试和页面截图
  • 需要精细控制浏览器行为,但希望保留 Go 风格 API

Rod 是另一款 Go 语言浏览器自动化库,设计理念是 链式调用、易于表达浏览器操作逻辑。同样基于 CDP,但提供了更直观、链式化的操作方式。

核心特点

特性 说明
链式调用 支持 page.Navigate().Element(...).Click() 风格,代码直观
高度封装 对 targetId、事件监听、元素等待等进行封装
内置异步支持 轻松处理页面异步加载和动态内容
截图/爬取方便 提供丰富方法获取截图、HTML、文本等
丰富插件 社区提供插件支持,如反反爬虫、JS 执行等

示例代码

go 复制代码
package main

import (
    "fmt"
    "github.com/go-rod/rod"
)

func main() {
    browser := rod.New().MustConnect()
    page := browser.MustPage("https://example.com")
    
    title := page.MustEval("() => document.title").String()
    fmt.Println("页面标题:", title)
    
    page.MustClose()
}

💡 适用场景

  • 快速开发浏览器自动化任务
  • 对动态页面、异步操作要求较高的爬虫
  • 喜欢链式风格 API 的开发者

chromedp 与 Rod 对比:

对比维度 chromedp Rod
开发风格 Go 原生风格,使用 Task List 和 context 链式调用风格,操作直观
学习曲线 中等,需要理解 Task 和 context 简单,上手快,API直观
异步支持 需要手动管理事件和等待 内置等待机制,更适合动态页面
性能 高性能,直接 CDP 通信 高性能,略低于 chromedp,但更易用
API 丰富度 聚焦 CDP 原生功能 封装更全面,包括元素操作、截图、爬取等
多页面管理 需要自己管理 context/targetId 内置 target 管理,链式 API 支持多页面操作
适合场景 高性能抓取、自动化测试 快速开发爬虫、动态页面交互、脚本自动化

💡 总结

  • chromedp 更贴近底层 CDP,适合需要精细控制浏览器的场景。
  • Rod 更关注开发效率和可读性,尤其适合快速实现浏览器交互逻辑。
  • 两者都不依赖 Node.js 或传统浏览器驱动,是 Go 语言中浏览器自动化的首选方案。

chrome

chromedp 是一个强大的 Go 语言库,用于控制 headless Chrome 或完整浏览器实例。它提供简洁的 API 实现页面导航、元素操作、截图、PDF 生成等常见浏览器自动化任务。

在项目目录下执行以下命令安装依赖。

python 复制代码
go get -u github.com/chromedp/chromedp

目前最新版本是 v0.15.1,执行上面的命令会下载一个go1.26,如果你们和我一样还在用go.1.25可以指定下载:

python 复制代码
go get github.com/chromedp/chromedp@v0.13.2

连接浏览器

1️⃣ 使用默认浏览器打开 Tab

go 复制代码
package main

import (
    "context"
    "github.com/chromedp/chromedp"
)

func main() {
    // 创建浏览器上下文
    ctx, cancel := chromedp.NewContext(context.Background())
    defer cancel()

    // 在这里可以添加浏览器任务,例如页面导航、抓取等
}

2️⃣ 连接本地浏览器(自定义 Chrome 路径)

go 复制代码
package main

import (
    "context"
    "github.com/chromedp/chromedp"
)

func main() {
    // 配置 Chrome 启动选项
    opts := append(chromedp.DefaultExecAllocatorOptions[:],
        // Windows 示例
        // chromedp.ExecPath("C:/Program Files/Google/Chrome/Application/chrome.exe"),
        // MacOS 示例
        chromedp.ExecPath("/Applications/Google Chrome.app/Contents/MacOS/Google Chrome"),
        chromedp.Flag("headless", false), // 设置是否无头模式
    )

    // 创建分配器上下文(可控制浏览器启动参数)
    allocCtx, cancel := chromedp.NewExecAllocator(context.Background(), opts...)
    defer cancel()

    // 基于分配器上下文创建浏览器上下文
    ctx, cancel := chromedp.NewContext(allocCtx)
    defer cancel()

    // 可以在 ctx 上执行浏览器任务
}

3️⃣ 连接远程 Chrome

go 复制代码
package main

import (
    "context"
    "github.com/chromedp/chromedp"
)

func main() {
    // 连接到远程 Chrome 实例
    allocatorContext, cancel := chromedp.NewRemoteAllocator(context.Background(), "ws://10.20.30.1:9222/")
    defer cancel()

    // 基于远程分配器创建浏览器上下文
    ctx, cancel := chromedp.NewContext(allocatorContext)
    defer cancel()

    // 可以在 ctx 上执行远程浏览器任务
}

说明

  1. 默认上下文适合快速启动浏览器并执行任务。
  2. 本地浏览器可以自定义 Chrome 路径、是否无头、启动参数等。
  3. 远程 Chrome允许连接已有的浏览器实例,适合分布式任务或远程调试。

常用方法

页面导航

go 复制代码
chromedp.Run(ctx,
    chromedp.Navigate("https://www.example.com"),
)

设备模拟

go 复制代码
import "github.com/chromedp/chromedp/device"

chromedp.Run(ctx,
    chromedp.Emulate(device.IPhone15ProMax), // 模拟 iPhone 15 Pro Max
    chromedp.Navigate("https://m.example.com"),
)

执行 JavaScript:

1️⃣ 同步执行示例

go 复制代码
var title string

chromedp.Run(ctx,
    chromedp.Evaluate(`document.title`, &title),
)
fmt.Println("页面标题:", title)

2️⃣ 异步 Promise 处理

go 复制代码
var result interface{}

chromedp.Run(ctx,
    chromedp.Evaluate(
        `someReturnPromiseFunc()`,
        &result,
        func(p *runtime.EvaluateParams) *runtime.EvaluateParams {
            return p.WithAwaitPromise(true) // 等待 Promise 完成
        },
    ),
)

获取元素文本

go 复制代码
var res string

chromedp.Run(ctx,
    chromedp.Navigate(`https://pkg.go.dev/time`),
    chromedp.Text(`.Documentation-overview`, &res, chromedp.NodeVisible),
)
fmt.Println("元素文本:", res)

点击元素并获取值

go 复制代码
var example string

chromedp.Run(ctx,
    chromedp.Navigate(`https://pkg.go.dev/time`),
    // 等待页面 footer 元素可见,保证页面已加载完成
    chromedp.WaitVisible(`body > footer`),
    // 点击页面上的 "Example" 连接
    chromedp.Click(`#example-After`, chromedp.NodeVisible),
    // 获取 textarea 区域的文本
    chromedp.Value(`#example-After textarea`, &example),
)
fmt.Println("Example 内容:", example)

使用示例

1️⃣ 提交表单(Submit)

go 复制代码
func doGithubSearch() {
    // 创建浏览器上下文
    ctx, cancel := chromedp.NewContext(context.Background(), chromedp.WithDebugf(log.Printf))
    defer cancel()

    // 执行任务
    var res string
    err := chromedp.Run(ctx, submit(`https://github.com/search`, `//input[@name="q"]`, `chromedp`, &res))
    if err != nil {
        log.Fatal(err)
    }

    log.Printf("got: `%s`", strings.TrimSpace(res))
}

// submit 返回一个 chromedp.Tasks 列表,用于搜索并抓取结果
func submit(urlStr, sel, q string, res *string) chromedp.Tasks {
    return chromedp.Tasks{
        chromedp.Navigate(urlStr),
        chromedp.WaitVisible(sel),
        chromedp.SendKeys(sel, q),
        chromedp.Submit(sel),
        chromedp.WaitVisible(`//*[contains(., 'repository results')]`),
        chromedp.Text(`(//ul[contains(@class, "repo-list")]/li[1]//p)[1]`, res),
    }
}

2️⃣ URL 截图

输入一个 URL,通过 chromedp 打开页面并进行截图,可截取特定元素或整个页面。

go 复制代码
package main

import (
    "context"
    "log"
    "os"

    "github.com/chromedp/chromedp"
)

func main() {
    // 新建浏览器上下文
    ctx, cancel := chromedp.NewContext(
        context.Background(),
        chromedp.WithDebugf(log.Printf),
    )
    defer cancel()

    var buf []byte

    // 截取页面特定元素
    if err := chromedp.Run(ctx, elementScreenshot(`https://pkg.go.dev/`, `img.Homepage-logo`, &buf)); err != nil {
        log.Fatal(err)
    }
    if err := os.WriteFile("elementScreenshot.png", buf, 0o644); err != nil {
        log.Fatal(err)
    }

    // 截取整个页面
    if err := chromedp.Run(ctx, fullScreenshot(`https://brank.as/`, 90, &buf)); err != nil {
        log.Fatal(err)
    }
    if err := os.WriteFile("fullScreenshot.png", buf, 0o644); err != nil {
        log.Fatal(err)
    }

    log.Printf("wrote elementScreenshot.png and fullScreenshot.png")
}

// 截取特定元素的屏幕截图
func elementScreenshot(urlStr, sel string, res *[]byte) chromedp.Tasks {
    return chromedp.Tasks{
        chromedp.Navigate(urlStr),
        chromedp.Screenshot(sel, res, chromedp.NodeVisible),
    }
}

// 截取整个浏览器视口
func fullScreenshot(urlstr string, quality int, res *[]byte) chromedp.Tasks {
    return chromedp.Tasks{
        chromedp.Navigate(urlstr),
        chromedp.FullScreenshot(res, quality),
    }
}

注意:chromedp.FullScreenshot 会覆盖设备模拟设置,如果之前有 device.Emulate,可以使用 device.Reset 重置。

3️⃣ URL 转 PDF

go 复制代码
package main

import (
    "context"
    "fmt"
    "log"
    "os"

    "github.com/chromedp/cdproto/page"
    "github.com/chromedp/chromedp"
)

func main() {
    // 新建浏览器上下文
    ctx, cancel := chromedp.NewContext(context.Background())
    defer cancel()

    var buf []byte

    // 打开页面并生成 PDF
    if err := chromedp.Run(ctx, printToPDF(`https://www.baidu.com/`, &buf)); err != nil {
        log.Fatal(err)
    }

    // 保存 PDF 文件
    if err := os.WriteFile("sample.pdf", buf, 0o644); err != nil {
        log.Fatal(err)
    }
    fmt.Println("wrote sample.pdf")
}

// printToPDF 返回生成 PDF 的 chromedp.Tasks
func printToPDF(urlStr string, res *[]byte) chromedp.Tasks {
    return chromedp.Tasks{
        chromedp.Navigate(urlStr),
        chromedp.ActionFunc(func(ctx context.Context) error {
            buf, _, err := page.PrintToPDF().WithPrintBackground(false).Do(ctx)
            if err != nil {
                return err
            }
            *res = buf
            return nil
        }),
    }
}

4️⃣ 小结

  • chromedp 支持:

    • 页面导航、表单输入与提交
    • 元素操作(获取文本、点击、输入)
    • 截图(元素或全页)
    • PDF 导出
    • 执行任意 JavaScript
  • 更多官方示例和进阶功能请查看 chromedp examples

如果代码运行出现报错如:

python 复制代码
2026/04/07 19:36:02 ERROR: could not unmarshal event: json: cannot unmarshal JSON string into Go network.IPAddressSpace within "/clientSecurityState/initiatorIPAddressSpace": unknown IPAddressSpace value: Loopback

这个报错其实和你写的 Task 没有直接关系,而是 chromedp 内部解析 Chrome DevTools Protocol (CDP) 事件时发生的。

Chrome 返回的 JSON 事件里有一个字段 /clientSecurityState/initiatorIPAddressSpace,值是 "Loopback"。

chromedp 内部用 network.IPAddressSpace 类型去解析这个字段,但它的类型定义里没有 Loopback 这个值。

因此 JSON 反序列化失败,导致报错。

核心原因是 chromedp 类型定义滞后于 Chrome 协议,升级 chromedp 通常能解决。

Rod

Rod的文档相对比较完善一些,可以参考:https://go-rod.github.io/#/get-started/README

安装命令:

python 复制代码
go get github.com/go-rod/rod

连接浏览器

让我们使用 Rod 来打开一个网页并获取它的截图。 首先创建 "main.go",并在其中输入以下内容:

python 复制代码
package main

import "github.com/go-rod/rod"

func main() {
    page := rod.New().MustConnect().MustPage("https://www.wikipedia.org/")
    page.MustWaitStable().MustScreenshot("a.png")
}

rod.New 用于创建浏览器对象,而 MustConnect 则会启动并连接到浏览器。 MustPage 会创建一个页面对象(类似于浏览器中的一个标签页)。

MustWaitStable 等到页面几乎没有变化。 MustScreenshot 会获取页面的截图。

程序会输出如下的一张截图 "a.png"。

默认情况下,Rod 会以无头模式(headless)运行浏览器 ,这样可以获得更好的性能。但在实际开发自动化任务时,我们往往更关心的是------调试是否方便

好在 Rod 提供了一些非常实用的调试辅助能力,可以让我们"看见浏览器在干嘛"。

在正式运行任务之前,我们可以对代码做一点小改动:

go 复制代码
package main

import (
    "time"

    "github.com/go-rod/rod"
)

func main() {
    page := rod.New().
        NoDefaultDevice(). // 禁用默认设备模拟
        MustConnect().
        MustPage("https://www.wikipedia.org/")

    page.MustWindowFullscreen() // 浏览器全屏
    page.MustWaitStable().MustScreenshot("a.png")

    // 防止程序过快退出,方便观察
    time.Sleep(time.Hour)
}

这里有几个非常实用的小技巧:

  • NoDefaultDevice

    关闭默认设备模拟(比如移动端),避免影响页面布局

  • MustWindowFullscreen

    让浏览器全屏展示,方便观察页面细节

  • time.Sleep(time.Hour)

    防止程序执行完立刻退出(否则你可能还没看清页面就没了)

接下来,用 Rod 提供的调试参数运行程序:

bash 复制代码
go run . -rod=show

👉 -rod=show 的作用是:让浏览器以"可视化模式"运行(非 headless)

运行后,你应该能看到一个真实的浏览器窗口被打开,并自动执行你的脚本逻辑。

当你观察完成后,可以回到终端,按下:

复制代码
CTRL + C

即可终止程序。

在开发阶段,强烈建议开启可视化调试:

  • 能直观看到页面加载过程
  • 更容易定位元素选择器问题
  • 避免"脚本执行了但不知道发生了什么"的情况

等脚本稳定后,再切换回 headless 模式以提升性能即可 🚀

自定义浏览器启动

在前面的示例中,我们都是直接使用:

go 复制代码
rod.New().MustConnect()

这种方式虽然简单,但在实际开发中,你很快会遇到一些更复杂的需求,比如:

  • 使用指定版本的 Chrome
  • 复用已有浏览器实例
  • 设置代理 / 用户数据目录
  • 在分布式环境中远程控制浏览器

这时候,就需要用到 Rod 的浏览器启动能力。

1️⃣ 连接到正在运行的浏览器

首先,你可以手动启动一个 Chrome,并开启远程调试端口。

例如在 macOS 上:

bash 复制代码
"/Applications/Google Chrome.app/Contents/MacOS/Google Chrome" \
--headless \
--remote-debugging-port=9222

启动后,会输出类似内容:

text 复制代码
DevTools listening on ws://127.0.0.1:9222/devtools/browser/xxxx

这个 ws://... 地址,就是浏览器的 控制入口(WebSocket 地址)

接下来,你就可以用 Rod 连接这个浏览器:

go 复制代码
package main

import "github.com/go-rod/rod"

func main() {
    u := "ws://127.0.0.1:9222/devtools/browser/xxxx"

    rod.New().
        ControlURL(u).
        MustConnect().
        MustPage("https://example.com")
}

2️⃣ 使用 launcher 自动启动浏览器(推荐)

手动启动浏览器虽然灵活,但步骤比较繁琐。

Rod 提供了一个非常好用的工具:launcher,帮你自动完成这些事情。

go 复制代码
package main

import (
    "github.com/go-rod/rod"
    "github.com/go-rod/rod/lib/launcher"
)

func main() {
    u := launcher.New().
        Bin("/Applications/Google Chrome.app/Contents/MacOS/Google Chrome").
        MustLaunch()

    rod.New().
        ControlURL(u).
        MustConnect().
        MustPage("https://example.com")
}

如果你不想手动写路径,可以这样:

go 复制代码
path, _ := launcher.LookPath()

u := launcher.New().
    Bin(path).
    MustLaunch()

rod.New().ControlURL(u).MustConnect().MustPage("https://example.com")

实际上,上面这些代码都可以简化为:

go 复制代码
rod.New().MustConnect().MustPage("https://example.com")

原因是:

  • 如果你没有设置 ControlURL
  • MustConnect() 会自动调用 launcher.New().MustLaunch()
  • 默认会下载并使用一个固定版本的浏览器(保证一致性)

👉 这也是 Rod "开箱即用"体验非常好的原因之一。

3️⃣ 自定义浏览器启动参数

在实际项目中,你可能需要:

  • 设置用户数据目录(保存 cookie)
  • 开启 / 关闭 headless
  • 配置代理

Rod 提供了非常灵活的 API 来控制这些参数。

基础方式:

go 复制代码
u := launcher.New().
    Set("user-data-dir", "path").
    Set("headless").
    Delete("--headless").
    MustLaunch()

rod.New().ControlURL(u).MustConnect().MustPage("https://example.com")

推荐方式(更优雅):

Rod 提供了一些 helper 方法:

go 复制代码
u := launcher.New().
    UserDataDir("path").
    Headless(true).
    Headless(false).
    MustLaunch()

rod.New().ControlURL(u).MustConnect().MustPage("https://example.com")

4️⃣ 清理用户数据

浏览器运行过程中会生成缓存、cookie 等数据(user-data-dir)。

Rod 提供了自动清理方法:

go 复制代码
l := launcher.New().
    Headless(false).
    Devtools(true)

defer l.Cleanup()

👉 程序结束后,会自动删除临时数据目录。

5️⃣ 远程浏览器管理(生产环境)

在实际生产环境(比如爬虫系统)中,通常会这样设计:

  • 爬虫任务(Worker)
  • 浏览器集群(Browser Pool)

两者分离,独立扩展。

Rod 提供了 launcher.Manager 来支持这种架构,可以:

  • 远程启动浏览器
  • 动态分配资源
  • 自动清理环境

6️⃣ Docker 运行浏览器(跨平台方案)

有些 Linux 环境安装 Chromium 很麻烦,这时候可以直接用官方镜像:

bash 复制代码
docker run -p 7317:7317 ghcr.io/go-rod/rod

然后在代码中连接远程浏览器即可。

👉 优点:

  • 跨平台一致
  • 已优化字体和渲染
  • 支持多浏览器实例
  • 自动清理 user-data-dir

7️⃣ 用户模式(复用登录态)

有时候你希望复用已有浏览器的登录状态(比如 GitHub 登录)。

Rod 提供了 用户模式(User Mode)

go 复制代码
wsURL := launcher.NewUserMode().MustLaunch()

rod.New().
    ControlURL(wsURL).
    MustConnect().
    NoDefaultDevice()

👉 这种模式下:

  • Rod 会接管你当前的浏览器
  • 就像一个"浏览器插件"一样工作
  • 可以直接复用已有 cookie / 登录态

常用操作

输入内容操作:

go 复制代码
package main

import (
    "time"

    "github.com/go-rod/rod"
)

func main() {
    browser := rod.New().MustConnect().NoDefaultDevice()
    page := browser.MustPage("https://www.wikipedia.org/").
        MustWindowFullscreen()

    // 输入关键词
    page.MustElement("#searchInput").MustInput("earth")

    page.MustWaitStable().MustScreenshot("a.png")
    time.Sleep(time.Hour)
}

💡 关键点

  • MustElement(selector)

    👉 自动等待元素出现(非常重要!)

  • MustInput("earth")

    👉 向输入框输入内容

👉 注意:这里不需要手动等待页面加载,Rod 已经帮你处理好了。

点击按钮操作:

go 复制代码
page.MustElement("#search-form > fieldset > button").MustClick()

完整示例

go 复制代码
package main

import (
    "time"

    "github.com/go-rod/rod"
)

func main() {
    browser := rod.New().MustConnect().NoDefaultDevice()
    page := browser.MustPage("https://www.wikipedia.org/").
        MustWindowFullscreen()

    page.MustElement("#searchInput").MustInput("earth")
    page.MustElement("#search-form > fieldset > button").MustClick()

    page.MustWaitStable().MustScreenshot("a.png")
    time.Sleep(time.Hour)
}

运行后,你会看到页面自动搜索 "earth",并截图结果。

调试神器:慢动作 + 可视化跟踪

自动化执行太快了,人眼根本看不清。

Rod 提供了一个非常好用的调试模式:

bash 复制代码
go run . -rod="show,slow=1s,trace"

效果说明

  • slow=1s

    👉 每一步操作前暂停 1 秒

  • trace

    👉 在页面上显示操作轨迹(类似虚拟鼠标)

你会看到:

  • 鼠标自动移动到输入框
  • 输入文字
  • 点击按钮

同时控制台也会输出详细日志,例如:

text 复制代码
[rod] [input] input earth
[rod] [input] left click

👉 调试体验非常丝滑,比 chromedp 直观很多。

代码方式开启慢动作

除了命令行,也可以在代码里写:

go 复制代码
rod.New().SlowMotion(2 * time.Second)

获取文本内容:

不仅能操作页面,还可以直接获取数据。

go 复制代码
package main

import (
    "fmt"

    "github.com/go-rod/rod"
)

func main() {
    page := rod.New().MustConnect().MustPage("https://www.wikipedia.org/")

    page.MustElement("#searchInput").MustInput("earth")
    page.MustElement("#search-form > fieldset > button").MustClick()

    el := page.MustElement("#mw-content-text > div.mw-parser-output > p:nth-child(6)")
    fmt.Println(el.MustText())
}

运行后会输出类似:

text 复制代码
Earth is the third planet from the Sun...

获取图片内容:

同样的方法,也可以抓取图片:

go 复制代码
package main

import (
    "github.com/go-rod/rod"
    "github.com/go-rod/rod/lib/utils"
)

func main() {
    page := rod.New().MustConnect().MustPage("https://www.wikipedia.org/")

    page.MustElement("#searchInput").MustInput("earth")
    page.MustElement("#search-form > fieldset > button").MustClick()

    el := page.MustElement("#mw-content-text > div.mw-parser-output > table.infobox > tbody > tr:nth-child(1) > td > a > img")
    _ = utils.OutputFile("b.png", el.MustResource())
}

👉 这里用的是:

  • MustResource():获取资源二进制数据
  • utils.OutputFile():保存为文件

异常处理:Must vs 非 Must 的设计哲学

在前面的示例中,你应该已经注意到了一个很明显的风格:

go 复制代码
page.MustElement(...)
page.MustNavigate(...)

这些方法都有一个共同点:

👉 都带有 Must 前缀

其实,这并不是 Rod 独有的设计,在 Go 标准库中也很常见,比如:

  • regexp.MustCompile

它的核心思想很简单:

出错就直接 panic,不让你继续往下执行

MustElement 为例:

go 复制代码
func (p *Page) MustElement(selector string) *rod.Element {
    el, err := (*rod.Page)(p).Element(selector)
    if err != nil {
        panic(err)
    }
    return el
}

👉 本质就是:

text 复制代码
Element + error 判断 + panic

没有任何魔法。

那什么时候该用 Must?

  • 冒烟测试(Smoke Test)
  • 自动化测试(E2E)
  • 简单脚本
  • 你"非常确定不会失败"的逻辑

例如:

go 复制代码
page.MustElement("#login").MustClick()

👉 简洁、直接、代码量少。

❌ 不适合使用 Must 的场景

  • 爬虫(页面结构不稳定)
  • 网络请求不可靠
  • 需要容错 / 重试
  • 生产级系统

👉 这种情况下,建议使用非 Must 版本

标准 Go 风格(推荐生产使用)

go 复制代码
page := rod.New().MustConnect().MustPage("https://example.com")

el, err := page.Element("a")
if err != nil {
    panic(err)
}

html, err := el.HTML()
if err != nil {
    panic(err)
}

fmt.Println(html)

👉 这才是 Go 最"正统"的错误处理方式:

  • 显式处理 error
  • 可控性强
  • 更适合复杂逻辑

如果你觉得上面的写法太啰嗦,Rod 也提供了一种"折中方案":

go 复制代码
page := rod.New().MustConnect().MustPage("https://example.com")

err := rod.Try(func() {
    fmt.Println(page.MustElement("a").MustHTML())
})

panic(err)

💡 它的本质

  • 捕获 MustXXX 的 panic
  • 转换成 error 返回

👉 相当于:

text 复制代码
panic → recover → error

这种方式虽然简洁,但有个问题:

👉 可能会捕获到你不想捕获的异常

所以更适合:

  • 小工具
  • 快速验证
  • Demo 代码

如何判断错误类型?

Rod 并没有搞什么"黑魔法",依然是标准 Go 写法。

go 复制代码
func handleError(err error) {
    var evalErr *rod.EvalError
		
		// Is:这个错是不是"它"
    if errors.Is(err, context.DeadlineExceeded) {
        // 超时错误
        fmt.Println("timeout err")
		
		// As:这个错能不能"变成它"
    } else if errors.As(err, &evalErr) {
        // JS 执行错误
        fmt.Println(evalErr.LineNumber)

    } else if err != nil {
        fmt.Println("can't handle", err)
    }
}

常见错误类型

  • context.DeadlineExceeded → 超时
  • *rod.EvalError → JS 执行错误
  • 其它 error → 网络 / DOM / CDP 问题

结语

这篇文章其实写得不算"重",更多是一个思路上的对比,而不是一篇手把手的教程。

我没有去详细展开每一个 API,也没有把 chromedp 或 Rod 的能力逐个拆解,因为在我看来,这些内容更适合通过官方文档和实际动手去掌握。像 CDP、本身的浏览器机制、XPath / CSS Selector,这些才是真正的"基本功",也是绕不开的部分。

如果从工程体验上来说,我个人是更偏向 Rod 的。它在调试体验、API 设计以及整体"顺手程度"上,确实更贴近开发者直觉,用起来也更舒服一些。

但如果把视野放大一点,和 Python 生态里的 Playwright 去对比,我还是会更推荐后者。原因也很现实:

  • 生态更成熟
  • 社区更活跃
  • 更新频率更高
  • 实战资料更多

尤其是在爬虫和信息采集场景中,真正的瓶颈往往并不在语言本身,而是在 网络 IO、反爬策略、数据处理链路 这些地方。在这种情况下,Python 依然是一个非常"能打"的选择。

那为什么还要写这篇文章?

其实很简单:

给 Go 提供一种可能性。

当你已经在用 Go 构建系统,或者某些场景下需要更好的并发控制、部署体验时,像 chromedp 和 Rod 这样的工具,至少能让你不用"被迫切换语言"。

它们可能不是最主流的选择,但在合适的场景下,是完全可用、甚至很好用的。

至于什么时候该用 Go,什么时候该用 Python------

这个问题本身,可能比工具的选择更值得思考。

相关推荐
前端郭德纲2 小时前
JavaScript 原型相关属性详解
开发语言·javascript·原型模式
于先生吖2 小时前
基于 SpringBoot 架构,高性能 JAVA 动漫短剧系统源码
java·开发语言·spring boot
无限进步_2 小时前
【C++&string】寻找字符串中第一个唯一字符:两种经典解法详解
开发语言·c++·git·算法·github·哈希算法·visual studio
@atweiwei2 小时前
Go语言面试篇数据结构底层原理精讲(上)
数据结构·面试·golang
jwn9993 小时前
Laravel11.x新特性全解析
android·开发语言·php·laravel
feifeigo1233 小时前
航天器交会的分布式模型预测控制(DMPC)MATLAB实现
开发语言·分布式·matlab
于先生吖3 小时前
支持二开与商用,JAVA 漫剧付费观看系统完整源码
java·开发语言
环黄金线HHJX.3 小时前
【从0到1】
开发语言·人工智能·算法·交互
曹牧3 小时前
Java: 从oracle表中获取一组kv序列
java·开发语言·oracle
呆萌很3 小时前
【GO】结构体方法练习题
golang