目录
- 引子
- [Chrome DevTools](#Chrome DevTools)
-
- [Chrome DevTools Protocol (CDP)](#Chrome DevTools Protocol (CDP))
-
- [Domain 分类](#Domain 分类)
- [targetId 解析](#targetId 解析)
- [chromedp vs rod](#chromedp vs rod)
- 结语
引子
在现代软件开发中,自动化浏览器操作已经不仅仅是测试的需求,而是成为了数据抓取、智能代理和自动化工具的重要组成部分。过去,开发者常常依赖 Selenium 这类传统方案,通过浏览器驱动直接操作浏览器。这种方式虽然成熟,但存在启动慢、资源占用高、跨平台配置复杂等问题,越来越难以满足现代化场景的需求。
对于 Python 用户来说,官方推荐使用 Playwright ,它提供了完整的 API,自动管理浏览器二进制,无需依赖 Node,就能实现可靠且高效的浏览器自动化。而对于 Go 开发者来说,虽然也存在官方的 playwright-go 封装,但其本质仍然是调用 Node 版 Playwright,依赖外部进程和 Node 环境,因此在实际工程中并不适合。
相比之下,Go 社区提供了 chromedp 和 rod 这两种基于 Chrome DevTools Protocol (CDP) 的原生方案。它们直接通过 CDP 与浏览器通信,无需额外依赖,性能优异,能够实现类似 Playwright 的浏览器控制能力,更加贴近现代化的自动化需求。对于希望在 Go 中构建高效、工程化的浏览器工具或智能代理系统的开发者而言,学习和掌握 chromedp 与 rod 无疑是更值得投入的方向。
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的浏览器,按 F12 或 Ctrl+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) 两个阶段:
-
Request(请求)
- 指定要执行的操作(command)
- 可以包含操作所需的参数(params)
- 通过 JSON-RPC 消息发送给浏览器
-
Response(响应)
-
浏览器返回执行结果
-
包含 result 或 error
- 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
}
}
💡 注意:
- 每个 request 都会返回 response,包括
result或error。- Runtime.evaluate 的
returnByValue: true表示返回值直接序列化,而非对象引用。- 某些操作可能触发额外 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.getTargets或Target.setDiscoverTargets获取或监听 Target。- Target 被关闭或卸载后,ID 失效,需要重新获取新的 Target。
targetId的作用层级:
1️⃣ CDP Method(${domain}.${command})的 Request/Response
-
作用层级:操作具体浏览器能力(页面导航、DOM 查询、JS 执行等)。
-
流程:
- Agent 发送一个 Request(包含 domain.command + params)
- 浏览器执行命令
- 返回 Response(包含 result 或 error)
- 有些命令还会触发 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 语言生态中,有两个成熟的库可以帮助我们更轻松地完成这些工作:chromedp 和 Rod。
- 它们都基于 CDP,但对开发者屏蔽了底层的复杂细节。
- 你无需手动管理 JSON-RPC 消息,也无需处理 targetId 的生命周期。
- 开发者可以用更直观、更 Go 风格的 API 来操作浏览器,实现页面导航、元素操作、JavaScript 执行、截图等功能。
换句话说,如果 CDP 是"手工驾驶的跑车",那么 chromedp 和 Rod 就是"自动驾驶模式",让我们专注于业务逻辑,而不用纠结底层协议。
好的,我帮你把 chromedp 和 Rod 的详细介绍 写出来,并加上一个 详细对比表格,内容可直接放到博客中。排版清晰,强调功能和适用场景。
简单对比
在 Go 生态中,如果我们想绕过 CDP 的底层复杂性,高效操作浏览器,chromedp 和 Rod 是目前最常用的两个库。下面分别详细介绍它们的特点与使用方式。
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 上执行远程浏览器任务
}
✅ 说明
- 默认上下文适合快速启动浏览器并执行任务。
- 本地浏览器可以自定义 Chrome 路径、是否无头、启动参数等。
- 远程 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------
这个问题本身,可能比工具的选择更值得思考。