chromedp是一个更快、更简单的Golang库,用于调用支持Chrome DevTools Protocol的浏览器,同时不需要额外的依赖(例如Selenium和PhantomJS)。
Chrome和Golang都与Google有着相当密切的关系,而Chrome DevTools其实就是Chrome浏览器按下F12之后的控制终端。
核心特点:
纯Go实现,无需外部驱动
直接使用Chrome DevTools Protocol
性能优异,资源占用少
支持无头模式和有头模式
功能强大,API简洁
对于Golang开发来说,使用chromedp更为便捷,主要优势包括:
零依赖部署:仅需要Chrome浏览器,不需要依赖ChromeDriver
简化部署流程:省去了依赖问题,有助于自动化构建和多平台架构迁移
原生Go支持:更好的类型安全和性能表现
更轻量级:减少了中间层的开销
Chromedp适用于以下场景:
自动化测试
网页爬虫开发
网页截图与PDF生成
模拟用户行为
性能监控与分析
网站自动登录与Token捕获
网络请求拦截与分析
前置要求:
下载并安装Chrome浏览器
安装Golang(推荐1.16+)
安装Chromedp库:
# 创建项目并初始化Go Module
mkdir chromedp-project
cd chromedp-project
go mod init chromedp-project
# 安装chromedp
go get -u github.com/chromedp/chromedp
package main
import (
"context"
"log"
"time"
"github.com/chromedp/chromedp"
)
func main() {
// chromedp依赖context上下文传递参数
ctx, cancel := chromedp.NewExecAllocator(
context.Background(),
// 以默认配置的数组为基础,覆写headless参数
append(
chromedp.DefaultExecAllocatorOptions[:],
chromedp.Flag("headless", false),
)...,
)
defer cancel()
// 创建chromedp上下文对象
ctx, cancel = chromedp.NewContext(
ctx,
chromedp.WithLogf(log.Printf),
)
defer cancel()
// 设置超时时间
ctx, cancel = context.WithTimeout(ctx, 30*time.Second)
defer cancel()
// 执行任务
if err := chromedp.Run(ctx, chromedp.Navigate("https://example.com")); err != nil {
log.Fatal(err)
}
}
func main() {
// 使用默认配置(默认为无头模式)
ctx, cancel := chromedp.NewContext(context.Background())
defer cancel()
// 执行任务
if err := chromedp.Run(ctx, chromedp.Navigate("https://example.com")); err != nil {
log.Fatal(err)
}
}
func basicNavigation() chromedp.Tasks {
return chromedp.Tasks{
// 导航到指定URL
chromedp.Navigate("https://example.com"),
// 等待页面加载完成
chromedp.WaitReady("body"),
// 等待特定元素可见
chromedp.WaitVisible("#content"),
}
}
获取元素选择器的方法:
右键目标元素,选择"检查"
在开发者工具中右键元素
选择 Copy → Copy selector
func clickExample() chromedp.Tasks {
return chromedp.Tasks{
// 使用CSS选择器点击元素
chromedp.Click(`#login-button`),
// 使用ID选择器
chromedp.Click(`button`, chromedp.ByID),
// 等待元素后点击
chromedp.WaitVisible(`#submit-btn`),
chromedp.Click(`#submit-btn`),
}
}
以GitMind为例,实现完整的自动登录流程,包括密码登录、Cookie处理、Token捕获等功能。完整的登录流程包含以下步骤:
登录阶段(步骤1-8):
打开GitMind首页
处理Cookie同意弹窗
点击登录按钮
切换到密码登录方式
自动输入账号密码
勾选用户协议
提交登录
捕获登录Token(通过网络监听)
AI思维导图生成阶段(步骤9-17): 9. 点击创建按钮跳转到新页面 10. 关闭跳转后的弹窗 11. 使用真实鼠标移动触发AI工具下拉菜单 12. 点击"AI思维导图"选项 13. 点击"长文本"模式按钮 14. 在文本域输入生成需求 15. 点击"生成脑图"按钮 16. 处理协议同意弹框(如果出现) 17. 确认页面跳转,等待AI生成
// 登录流程
func gitmindLoginTasks() chromedp.Tasks {
return chromedp.Tasks{
// 步骤1: 打开GitMind首页
chromedp.Navigate(gitmindURL),
chromedp.Sleep(3 * time.Second),
// 步骤2: 处理Cookie弹窗(如果出现)
chromedp.ActionFunc(handleCookieConsent),
chromedp.Sleep(1 * time.Second),
// 步骤3: 点击登录按钮
chromedp.Click(`a.login`, chromedp.ByQuery),
chromedp.Sleep(1 * time.Second),
// 步骤4: 切换到密码登录
chromedp.Click(`.login-by-password p`, chromedp.ByQuery),
chromedp.Sleep(1 * time.Second),
// 步骤5: 输入手机号
chromedp.SendKeys(`input[name="account"]`, phoneNumber, chromedp.ByQuery),
chromedp.Sleep(500 * time.Millisecond),
// 步骤6: 输入密码
chromedp.SendKeys(`input[name="password"]`, password, chromedp.ByQuery),
chromedp.Sleep(500 * time.Millisecond),
// 步骤7: 勾选协议(如果需要)
chromedp.ActionFunc(checkAgreement),
chromedp.Sleep(500 * time.Millisecond),
// 步骤8: 提交登录
chromedp.Click(`#passwordLoginBtn`, chromedp.ByID),
chromedp.Sleep(1 * time.Second),
}
}
func afterLoginTasks() chromedp.Tasks {
return chromedp.Tasks{
// 步骤9: 点击创建按钮跳转
chromedp.ActionFunc(func(ctx context.Context) error {
log.Println("[步骤9] 点击创建按钮...")
selector := a.h-8.leading-8.break-keep.px-5.border.border-solid.rounded-\[12px\].border-black-default
return chromedp.Click(selector, chromedp.ByQuery).Do(ctx)
}),
chromedp.Sleep(3 * time.Second),
// 步骤10: 关闭跳转后的弹窗
chromedp.ActionFunc(handlePopupClose),
chromedp.Sleep(1 * time.Second),
// 步骤11: 真实鼠标移动触发AI工具下拉菜单
chromedp.ActionFunc(hoverAIButton),
chromedp.Sleep(2 * time.Second),
// 步骤12: 点击"AI思维导图"
chromedp.ActionFunc(func(ctx context.Context) error {
log.Println("[步骤12] 点击AI思维导图...")
return chromedp.Click(`.mind-dropdown-item.ai-generate`, chromedp.ByQuery).Do(ctx)
}),
chromedp.Sleep(2 * time.Second),
// 步骤13: 点击"长文本"模式
chromedp.ActionFunc(clickLongTextMode),
chromedp.Sleep(1 * time.Second),
// 步骤14: 输入生成需求
chromedp.ActionFunc(func(ctx context.Context) error {
log.Println("[步骤14] 输入生成需求...")
inputText := "请帮我生成英语四级考试的重点单词和语法考点。"
return chromedp.SendKeys(`.mind-input textarea`, inputText, chromedp.ByQuery).Do(ctx)
}),
chromedp.Sleep(1 * time.Second),
// 步骤15: 点击"生成脑图"按钮
chromedp.ActionFunc(clickGenerateButton),
chromedp.Sleep(2 * time.Second),
// 步骤16: 处理协议弹框(如果出现)
chromedp.ActionFunc(handleAgreementDialog),
chromedp.Sleep(5 * time.Second),
// 步骤17: 确认页面跳转
chromedp.ActionFunc(func(ctx context.Context) error {
var currentURL string
chromedp.Evaluate(`window.location.href`, ¤tURL).Do(ctx)
log.Printf("[步骤17] 页面已跳转: %s", currentURL)
log.Println("✓ AI正在生成思维导图...")
return nil
}),
}
}
处理可能出现的Cookie同意弹窗:
func handleCookieConsent(ctx context.Context) error {
// 检查Cookie按钮是否存在
var cookieBtnExists bool
checkScript := `
(function() {
const btn = document.querySelector('#accept-cookie-btn');
return btn && btn.offsetParent !== null;
})();
`
if err := chromedp.Evaluate(checkScript, &cookieBtnExists).Do(ctx); err != nil {
return nil // 忽略错误,继续执行
}
if cookieBtnExists {
log.Println("正在点击接受Cookie...")
chromedp.Click(`#accept-cookie-btn`, chromedp.ByID).Do(ctx)
}
return nil
}
检查并勾选用户协议:
func checkAgreement(ctx context.Context) error {
// 检查协议是否已勾选
var isChecked bool
checkScript := `
(function() {
const checkbox = document.querySelector('.checkmark');
if (!checkbox) return true;
const parent = checkbox.closest('label, div');
if (parent) {
return parent.classList.contains('checked');
}
return false;
})();
`
if err := chromedp.Evaluate(checkScript, &isChecked).Do(ctx); err != nil {
return nil
}
if !isChecked {
log.Println("正在勾选用户协议...")
chromedp.Click(`.checkmark`, chromedp.ByQuery).Do(ctx)
}
return nil
}
使用网络监听捕获登录后返回的API Token,这是验证登录成功的可靠方式。
启用网络监听:
import (
"github.com/chromedp/cdproto/cdp"
"github.com/chromedp/cdproto/network"
)
func main() {
ctx, cancel := chromedp.NewContext(context.Background())
defer cancel()
// 创建channel接收token
tokenChan := make(chan string, 1)
// 启用网络监听
chromedp.ListenTarget(ctx, func(ev interface{}) {
switch ev := ev.(type) {
case *network.EventResponseReceived:
resp := ev.Response
// 过滤登录相关的API请求
if strings.Contains(resp.URL, "/api") ||
strings.Contains(resp.URL, "login") ||
strings.Contains(resp.URL, "auth") {
// 异步获取响应体
go func() {
c := chromedp.FromContext(ctx)
rbp := network.GetResponseBody(ev.RequestID)
body, err := rbp.Do(cdp.WithExecutor(ctx, c.Target))
if err != nil {
return
}
// 解析JSON并提取token
var result map[string]interface{}
if err := json.Unmarshal(body, &result); err == nil {
token := extractToken(result)
if token != "" {
tokenChan <- token
}
}
}()
}
}
})
// 执行登录任务
chromedp.Run(ctx, gitmindLoginTasks())
// 等待token
select {
case token := <-tokenChan:
log.Printf("✓ 登录成功!Token: %s", token)
case <-time.After(5 * time.Second):
log.Println("✗ 登录超时")
}
}
Token提取函数:
func extractToken(data map[string]interface{}) string {
// 常见的token字段名
tokenFields := [ ]string{
"api_token", "token", "access_token", "accessToken",
"auth_token", "authToken", "session_token", "jwt",
}
// 1. 从根级别查找
for _, field := range tokenFields {
if val, ok := data[field]; ok {
if token, ok := val.(string); ok && token != "" {
return token
}
}
}
// 2. 从data字段中查找
if dataObj, ok := data["data"].(map[string]interface{}); ok {
for _, field := range tokenFields {
if val, ok := dataObj[field]; ok {
if token, ok := val.(string); ok && token != "" {
return token
}
}
}
}
// 3. 从result字段中查找
if resultObj, ok := data["result"].(map[string]interface{}); ok {
for _, field := range tokenFields {
if val, ok := resultObj[field]; ok {
if token, ok := val.(string); ok && token != "" {
return token
}
}
}
}
return ""
}
备用方案 - 从LocalStorage获取Token:
func getTokenFromStorage(ctx context.Context) (string, error) {
var token string
err := chromedp.Run(ctx,
chromedp.Evaluate(`
localStorage.getItem('token') ||
localStorage.getItem('api_token') ||
localStorage.getItem('access_token')
`, &token),
)
if err != nil {
return "", err
}
if token == "" || token == "null" {
return "", fmt.Errorf("未找到token")
}
return token, nil
}
为了获得清晰的输出,可以自定义日志系统:
import "io"
// 自定义logger用于业务日志
var logger = log.New(os.Stdout, "", log.LstdFlags)
func main() {
// 禁用chromedp内部的ERROR日志
log.SetOutput(io.Discard)
// 使用自定义logger输出业务日志
logger.Println("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━")
logger.Println(" GitMind 自动登录示例")
logger.Println("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━")
logger.Printf(" 目标网站: %s", gitmindURL)
logger.Printf(" 手机号: %s", maskPhone(phoneNumber))
logger.Println("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━")
// ... 执行登录任务
}
// 辅助函数:隐藏手机号中间4位
func maskPhone(phone string) string {
if len(phone) != 11 {
return phone
}
return phone[:3] + "****" + phone[7:]
}
将以上所有部分整合在一起:
package main
import (
"context"
"encoding/json"
"io"
"log"
"os"
"strings"
"time"
"github.com/chromedp/cdproto/cdp"
"github.com/chromedp/cdproto/network"
"github.com/chromedp/chromedp"
)
const (
gitmindURL = "https://gitmind.cn/"
phoneNumber = "YOUR_PHONE"
password = "YOUR_PASSWORD"
)
var logger = log.New(os.Stdout, "", log.LstdFlags)
func main() {
log.SetOutput(io.Discard) // 禁用chromedp内部日志
// 创建有头模式浏览器
opts := append(chromedp.DefaultExecAllocatorOptions[:],
chromedp.Flag("headless", false),
chromedp.WindowSize(1400, 900),
)
allocCtx, cancel := chromedp.NewExecAllocator(context.Background(), opts...)
defer cancel()
ctx, cancel := chromedp.NewContext(allocCtx)
defer cancel()
ctx, cancel = context.WithTimeout(ctx, 60*time.Second)
defer cancel()
// Token捕获通道
tokenChan := make(chan string, 1)
// 网络监听
chromedp.ListenTarget(ctx, func(ev interface{}) {
switch ev := ev.(type) {
case *network.EventResponseReceived:
resp := ev.Response
if strings.Contains(resp.URL, "/api") ||
strings.Contains(resp.URL, "login") {
go captureToken(ctx, ev.RequestID, tokenChan)
}
}
})
// 执行登录
if err := chromedp.Run(ctx, gitmindLoginTasks()); err != nil {
logger.Printf("登录失败: %v", err)
return
}
// 等待Token
select {
case token := <-tokenChan:
logger.Printf("✓ 登录成功!Token: %s", token)
case <-time.After(5 * time.Second):
logger.Println("✗ 登录超时")
}
time.Sleep(30 * time.Second) // 保持浏览器打开
}
func captureToken(ctx context.Context, reqID network.RequestID, ch chan<- string) {
c := chromedp.FromContext(ctx)
body, err := network.GetResponseBody(reqID).Do(cdp.WithExecutor(ctx, c.Target))
if err != nil {
return
}
var result map[string]interface{}
if err := json.Unmarshal(body, &result); err == nil {
if token := extractToken(result); token != "" {
select {
case ch <- token:
default:
}
}
}
}
import "github.com/chromedp/cdproto/network"
func saveCookies() chromedp.ActionFunc {
return func(ctx context.Context) error {
// 等待登录完成并跳转
if err := chromedp.WaitVisible(`#app`, chromedp.ByID).Do(ctx); err != nil {
return err
}
// 1. 获取所有Cookies
cookies, err := network.GetAllCookies().Do(ctx)
if err != nil {
return err
}
// 2. 序列化Cookies
cookiesData, err := network.GetAllCookiesReturns{Cookies: cookies}.MarshalJSON()
if err != nil {
return err
}
// 3. 保存到文件
if err := os.WriteFile("cookies.tmp", cookiesData, 0755); err != nil {
return err
}
log.Println("Cookies已保存")
return nil
}
}
func loadCookies() chromedp.ActionFunc {
return func(ctx context.Context) error {
// 检查Cookies文件是否存在
if _, err := os.Stat("cookies.tmp"); os.IsNotExist(err) {
return nil
}
// 读取Cookies数据
cookiesData, err := os.ReadFile("cookies.tmp")
if err != nil {
return err
}
// 反序列化
cookiesParams := network.SetCookiesParams{}
if err := cookiesParams.UnmarshalJSON(cookiesData); err != nil {
return err
}
// 设置Cookies
log.Println("正在加载Cookies...")
return network.SetCookies(cookiesParams.Cookies).Do(ctx)
}
}
chromedp提供了三个主要的截图函数,分别针对不同的使用场景。
捕获当前浏览器视口的可见区域。
// 视口截图实现
func CaptureScreenshot(res *[ ]byte) chromedp.Action {
if res == nil {
panic("res cannot be nil")
}
return chromedp.ActionFunc(func(ctx context.Context) error {
var err error
*res, err = page.CaptureScreenshot().
WithFromSurface(true).
Do(ctx)
return err
})
}
// 使用示例
func captureViewport() error {
ctx, cancel := chromedp.NewContext(context.Background())
defer cancel()
var buf [ ]byte
err := chromedp.Run(ctx,
chromedp.Navigate("https://example.com"),
chromedp.CaptureScreenshot(&buf),
)
if err != nil {
return err
}
return os.WriteFile("viewport_screenshot.png", buf, 0644)
}
捕获整个页面的完整截图,包括超出视口的部分。
// 全屏截图实现
func FullScreenshot(res *[ ]byte, quality int) chromedp.Action {
if res == nil {
panic("res cannot be nil")
}
return chromedp.ActionFunc(func(ctx context.Context) error {
// 根据质量参数选择格式
format := page.CaptureScreenshotFormatPng
if quality != 100 {
format = page.CaptureScreenshotFormatJpeg
}
var err error
*res, err = page.CaptureScreenshot().
WithCaptureBeyondViewport(true). // 捕获超出视口的内容
WithFromSurface(true).
WithFormat(format).
WithQuality(int64(quality)).
Do(ctx)
return err
})
}
// 使用示例
func captureFullPage() error {
ctx, cancel := chromedp.NewContext(context.Background())
defer cancel()
var buf [ ]byte
// 高质量PNG截图
err := chromedp.Run(ctx,
chromedp.Navigate("https://example.com"),
FullScreenshot(&buf, 100),
)
if err != nil {
return err
}
return os.WriteFile("fullpage_screenshot.png", buf, 0644)
}
专门用于捕获特定DOM元素的截图。
// 元素截图
func Screenshot(sel interface{}, picbuf *[ ]byte, opts ...chromedp.QueryOption) chromedp.QueryAction {
return ScreenshotScale(sel, 1, picbuf, opts...)
}
// 支持缩放的元素截图
func ScreenshotScale(sel interface{}, scale float64, picbuf *[ ]byte, opts ...chromedp.QueryOption) chromedp.QueryAction {
if picbuf == nil {
panic("picbuf cannot be nil")
}
return chromedp.QueryAfter(sel, func(ctx context.Context, execCtx runtime.ExecutionContextID, nodes ...*cdp.Node) error {
if len(nodes) < 1 {
return fmt.Errorf("selector %q did not return any nodes", sel)
}
// 截取节点截图
return chromedp.Screenshot(nodes[0].NodeID, picbuf).Do(ctx)
}, append(opts, chromedp.NodeVisible)...)
}
// 使用示例
func captureElement() error {
ctx, cancel := chromedp.NewContext(context.Background())
defer cancel()
var buf [ ]byte
// 捕获特定元素
err := chromedp.Run(ctx,
chromedp.Navigate("https://example.com"),
chromedp.Screenshot("#main-content", &buf, chromedp.ByID),
)
if err != nil {
return err
}
// 2倍缩放截图
err = chromedp.Run(ctx,
chromedp.ScreenshotScale(".article", 2.0, &buf, chromedp.ByQuery),
)
return os.WriteFile("element_screenshot.png", buf, 0644)
}
关键参数:
WithFromSurface(true): 从表面捕获截图,确保高质量渲染
WithCaptureBeyondViewport(true): 捕获超出视口的内容
WithClip(&clip): 指定裁剪区域
WithFormat(): 设置图片格式(PNG/JPEG)
WithQuality(): 设置JPEG质量(0-100)
处理高DPI屏幕:
func captureHighDPI() error {
ctx, cancel := chromedp.NewContext(context.Background())
defer cancel()
var buf [ ]byte
err := chromedp.Run(ctx,
chromedp.EmulateViewport(905, 705, chromedp.EmulateScale(1.5)),
chromedp.Navigate("https://example.com"),
chromedp.Screenshot("#target-element", &buf, chromedp.ByID),
)
return err
}
确保页面完全加载:
func captureWithWait() chromedp.Tasks {
return chromedp.Tasks{
chromedp.Navigate("https://example.com"),
chromedp.WaitReady("body"),
chromedp.WaitVisible("#content"),
chromedp.Sleep(2 * time.Second), // 额外等待确保渲染完成
chromedp.Screenshot("#content", &buf),
}
}
批量截图:
func batchScreenshot(ctx context.Context) error {
elements := [ ]string{"#header", "#main", "#footer"}
results := make(map[string][ ]byte)
for _, selector := range elements {
var buf [ ]byte
err := chromedp.Run(ctx,
chromedp.Screenshot(selector, &buf),
)
if err != nil {
log.Printf("截图失败 %s: %v", selector, err)
continue
}
results[selector] = buf
// 保存文件
filename := fmt.Sprintf("%s.png", strings.ReplaceAll(selector, "#", ""))
os.WriteFile(filename, buf, 0644)
}
return nil
}
chromedp通过Chrome DevTools Protocol提供强大的PDF生成能力。
import "github.com/chromedp/cdproto/page"
func generatePDF(ctx context.Context, url string, outputPath string) error {
var buf [ ]byte
err := chromedp.Run(ctx,
chromedp.Navigate(url),
chromedp.ActionFunc(func(ctx context.Context) error {
var err error
buf, _, err = page.PrintToPDF().Do(ctx)
return err
}),
)
if err != nil {
return err
}
return os.WriteFile(outputPath, buf, 0644)
}
func advancedPDF(ctx context.Context, url string) error {
var buf [ ]byte
err := chromedp.Run(ctx,
chromedp.Navigate(url),
chromedp.ActionFunc(func(ctx context.Context) error {
var err error
buf, _, err = page.PrintToPDF().
WithLandscape(true). // 横向模式
WithDisplayHeaderFooter(true). // 显示页眉页脚
WithHeaderTemplate(`
<div style="font-size:8px;width:100%;text-align:center;">
<span class="title"></span> -- <span class="url"></span>
</div>
`).
WithFooterTemplate(`
<div style="font-size:8px;width:100%;text-align:center;">
第 <span class="pageNumber"></span> 页 / 共 <span class="totalPages"></span> 页
</div>
`).
WithPrintBackground(true). // 打印背景
WithMarginTop(0.5). // 上边距(英寸)
WithMarginBottom(0.5).
WithMarginLeft(0.5).
WithMarginRight(0.5).
WithPaperWidth(8.27). // A4宽度
WithPaperHeight(11.69). // A4高度
WithScale(1.0). // 缩放比例
Do(ctx)
return err
}),
)
if err != nil {
return err
}
return os.WriteFile("advanced_output.pdf", buf, 0644)
}
| 配置选项 | 类型 | 默认值 | 描述 |
|---|---|---|---|
WithLandscape |
bool | false | 横向模式 |
WithDisplayHeaderFooter |
bool | false | 显示页眉页脚 |
WithHeaderTemplate |
string | "" | 页眉HTML模板 |
WithFooterTemplate |
string | "" | 页脚HTML模板 |
WithPrintBackground |
bool | false | 打印背景图形 |
WithScale |
float64 | 1.0 | 缩放比例 |
WithPaperWidth |
float64 | 8.5 | 纸张宽度(英寸) |
WithPaperHeight |
float64 | 11.0 | 纸张高度(英寸) |
WithMarginTop |
float64 | 0.4 | 上边距(英寸) |
WithMarginBottom |
float64 | 0.4 | 下边距(英寸) |
WithMarginLeft |
float64 | 0.4 | 左边距(英寸) |
WithMarginRight |
float64 | 0.4 | 右边距(英寸) |
WithPageRanges |
string | "" | 页面范围 |
WithPreferCSSPageSize |
bool | false | 优先CSS页面大小 |
支持的动态变量:
class="title": 页面标题
class="url": 页面URL
class="pageNumber": 当前页码
class="totalPages": 总页数
class="date": 当前日期
<!-- 页眉模板示例 -->
<div style="font-size:10px;width:100%;text-align:center;">
<span class="title"></span> -
<span class="date"></span>
</div>
<!-- 页脚模板示例 -->
<div style="font-size:8px;width:100%;text-align:center;">
第 <span class="pageNumber"></span> 页 / 共 <span class="totalPages"></span> 页
</div>
网页存档:
func archiveWebPage(ctx context.Context, url string, archiveDir string) error {
// 获取页面标题
var title string
err := chromedp.Run(ctx,
chromedp.Navigate(url),
chromedp.Title(&title),
)
if err != nil {
return err
}
// 生成PDF
filename := fmt.Sprintf("%s/%s.pdf", archiveDir, sanitizeFilename(title))
return generatePDF(ctx, url, filename)
}
func sanitizeFilename(name string) string {
// 移除文件名中的非法字符
invalid := [ ]string{"/", "\", ":", "*", "?", "\"", "<", ">", "|"}
for _, char := range invalid {
name = strings.ReplaceAll(name, char, "_")
}
return name
}
批量报表生成:
func generateReports(ctx context.Context, urls [ ]string, outputDir string) error {
for i, url := range urls {
outputPath := fmt.Sprintf("%s/report_%d.pdf", outputDir, i+1)
err := generatePDF(ctx, url, outputPath)
if err != nil {
return fmt.Errorf("生成报表失败 %s: %w", url, err)
}
log.Printf("已生成报表 %d/%d", i+1, len(urls))
}
return nil
}
自定义打印样式:
func printWithCustomStyle(ctx context.Context, url string) error {
err := chromedp.Run(ctx,
chromedp.Navigate(url),
// 注入自定义CSS
chromedp.ActionFunc(func(ctx context.Context) error {
script := `
const style = document.createElement('style');
style.textContent = \`
@media print {
.no-print { display: none !important; }
.print-only { display: block !important; }
body { font-size: 12pt; line-height: 1.6; }
}
\`;
document.head.appendChild(style);
`
return chromedp.Evaluate(script, nil).Do(ctx)
}),
// 生成PDF
chromedp.ActionFunc(func(ctx context.Context) error {
buf, _, err := page.PrintToPDF().
WithPrintBackground(true).
Do(ctx)
if err != nil {
return err
}
return os.WriteFile("styled_document.pdf", buf, 0644)
}),
)
return err
}
func executeJS(ctx context.Context) error {
var result string
err := chromedp.Run(ctx,
chromedp.Navigate("https://example.com"),
chromedp.Evaluate(`document.title`, &result),
)
log.Printf("页面标题: %s", result)
return err
}
func complexJS(ctx context.Context) error {
// 执行复杂的JavaScript脚本
script := `
(function() {
const elements = document.querySelectorAll('a');
const links = Array.from(elements).map(el => ({
text: el.textContent,
href: el.href
}));
return JSON.stringify(links);
})();
`
var result string
err := chromedp.Run(ctx,
chromedp.Navigate("https://example.com"),
chromedp.Evaluate(script, &result),
)
if err != nil {
return err
}
log.Printf("提取的链接: %s", result)
return nil
}
// 不推荐:每次都创建新实例
func badPractice(urls [ ]string) {
for _, url := range urls {
ctx, cancel := chromedp.NewContext(context.Background())
chromedp.Run(ctx, chromedp.Navigate(url))
cancel()
}
}
// 推荐:复用浏览器实例
func goodPractice(urls [ ]string) {
ctx, cancel := chromedp.NewContext(context.Background())
defer cancel()
for _, url := range urls {
chromedp.Run(ctx, chromedp.Navigate(url))
}
}
func parallelScreenshots(urls [ ]string) error {
var wg sync.WaitGroup
errChan := make(chan error, len(urls))
for i, url := range urls {
wg.Add(1)
go func(index int, u string) {
defer wg.Done()
ctx, cancel := chromedp.NewContext(context.Background())
defer cancel()
var buf [ ]byte
err := chromedp.Run(ctx,
chromedp.Navigate(u),
chromedp.FullScreenshot(&buf, 90),
)
if err != nil {
errChan <- err
return
}
filename := fmt.Sprintf("screenshot_%d.jpg", index)
os.WriteFile(filename, buf, 0644)
}(i, url)
}
wg.Wait()
close(errChan)
// 检查是否有错误
for err := range errChan {
if err != nil {
return err
}
}
return nil
}
func concurrentWithLimit(urls [ ]string, maxConcurrent int) error {
sem := make(chan struct{}, maxConcurrent)
var wg sync.WaitGroup
for i, url := range urls {
wg.Add(1)
sem <- struct{}{} // 获取信号量
go func(index int, u string) {
defer wg.Done()
defer func() { <-sem }() // 释放信号量
ctx, cancel := chromedp.NewContext(context.Background())
defer cancel()
var buf [ ]byte
chromedp.Run(ctx,
chromedp.Navigate(u),
chromedp.CaptureScreenshot(&buf),
)
filename := fmt.Sprintf("page_%d.png", index)
os.WriteFile(filename, buf, 0644)
}(i, url)
}
wg.Wait()
return nil
}
func withTimeout(url string) error {
ctx, cancel := chromedp.NewContext(context.Background())
defer cancel()
// 设置30秒超时
ctx, cancel = context.WithTimeout(ctx, 30*time.Second)
defer cancel()
err := chromedp.Run(ctx,
chromedp.Navigate(url),
chromedp.WaitReady("body"),
)
if err != nil {
if err == context.DeadlineExceeded {
return fmt.Errorf("操作超时: %w", err)
}
return err
}
return nil
}
func withRetry(url string, maxRetries int) error {
var lastErr error
for i := 0; i < maxRetries; i++ {
ctx, cancel := chromedp.NewContext(context.Background())
err := chromedp.Run(ctx,
chromedp.Navigate(url),
chromedp.WaitReady("body"),
)
cancel()
if err == nil {
return nil
}
lastErr = err
log.Printf("尝试 %d/%d 失败: %v", i+1, maxRetries, err)
time.Sleep(time.Second * 2)
}
return fmt.Errorf("重试%d次后仍失败: %w", maxRetries, lastErr)
}
func safeExecution(url string) (err error) {
ctx, cancel := chromedp.NewContext(context.Background())
defer cancel()
defer func() {
if r := recover(); r != nil {
err = fmt.Errorf("panic recovered: %v", r)
}
}()
err = chromedp.Run(ctx,
chromedp.Navigate(url),
chromedp.WaitReady("body"),
)
return err
}
// 生产环境推荐配置
func productionConfig() (context.Context, context.CancelFunc) {
opts := append(chromedp.DefaultExecAllocatorOptions[:],
chromedp.Flag("headless", true),
chromedp.Flag("disable-gpu", true),
chromedp.Flag("no-sandbox", true),
chromedp.Flag("disable-dev-shm-usage", true),
chromedp.Flag("disable-extensions", true),
chromedp.Flag("disable-images", false), // 根据需求调整
chromedp.WindowSize(1920, 1080),
)
ctx, cancel := chromedp.NewExecAllocator(context.Background(), opts...)
ctx, cancel = chromedp.NewContext(ctx)
return ctx, cancel
}
func withLogging() {
ctx, cancel := chromedp.NewContext(
context.Background(),
chromedp.WithLogf(log.Printf),
chromedp.WithErrorf(log.Printf),
chromedp.WithDebugf(log.Printf),
)
defer cancel()
// 执行任务...
}
func smartWait(ctx context.Context) error {
return chromedp.Run(ctx,
chromedp.Navigate("https://example.com"),
// 等待DOM就绪
chromedp.WaitReady("body"),
// 等待特定元素可见
chromedp.WaitVisible("#content", chromedp.ByID),
// 等待网络空闲
chromedp.ActionFunc(func(ctx context.Context) error {
time.Sleep(2 * time.Second)
return nil
}),
)
}
func enableDevToolsLog() {
ctx, cancel := chromedp.NewContext(
context.Background(),
chromedp.WithLogf(log.Printf),
)
defer cancel()
chromedp.ListenTarget(ctx, func(ev interface{}) {
log.Printf("DevTools Event: %T %+v\n", ev, ev)
})
}
func saveHTML(ctx context.Context, url string) error {
var html string
err := chromedp.Run(ctx,
chromedp.Navigate(url),
chromedp.OuterHTML("html", &html),
)
if err != nil {
return err
}
return os.WriteFile("page.html", [ ]byte(html), 0644)
}
Chrome DevTools Protocol: chromedevtools.github.io/devtools-protocol
如果您喜欢我的文章,请点击下面按钮随意打赏,您的支持是我最大的动力。
最新评论