基于Chrome DevTools Protocol的Go浏览器自动化:Gbrow实战与优化
1. 项目概述:一个被低估的浏览器自动化利器
如果你经常和网页数据打交道,或者需要自动化一些重复的浏览器操作,那么你肯定听说过或者用过 Selenium、Puppeteer 这类工具。它们功能强大,但有时候也显得“笨重”——需要安装浏览器驱动、管理复杂的版本兼容性,写起代码来配置项一大堆。今天要聊的这个项目Gbrow,在我看来,是一个被严重低估的轻量级替代方案。它没有 Selenium 那么庞大的生态,也没有 Playwright 那么全面的功能覆盖,但它精准地解决了一个核心痛点:用最简单、最直接的方式,通过 Go 语言来控制一个无头 Chrome/Chromium 浏览器,执行网页操作并获取结果。
我第一次接触 Gbrow 是在一个需要快速爬取大量动态渲染页面的小项目中。当时被 Selenium 的驱动问题和资源占用搞得有点烦,就想找找有没有更“Go 风格”(简单、高效、单一职责)的工具。Gbrow 的 README 非常简洁,几乎没有任何废话,这反而吸引了我。它的核心思路是直接利用 Chrome DevTools Protocol (CDTP),通过 WebSocket 与浏览器实例通信,绕过了所有中间驱动层。这意味着更少的依赖、更直接的操控和理论上更好的性能。经过几个项目的实战,我发现对于中等复杂度的网页自动化任务,Gbrow 完全能够胜任,并且能带来意想不到的开发效率提升。这篇文章,我就来详细拆解一下这个工具,分享我的使用心得和避坑指南。
2. 核心设计思路与技术选型解析
2.1 为什么选择 Chrome DevTools Protocol (CDTP)
要理解 Gbrow 的优雅之处,必须先明白它底层依赖的技术:Chrome DevTools Protocol。这不是一个新东西,它是 Chrome/Chromium 浏览器内置的一个基于 JSON-RPC 的调试协议。我们平时按 F12 打开的开发者工具,其所有功能(检查元素、监控网络、执行控制台命令)都是通过这个协议与浏览器内核通信实现的。
Gbrow 的选择非常聪明。它没有去自己实现一个浏览器渲染引擎(那是巨量工程),也没有去封装一个像 ChromeDriver 这样的中间层(Selenium 的方案)。而是直接“对话”浏览器内核。这样做有几个显著优势:
- 无驱动依赖:你不需要单独下载和管理 ChromeDriver,只要系统有一个 Chrome 或 Chromium 浏览器即可。版本兼容性问题大大减少。
- 功能原生且强大:CDTP 暴露了浏览器几乎所有的底层能力,包括 DOM 操作、网络拦截、JavaScript 执行、性能分析等。这意味着 Gbrow 理论上可以实现任何开发者工具能做的事情。
- 协议稳定:CDTP 由 Google 维护,虽然也会迭代,但其核心接口相对稳定,这为上层库的稳定性提供了基础。
- 跨平台一致:只要浏览器支持 CDTP(现代 Chrome/Chromium/Edge 都支持),Gbrow 的行为在不同操作系统上就是一致的。
当然,直接使用 CDTP 也有挑战,主要是协议比较底层,消息格式复杂。Gbrow 的价值就在于它封装了这些复杂性,提供了友好的 Go API。
2.2 Gbrow 的架构与核心抽象
Gbrow 的代码库不大,结构清晰。它主要做了以下几层抽象:
- 连接层:负责启动浏览器进程(或连接到已有实例),并建立 WebSocket 连接。这是通过
chromedp/launcher包(通常与chromedp项目配合使用,Gbrow 早期版本或某些用法会涉及)或直接指定浏览器路径和调试端口来实现的。 - 会话层:对应 CDTP 中的 “Target”。一个浏览器标签页(Tab)就是一个 Target。Gbrow 的
Browser和Page结构体管理了这些会话的生命周期。 - 领域层:这是核心。Gbrow 将 CDTP 的不同功能域(Domain)封装成了 Go 的包或结构体方法。例如:
DOM: 用于查询、修改文档对象模型。Runtime: 用于执行 JavaScript 代码,处理异常。Network: 用于监听网络请求和响应,可以启用/禁用缓存,模拟离线状态。Page: 用于控制页面导航、截图、打印PDF等。Input: 用于模拟鼠标点击、键盘输入等用户交互。
- 工具层:提供了一些更高级的、组合操作的便利函数。例如,等待某个元素出现、获取元素的属性或文本等。这些函数内部也是调用底层领域 API 实现的。
这种架构使得 Gbrow 既保持了底层协议的灵活性(你可以直接发送原始的 CDTP 命令),又提供了足够便捷的高级 API 来覆盖大部分常见场景。
注意:Gbrow 项目本身可能处于维护状态或活跃度不高,社区更主流的同类库是
chromedp。但理解 Gbrow 的设计思想对于使用任何基于 CDTP 的库都有帮助。下文的部分实操示例和思路是相通的,我会指出其中的关键点。
3. 环境准备与基础实操
3.1 安装与最小化示例
首先,确保你的 Go 开发环境(Go 1.16+)已经就绪。然后获取 Gbrow:
go get -u github.com/ashish797/Gbrow由于 Gbrow 需要与一个真实的 Chrome/Chromium 浏览器交互,所以你必须确保系统中已安装。在 macOS 上可以用 Homebrew (brew install --cask google-chrome),在 Ubuntu/Debian 上可以用apt,在 Windows 上则直接下载安装包。
下面是一个最基础的示例,打开一个页面,获取其标题:
package main import ( "context" "fmt" "log" "time" "github.com/ashish797/Gbrow" ) func main() { // 1. 创建上下文,用于控制超时和取消 ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) defer cancel() // 2. 启动浏览器。这里使用默认配置,启动一个无头模式的新实例。 // 关键点:`Gbrow.New` 内部会尝试查找系统 Chrome 路径并启动。 browser, err := Gbrow.New(ctx) if err != nil { log.Fatalf("无法启动浏览器: %v", err) } defer browser.Close() // 确保程序退出前关闭浏览器,释放资源 // 3. 创建一个新的页面(标签页) page, err := browser.NewPage(ctx) if err != nil { log.Fatalf("无法创建新页面: %v", err) } // 4. 导航到目标网址 err = page.Navigate(ctx, "https://example.com") if err != nil { log.Fatalf("导航失败: %v", err) } // 5. 等待页面加载(简单起见,这里用固定等待。生产环境应用更智能的等待策略) time.Sleep(2 * time.Second) // 6. 执行JavaScript代码来获取页面标题 title, err := page.Evaluate(ctx, `() => document.title`) if err != nil { log.Fatalf("执行JS失败: %v", err) } // 7. 输出结果 fmt.Printf("页面标题: %s\n", title) }这个例子揭示了几个关键操作:启动、创建页面、导航、执行 JS。但其中time.Sleep(2 * time.Second)是非常不推荐的写法,我们马上会讲如何优化。
3.2 浏览器启动参数详解
直接使用Gbrow.New(ctx)会采用默认参数。但在实际项目中,我们经常需要定制浏览器的行为。这时需要用到Gbrow.NewWithOptions或理解如何配置启动器。
一个更健壮的启动方式可能如下(这里以结合chromedp/launcher为例,因为 Gbrow 内部或社区实践常如此):
import ( "github.com/chromedp/chromedp/launcher" ) func main() { ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second) defer cancel() // 使用 launcher 库进行更精细的控制 l := launcher.New(). Headless(true). // 无头模式,不显示GUI Set("disable-gpu", ""). // 在某些虚拟化环境中可能需要 Set("no-sandbox", ""). // 在Docker或某些Linux环境中需要,但有安全风险 Set("disable-dev-shm-usage", ""). // 解决共享内存问题,常见于Docker Set("window-size", "1920,1080"). // 设置视口大小 Set("blink-settings", "imagesEnabled=false"). // 禁止加载图片,加速 UserDataDir("/tmp/custom_profile") // 设置用户数据目录,可以保存Cookie、缓存 // 启动浏览器,获取WebSocket调试URL wsURL, err := l.Launch(ctx) if err != nil { log.Fatal(err) } defer l.Kill() // 确保关闭 // 使用 Gbrow 连接到这个已启动的浏览器实例 browser, err := Gbrow.Connect(ctx, wsURL) if err != nil { log.Fatal(err) } defer browser.Close() // ... 后续页面操作 }关键启动参数解析:
headless: 无头模式是自动化测试和爬虫的标配,节省资源。调试时可以设为false以便观察。no-sandbox&disable-dev-shm-usage: 在 Docker 容器或 CI/CD 环境中运行时,几乎总是需要这两个参数来避免启动崩溃。但请注意,no-sandbox降低了安全性,仅应在受控环境使用。window-size: 设置浏览器窗口(视口)大小。很多现代网站是响应式的,视口大小会影响页面布局和加载的元素。blink-settings=imagesEnabled=false: 这是一个提速技巧。如果目标数据不依赖图片,禁用图片加载可以显著减少网络请求和渲染时间。user-data-dir: 指定用户数据目录。这允许你持久化会话(如登录状态)、缓存和扩展。对于需要登录的网站,先手动登录一次并保存到这个目录,后续自动化脚本就可以直接携带Cookie了,非常有用。
4. 核心操作:导航、等待与元素交互
4.1 智能等待策略(告别 Sleep)
前面例子中的time.Sleep是万恶之源。网络速度和页面复杂度不确定,固定等待要么太慢(浪费时间),要么太快(元素还没加载出来导致失败)。Gbrow 基于 CDTP,提供了多种等待条件。
最佳实践:组合使用多种等待条件
等待导航完成:
page.Navigate本身返回时,只代表导航指令已发出,不保证页面加载完成。CDTP 有Page.loadEventFired事件。在 Gbrow 中,你可能需要监听事件或使用page.WaitForLoadState(如果API提供)或通过执行JS判断document.readyState。// 假设有一个 WaitLoad 辅助函数 err = page.Navigate(ctx, "https://example.com") if err != nil { ... } err = waitForLoad(ctx, page) // 自定义函数,内部通过事件或轮询实现等待特定元素出现:这是最常用的等待。可以通过 DOM 选择器、XPath 或文本内容来等待。
// 思路:循环查询,直到找到元素或超时 func waitForSelector(ctx context.Context, page *Gbrow.Page, selector string) error { deadline, ok := ctx.Deadline() if !ok { deadline = time.Now().Add(30 * time.Second) } for time.Now().Before(deadline) { // 使用 Evaluate 执行JS查询元素 exists, err := page.Evaluate(ctx, fmt.Sprintf(`() => document.querySelector('%s') !== null`, selector)) if err != nil { return err } if exists.(bool) { return nil // 找到了 } select { case <-time.After(200 * time.Millisecond): // 轮询间隔 continue case <-ctx.Done(): return ctx.Err() } } return fmt.Errorf("等待元素超时: %s", selector) }在实际使用中,你应该封装一个健壮的
WaitVisible函数,它结合了元素存在和可见性(offsetParent !== null且样式非隐藏)。等待网络空闲:对于单页应用 (SPA),页面初始加载后,数据可能通过 AJAX/Fetch 动态加载。可以监听网络请求,当一段时间内没有网络活动时,认为页面“稳定”了。CDTP 的
Network领域可以启用请求追踪。
4.2 元素定位与操作
定位到元素后,就可以进行交互了。Gbrow 的核心是通过page.Evaluate执行 JavaScript 来操作 DOM。
获取元素属性和文本:
// 获取单个元素的文本内容 text, err := page.Evaluate(ctx, `() => { const el = document.querySelector('#main .title'); return el ? el.textContent.trim() : null; }`) if err != nil { ... } fmt.Printf("标题文本: %v\n", text) // 获取多个元素(例如列表) items, err := page.Evaluate(ctx, `() => { const nodes = document.querySelectorAll('.item-list li'); return Array.from(nodes).map(li => li.textContent.trim()); }`) if err != nil { ... } if list, ok := items.([]interface{}); ok { for i, item := range list { fmt.Printf("Item %d: %s\n", i+1, item) } }模拟用户交互:
模拟点击、输入等操作,需要用到 CDTP 的Input领域。Gbrow 可能封装了相关方法,如果没有,你需要直接发送 CDTP 命令。
// 假设 page 有 Click 和 Type 方法(或类似功能) // 1. 点击一个按钮 err = page.Click(ctx, `#submit-button`) if err != nil { ... } // 2. 在输入框中输入文本 err = page.Type(ctx, `#search-input`, "Go语言编程") if err != nil { ... } // 如果Gbrow没有直接封装,你需要构造Input.dispatchMouseEvent和Input.dispatchKeyEvent命令 // 这涉及到计算元素的坐标,相对复杂。这也是为什么很多人选择 chromedp 的原因,它封装了这些高级动作。执行复杂 JavaScript:
page.Evaluate是你的瑞士军刀。你可以把任何复杂的逻辑封装成一个 JS 函数执行,并返回结果给 Go。
// 示例:滚动到页面底部,并检测是否已滚动到底 var isAtBottom bool for i := 0; i < 10; i++ { // 最多尝试滚动10次 isAtBottom, err = page.Evaluate(ctx, `() => { const scrollHeight = document.documentElement.scrollHeight; const clientHeight = document.documentElement.clientHeight; const scrollTop = document.documentElement.scrollTop || document.body.scrollTop; // 滚动到底部 window.scrollTo(0, scrollHeight); // 判断是否已到底 return scrollTop + clientHeight >= scrollHeight - 10; // 允许10像素误差 }`) if err != nil { ... } if isAtBottom.(bool) { break } time.Sleep(1 * time.Second) // 等待新内容加载 }5. 高级技巧与性能优化
5.1 网络请求拦截与修改
这是 CDTP 非常强大的一个功能。你可以监听所有网络请求,并选择性地阻止、修改或 mock 响应。这对于测试、性能分析或爬取特定资源非常有用。
// 伪代码,展示思路 // 1. 启用 Network 领域 err = browser.Send(ctx, &gcdapi.NetworkEnable{}) if err != nil { ... } // 2. 监听请求事件 browser.RegisterEvent("Network.requestWillBeSent", func(event interface{}) { // 类型断言获取请求详情 // 可以在这里记录请求,或根据URL决定是否拦截 }) // 3. 监听响应事件 browser.RegisterEvent("Network.responseReceived", func(event interface{}) { // 获取响应详情,包括状态码、头部、body(可能需要额外调用获取) }) // 4. 拦截并修改请求(需要调用 Network.setRequestInterception) // 然后监听 Network.requestIntercepted 事件,并决定是继续、修改还是返回自定义响应。实战应用:
- 屏蔽广告和跟踪器:拦截 URL 匹配特定模式的请求(如
ads.,track.),直接中止 (Fail命令)。 - 注入 Mock 数据:对于特定的 API 请求,直接返回预先准备好的 JSON 数据,用于前端测试或开发。
- 资源替换:将请求的某个 CSS 或 JS 文件替换成本地版本。
- 性能监控:统计所有请求的耗时、大小,找出性能瓶颈。
5.2 处理弹窗、新标签页和 iframe
- 弹窗 (Alert, Confirm, Prompt):CDTP 的
Page领域可以监听javascriptDialogOpening事件,并通过Page.handleJavaScriptDialog命令来接受或取消,甚至可以输入文本(针对 Prompt)。 - 新标签页/窗口:监听
Target.targetCreated事件。当新标签页创建时,你可以选择连接到这个新 Target,或者忽略/关闭它。 - iframe:iframe 内部是一个独立的文档。你需要先获取到 iframe 的
FrameId,然后在后续的 DOM 操作命令中指定这个 FrameId,才能操作 iframe 内的元素。Gbrow 的 API 可能需要你显式切换到 iframe 的上下文。
5.3 性能优化与资源管理
浏览器自动化是资源密集型任务。以下优化手段能显著提升稳定性和效率:
复用浏览器实例:最昂贵的操作是启动浏览器。对于需要处理大量页面的任务,应该启动一个浏览器实例,然后创建多个页面(Tab)来并行或串行处理,最后统一关闭浏览器。避免为每个任务都启动/关闭一次浏览器。
合理设置超时:为每个操作(导航、等待、查询)设置独立的、合理的超时时间。使用
context.WithTimeout创建子上下文。全局超时要足够长,局部操作超时可以短一些。限制并发:即使使用多个 Page,并发数也不宜过高。一个浏览器进程的内存和CPU占用是有限的。通常,根据机器配置,并发 5-10 个页面是比较稳妥的。可以使用 Go 的
goroutine配合semaphore(信号量)或worker pool模式来控制。清理资源:
- 及时关闭不再需要的 Page (
page.Close)。 - 在导航到新页面前,可以考虑清理旧页面的 JS 内存(通过
Runtime领域的collectGarbage命令,但效果有限)。 - 监控浏览器进程的内存使用,如果异常增长,可能需要重启浏览器实例。
- 及时关闭不再需要的 Page (
无头模式与渲染优化:
- 始终使用无头模式 (
headless=true)。 - 禁用图片、CSS、字体等非必要资源(通过
Network拦截或启动参数blink-settings)。 - 禁用 GPU 加速 (
disable-gpu)。 - 使用固定的、适中的视口大小。
- 始终使用无头模式 (
6. 常见问题排查与调试技巧
6.1 典型错误与解决方案
| 问题现象 | 可能原因 | 解决方案 |
|---|---|---|
| 浏览器启动失败,报错关于沙箱 | 运行在 Docker 或受限的 Linux 环境(如某些 CI) | 添加启动参数--no-sandbox和--disable-dev-shm-usage。注意安全风险。 |
执行Evaluate返回nil或错误 | 1. 元素选择器写错,没找到元素。 2. 页面尚未加载完成,元素不存在。 3. JS 代码本身有语法错误。 | 1. 在浏览器开发者工具中测试选择器。 2. 添加足够的等待(智能等待,非 Sleep)。 3. 先在浏览器控制台测试 JS 代码片段。 |
| 页面卡住,操作超时 | 1. 页面有未处理的模态弹窗。 2. 页面 JS 报错导致后续逻辑中断。 3. 网络请求慢或失败。 4. 死循环或长时间同步 JS 执行。 | 1. 监听并处理javascriptDialogOpening事件。2. 监听 Runtime.exceptionThrown事件,记录错误。3. 增加超时时间,或检查网络拦截规则。 4. 难以避免,可设置强制超时并重启页面。 |
| 内存使用持续增长 | 1. 页面缓存未清理。 2. 浏览器实例或页面未正确关闭。 3. 目标网站本身有内存泄漏。 | 1. 定期导航到about:blank或关闭重开页面。2. 确保 defer browser.Close()和defer page.Close()被调用。3. 限制单个页面的生命周期,定期重启整个浏览器进程。 |
| 无法在 iframe 内操作元素 | 操作上下文仍在主文档 | 需要先获取 iframe 的FrameId,然后通过 CDTP 的DOM.resolveNode或类似方法,将操作上下文切换到 iframe。 |
| 模拟点击/输入无效 | 1. 元素被遮挡。 2. 元素是 <div>模拟的按钮,需要触发特定事件。3. 坐标计算错误。 | 1. 检查元素是否可见 (offsetParent,visibility,display)。2. 尝试直接执行 element.click()JS 事件,而非模拟鼠标事件。3. 使用 DOM.getBoxModel获取精确坐标,或直接使用 JS 点击。 |
6.2 调试技巧:让不可见的过程可见
- 关闭无头模式:在开发阶段,将
headless设为false。你会看到一个真实的浏览器窗口在操作,直观看到哪里出错了。 - 启用详细日志:许多 CDTP 客户端库(包括 Gbrow 可能依赖的底层库)支持日志输出。启用它可以看到所有发送和接收的 CDTP 命令,对于理解底层交互和排查协议级错误至关重要。
- 保存截图和 HTML:在关键步骤失败时,自动保存当前页面的截图 (
Page.captureScreenshot命令) 和 HTML 源码 (Page.getContent或执行document.documentElement.outerHTML)。这是事后分析的黄金资料。// 截图示例 buf, err := page.CaptureScreenshot(ctx) if err == nil { ioutil.WriteFile("debug_screenshot.png", buf, 0644) } - 注入调试代码:在页面中注入你自己的 JS 代码,用于监控状态或输出日志。
page.Evaluate(ctx, `() => { // 重写 console.log,使其输出也通过CDTP传回Go端(需要监听Runtime.consoleAPICalled事件) // 或者简单地在页面中插入一个可见的调试面板 }`) - 使用
Runtime.evaluate的returnByValue:当从页面获取复杂对象时,使用returnByValue: true选项可以避免引用问题,直接将对象序列化后返回。
6.3 与更成熟库(如 chromedp)的对比与选择
Gbrow 展示了基于 CDTP 的 Go 库的核心思想。但目前 Go 生态中更活跃、功能更完善的是 chromedp 。如果你的项目需要投入生产,我建议优先考虑chromedp。
chromedp 的优势:
- 更高的抽象层级:提供了
chromedp.Click,chromedp.SendKeys,chromedp.WaitVisible等语义化操作,无需手动拼写 JS。 - 强大的任务流系统:使用
chromedp.Tasks(一个Action接口的切片) 来组织一系列操作,代码更清晰,且自带智能等待。 - 更活跃的社区和维护:Issue 和 PR 响应更快,文档更全面。
- 内置更多实用功能:如文件下载、更简单的截图、PDF 生成等。
那么 Gbrow 的价值何在?
- 学习价值:代码更简洁,是理解 CDTP 与 Go 如何交互的绝佳教材。
- 轻量级需求:如果你的需求极其简单,只是打开页面、执行一两句 JS 并获取结果,Gbrow 的轻量可能更合适。
- 定制化基础:如果你想构建一个高度定制化的浏览器自动化框架,以 Gbrow 这种更接近协议层的基础进行开发,可能比在 chromedp 上改造更直接。
迁移建议:如果你从 Gbrow 起步,理解了核心概念,当遇到功能瓶颈时,迁移到 chromedp 是相对平滑的,因为它们的底层原理完全相同。你的经验(等待策略、启动参数、异常处理)都可以直接复用。
7. 实战案例:构建一个简单的商品价格监控器
让我们用一个实际例子来串联以上知识。假设我们需要监控某个电商网站(例如一个示例网站demo-shop.com)上某件商品的价格变化。
目标:每30分钟检查一次商品页面,如果价格低于设定阈值,则发送通知。
核心步骤:
- 启动并配置浏览器(无头,禁用图片)。
- 导航到商品页面。
- 等待价格元素加载完成。
- 提取价格文本,并转换为数值。
- 与阈值比较。
- 清理资源。
- 使用定时任务调度。
package main import ( "context" "fmt" "log" "regexp" "strconv" "time" "github.com/chromedp/chromedp/launcher" // 这里使用 launcher 启动 "github.com/ashish797/Gbrow" ) func scrapePrice(ctx context.Context, url string) (float64, error) { // 启动浏览器 l := launcher.New(). Headless(true). Set("disable-gpu", ""). Set("no-sandbox", ""). Set("disable-dev-shm-usage", ""). Set("blink-settings", "imagesEnabled=false") wsURL, err := l.Launch(ctx) if err != nil { return 0, fmt.Errorf("启动浏览器失败: %w", err) } defer l.Kill() browser, err := Gbrow.Connect(ctx, wsURL) if err != nil { return 0, fmt.Errorf("连接浏览器失败: %w", err) } defer browser.Close() page, err := browser.NewPage(ctx) if err != nil { return 0, fmt.Errorf("创建页面失败: %w", err) } defer page.Close() // 导航 err = page.Navigate(ctx, url) if err != nil { return 0, fmt.Errorf("导航失败: %w", err) } // 智能等待:等待价格元素出现(假设其CSS选择器是 `.product-price`) priceSelector := `.product-price` var priceText string // 这里需要一个自定义的 waitAndGetText 函数,结合轮询和JS执行 err = waitAndGetText(ctx, page, priceSelector, &priceText) if err != nil { return 0, fmt.Errorf("等待或获取价格文本失败: %w", err) } // 清洗和转换价格文本,例如从 "$123.45" 或 "¥1,234" 中提取数字 re := regexp.MustCompile(`[0-9,.]+`) matches := re.FindStringSubmatch(priceText) if len(matches) == 0 { return 0, fmt.Errorf("无法从文本 '%s' 中解析出价格", priceText) } numStr := matches[0] // 处理千位分隔符 numStr = regexp.MustCompile(`,`).ReplaceAllString(numStr, "") price, err := strconv.ParseFloat(numStr, 64) if err != nil { return 0, fmt.Errorf("价格转换失败 '%s': %w", numStr, err) } return price, nil } // waitAndGetText 是一个辅助函数,等待元素出现并获取其文本 func waitAndGetText(ctx context.Context, page *Gbrow.Page, selector string, result *string) error { // ... 实现轮询逻辑,使用 page.Evaluate 检查元素并获取 textContent // 这里省略具体实现,参考前面的 waitForSelector 思路 return nil } func main() { productURL := "https://demo-shop.com/product/123" threshold := 100.0 ticker := time.NewTicker(30 * time.Minute) defer ticker.Stop() for { select { case <-ticker.C: ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second) price, err := scrapePrice(ctx, productURL) cancel() if err != nil { log.Printf("抓取失败: %v", err) continue } log.Printf("当前价格: %.2f", price) if price < threshold { log.Printf("【警报】价格低于阈值 %.2f! 当前: %.2f", threshold, price) // 这里可以集成邮件、钉钉、Telegram等通知 // sendNotification(fmt.Sprintf("商品降价啦!当前价格 %.2f", price)) } } } }这个案例的优化点:
- 浏览器复用:目前的代码每次抓取都启动新浏览器,开销大。应该改为启动一个常驻浏览器进程,每次抓取创建新页面。
- 错误恢复:网络波动或页面结构微调可能导致失败。应加入重试机制,并在连续失败后报警。
- 反爬应对:频繁访问可能触发反爬。需要添加随机延迟、使用代理池、管理 Cookies 等策略。
- 结构化数据提取:价格可能只是我们需要信息的一部分。可以扩展脚本,同时抓取商品名称、库存状态、评分等。
通过这个案例,你可以看到,基于 Gbrow(或类似 CDTP 库)的核心思路,我们可以构建出功能强大的网页自动化工具。关键在于理解浏览器协议、设计稳健的等待与错误处理逻辑,以及做好资源管理。
