最近有项目需要使用无头浏览器进行后台任务处理。古早以前使用 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加持下分分钟搞定。可以迎接端午安康了!