Chromedp原理及使用

Chromedp基础知识

什么是Chromedp

chromedp是一个更快更简单的Golang库,用于调用支持Chrome DevTools Protocol的浏览器,同时不需要额外的依赖(例如SeleniumPhantomJS)。

Chrome和Golang都与Google有着相当密切的关系,而Chrome DevTools其实就是Chrome浏览器按下F12之后的控制终端。

核心特点:

  • 纯Go实现,无需外部驱动

  • 直接使用Chrome DevTools Protocol

  • 性能优异,资源占用少

  • 支持无头模式和有头模式

  • 功能强大,API简洁

为什么选择Chromedp而不是Selenium

对于Golang开发来说,使用chromedp更为便捷,主要优势包括:

  1. 零依赖部署:仅需要Chrome浏览器,不需要依赖ChromeDriver

  2. 简化部署流程:省去了依赖问题,有助于自动化构建和多平台架构迁移

  3. 原生Go支持:更好的类型安全和性能表现

  4. 更轻量级:减少了中间层的开销

应用场景

Chromedp适用于以下场景:

  • 自动化测试

  • 网页爬虫开发

  • 网页截图与PDF生成

  • 模拟用户行为

  • 性能监控与分析

  • 网站自动登录与Token捕获

  • 网络请求拦截与分析


环境配置与安装

安装步骤

前置要求:

  1. 下载并安装Chrome浏览器

  2. 安装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"),
    }
}

元素定位与点击

获取元素选择器的方法:

  1. 右键目标元素,选择"检查"

  2. 在开发者工具中右键元素

  3. 选择 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):

  1. 打开GitMind首页

  2. 处理Cookie同意弹窗

  3. 点击登录按钮

  4. 切换到密码登录方式

  5. 自动输入账号密码

  6. 勾选用户协议

  7. 提交登录

  8. 捕获登录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`, &currentURL).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
}

网络监听与Token捕获

使用网络监听捕获登录后返回的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:
            }
        }
    }
}

Cookies管理实现免登录

保存Cookies

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
    }
}

加载Cookies

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提供了三个主要的截图函数,分别针对不同的使用场景。

CaptureScreenshot - 视口截图

捕获当前浏览器视口的可见区域。

// 视口截图实现

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)
}

FullScreenshot - 全屏截图

捕获整个页面的完整截图,包括超出视口的部分。

// 全屏截图实现

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)
}

Screenshot - 元素截图

专门用于捕获特定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
}

PDF导出功能

chromedp通过Chrome DevTools Protocol提供强大的PDF生成能力。

基础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)
}

高级PDF配置

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)
}

PDF配置选项详解

配置选项 类型 默认值 描述
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
}

JavaScript执行

执行JavaScript代码

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
}

复杂的JavaScript操作

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
        }),
    )
}

调试技巧

启用DevTools日志

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)
    })
}

保存HTML内容

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)

}

打 赏