玩转chromedp

最近有项目需要使用无头浏览器进行后台任务处理。古早以前使用 PhantomJS 进行无头浏览器操作,但现在已经不再维护了。现在推荐使用 Chrome DevTools Protocol (CDP) 来实现无头浏览器的功能。

Chrome DevTools Protocol (CDP)

CDP 是 Chrome 浏览器提供的一个协议,允许开发者通过编程方式控制浏览器的行为。它可以用于调试、性能分析、自动化测试等场景。

CDP 的文档可以在这里找到:Chrome DevTools Protocol

Golang 中的 CDP 实现

作为一个gopher, 综合开发、调试和部署成本,决定继续选用golang chromedp 进行无头浏览器操作。这个项目比六七年前使用涨了快10倍的star, 不得不说这个项目的生命力真的很强。

这个项目符合golang项目一贯的开箱即用风格。示例也非常丰富,因此一般的测试直接找到对应示例进行修改即可。但惟独自己计划项目中需要使用的几个特性要求找不到匹配的实现。

特性要求

  • 安全隔离,需要再远程服务运行浏览器
  • 响应时效,页面打开机操作与本地打开不能有显著差异
  • 成本控制,单台服务器上需要支持多个浏览器实例运行,上下文隔离
  • 业务流程匹配,主要是需要对页面操作支持中断操作和持续执行

代码是不可能自己亲自花时间写的。借用cursor, 花了两分钟生成,两分钟调整代码结构:

package main

import (
"context"
"log"
"os"
"time"

"github.com/chromedp/cdproto/network"
"github.com/chromedp/cdproto/target"
"github.com/chromedp/chromedp"
)

const cookie = "TODO"

const REMOTE_ADDR = "ws://127.0.0.1:9222"

const REMOTE_ALLOC_CTX = true

func main() {

id, err := openBaidu()
if err != nil {
log.Fatalf("打开百度失败: %v", err)
}

search(id, "当前时间")

log.Printf("openBaidu and search success, id: %s", id)

}

func openBaidu() (target.ID, error) {
allocCtx := getAllocCtx()

// 创建一个带取消功能的上下文
ctx, _ := chromedp.NewContext(
// context.Background(),
allocCtx,
// 设置日志级别
chromedp.WithTargetID("20E9DFB7B355658007B89CC47C5BAC3A"),
)

// 截图保存路径
screenshotPath := "1.open_baidu_result.png"

// 定义截图缓冲区
var buf []byte

// 设置全局请求头,包含Cookie
// 这种方式比在每个请求上单独设置更可靠
headers := map[string]interface{}{
"Cookie": cookie,
}

// 执行任务
err := chromedp.Run(ctx,
// 首先设置全局请求头
network.SetExtraHTTPHeaders(headers),

// 导航到百度首页
chromedp.Navigate("https://www.baidu.com"),

// 等待搜索框元素加载完成
chromedp.WaitVisible("#kw", chromedp.ByID),

// 等待一下确保结果完全加载
chromedp.Sleep(1*time.Second),

// 捕获整个页面的屏幕截图
chromedp.FullScreenshot(&buf, 90),
)

if err != nil {
log.Fatalf("执行任务失败: %v", err)
return "", err
}

// 保存截图
if err := os.WriteFile(screenshotPath, buf, 0644); err != nil {
log.Fatalf("保存截图失败: %v", err)
return "", err
}

cdpCtx := chromedp.FromContext(ctx)
return cdpCtx.Target.TargetID, nil
}

func search(id target.ID, query string) {

allocCtx := getAllocCtx()

ctx, _ := chromedp.NewContext(allocCtx, chromedp.WithTargetID(id))

// 定义截图缓冲区
var buf []byte

chromedp.Run(ctx,
chromedp.SendKeys("#kw", query, chromedp.ByID),
chromedp.Click("#su", chromedp.ByID),
chromedp.WaitVisible("#content_left", chromedp.ByID),
// 捕获整个页面的屏幕截图
chromedp.Sleep(1*time.Second),
chromedp.FullScreenshot(&buf, 90),
)

// 保存截图
if err := os.WriteFile("2.search_result.png", buf, 0644); err != nil {
log.Fatalf("保存截图失败: %v", err)
}
log.Printf("保存截图到 %s\n", "search_result.png")
}

func getAllocCtx() context.Context {
if REMOTE_ALLOC_CTX {
allocCtx, _ := chromedp.NewRemoteAllocator(context.Background(), REMOTE_ADDR)
return allocCtx
} else {
opts := append(chromedp.DefaultExecAllocatorOptions[:],
chromedp.Flag("headless", false),
)
allocCtx, _ := chromedp.NewExecAllocator(context.Background(), opts...)
return allocCtx
}
}

简单说明一下:

  • 前3个特性要求
  • 通过 chromedp.NewRemoteAllocator 创建远程浏览器实例,支持多实例隔离
  • 通过 chromedp.WithTargetID 设置浏览器实例 ID,支持tab页面关联,实现中断交互

本来计划要花点时间的任务,在AI加持下分分钟搞定。可以迎接端午安康了!