【Go语言爬虫系列03】Colly高级特性与并发控制

📚 原创系列: “Go语言爬虫系列”

🔄 转载说明: 本文最初发布于"Gopher部落"微信公众号,经原作者授权转载。

🔗 关注原创: 欢迎扫描文末二维码,关注"Gopher部落"微信公众号获取第一手Go技术文章。

📑 Go语言爬虫系列导航

本文是【Go语言爬虫系列】的第3篇,点击下方链接查看更多文章

🚀 Go爬虫系列:共12篇
  1. 爬虫入门与Colly框架基础
  2. HTML解析与Goquery技术详解
  3. Colly高级特性与并发控制👈 当前位置
  4. 爬虫架构设计与实现(即将发布)
  5. 反爬虫策略应对技术(即将发布)
  6. 模拟登录与会话维持(即将发布)
  7. 动态网页爬取技术(即将发布)
  8. 分布式爬虫设计与实现(即将发布)
  9. 数据存储与处理(即将发布)
  10. 爬虫性能优化技术(即将发布)
  11. 爬虫安全与合规性(即将发布)
  12. 综合项目实战:新闻聚合系统(即将发布)

📖 文章导读

在前两篇文章中,我们学习了爬虫的基本概念、Colly框架的基础用法以及HTML解析技术。本文作为系列的第三篇,将深入探讨Colly框架的高级特性和并发控制技术,重点介绍:

  1. Colly高级配置与性能调优
  2. 异步并发爬取与协程控制
  3. 自定义中间件开发
  4. 请求限速与防御策略
  5. 错误处理与重试机制
  6. 应对反爬虫技术的实用策略

通过本文的学习,您将掌握构建高效、稳定、大规模爬虫系统的核心技术,能够处理更加复杂的爬取任务,同时有效应对各种网站反爬策略。

一、Colly高级配置与性能优化

1.1 Collector的高级配置项

在前面的文章中,我们简单介绍了Colly的基本用法。实际上,Collector对象支持多种高级配置选项,可以精细控制爬虫行为:

c := colly.NewCollector(
    // 设置用户代理
    colly.UserAgent("Mozilla/5.0 (Windows NT 10.0; Win64; x64) Chrome/96.0.4664.110"),
    
    // 允许访问的域名,支持通配符
    colly.AllowedDomains("example.com", "*.example.com"),
    
    // 允许重复访问
    colly.AllowURLRevisit(),
    
    // 忽略robots.txt规则(请谨慎使用)
    colly.IgnoreRobotsTxt(),
    
    // 设置最大深度
    colly.MaxDepth(5),
    
    // 异步模式
    colly.Async(true),
    
    // 设置并发数
    colly.Parallelism(5),
    
    // 自定义请求头
    colly.Headers(map[string]string{
        "Accept": "text/html,application/xhtml+xml,application/xml",
        "Accept-Language": "zh-CN,zh;q=0.9,en;q=0.8",
    }),
    
    // URL过滤器
    colly.URLFilters(
        regexp.MustCompile("https://example.com/path/.*"),
    ),
)

1.2 HTTP客户端定制

Colly允许您自定义底层的HTTP客户端,这对于处理特殊网络环境(如代理、特殊证书等)非常有用:

// 创建自定义Transport
transport := &http.Transport{
    // 设置代理
    Proxy: http.ProxyURL(&url.URL{
        Scheme: "http",
        Host:   "proxy.example.com:8080",
    }),
    
    // TLS配置
    TLSClientConfig: &tls.Config{
        InsecureSkipVerify: true, // 忽略证书验证(仅用于测试)
    },
    
    // 连接池设置
    MaxIdleConnsPerHost: 20,
    DisableKeepAlives:   false,
    
    // 超时设置
    DialContext: (&net.Dialer{
        Timeout:   30 * time.Second,
        KeepAlive: 30 * time.Second,
    }).DialContext,
}

// 设置自定义HTTP客户端
c.WithTransport(transport)

1.3 请求上下文和元数据

Colly提供了请求上下文(Context)功能,允许在不同的回调函数之间传递数据:

c.OnRequest(func(r *colly.Request) {
    // 设置上下文数据
    r.Ctx.Put("key", "value")
    r.Ctx.Put("timestamp", time.Now().Unix())
})

c.OnResponse(func(r *colly.Response) {
    // 获取上下文数据
    key := r.Ctx.Get("key")
    timestamp := r.Ctx.GetAny("timestamp").(int64)
    
    fmt.Printf("请求 %s 处理成功,上下文数据: %s, 时间戳: %d\n", 
        r.Request.URL, key, timestamp)
})

这个特性在处理需要关联请求与响应的场景中非常有用,例如:记录请求时间、传递页面类型信息、关联父子页面等。

1.4 存储后端配置

Colly使用存储后端来记录已访问的URL,防止重复爬取。默认使用内存存储,但对于大型爬虫,可以配置使用其他存储后端:

// 内存存储(默认)
c.SetStorage(colly.NewInMemoryStorage())

// SQLite存储
storage := &colly.SQLiteStorage{
    FileName: "visited_urls.db",
}
if err := storage.Init(); err != nil {
    log.Fatal(err)
}
c.SetStorage(storage)

// Redis存储
storage := &colly.RedisStorage{
    Address:  "127.0.0.1:6379",
    Password: "",
    DB:       0,
    Prefix:   "colly:",
}
if err := storage.Init(); err != nil {
    log.Fatal(err)
}
c.SetStorage(storage)

为大规模爬虫选择合适的存储后端能显著提高效率和可靠性。例如,Redis存储允许多个爬虫实例共享URL访问状态,便于分布式爬取。

二、异步并发爬取与协程控制

2.1 开启异步模式

Colly默认是同步模式,这意味着每个请求会阻塞等待完成。在异步模式下,请求会并发执行,大幅提高爬取效率:

// 方式1:创建时启用异步
c := colly.NewCollector(colly.Async(true))

// 方式2:后续启用异步
c.Async = true

// 设置并发数(默认为1)
c.Parallelism = 5

// 启动多个URL爬取
for _, url := range urls {
    c.Visit(url)
}

// 等待所有请求完成
c.Wait()

关键点说明:

  • 异步模式下必须调用Wait()方法等待所有请求完成
  • Parallelism设置了最大并发请求数
  • 异步模式适合大量页面的并发爬取

2.2 并发控制与限制

虽然高并发可以提高爬取速度,但过高的并发可能导致服务器拒绝请求或IP被封。Colly提供了多种并发控制机制:

// 创建限速器
limiter := colly.NewLimiter(
    // 每域名最大并发数
    colly.IgnoreRegexRules(),
    // 域名限速规则,每5秒最多2个请求
    colly.PerDomain(&colly.LimiterRule{
        DomainGlob:  "*.example.com",
        Parallelism: 2,
        Delay:       2 * time.Second,
        RandomDelay: 1 * time.Second,
    }),
    // 随机延迟
    colly.RandomDelay(500*time.Millisecond),
)

// 应用限速器
c.SetLimiter(limiter)

限速参数详解:

  • Parallelism: 每个域名的最大并发数
  • Delay: 请求之间的固定延迟
  • RandomDelay: 在固定延迟基础上增加的随机延迟
  • DomainGlob: 域名匹配模式,支持通配符

2.3 使用队列管理请求

对于大规模爬虫,使用队列可以更好地管理请求,支持优先级、存储和恢复等功能:

// 创建收集器
c := colly.NewCollector()

// 配置回调函数...

// 创建队列
q, _ := queue.New(
    2, // 线程数
    &queue.InMemoryQueueStorage{MaxSize: 10000}, // 队列存储
)

// 添加URL到队列
for _, url := range urls {
    q.AddURL(url)
}

// 启动队列处理
q.Run(c)

高级队列配置示例:

// 创建基于Redis的队列存储
storage := &queue.RedisQueueStorage{
    Address:  "localhost:6379",
    Password: "",
    DB:       0,
    Prefix:   "crawler_queue:",
}

// 初始化存储
if err := storage.Init(); err != nil {
    log.Fatal(err)
}

// 创建队列
q, _ := queue.New(
    10, // 线程数
    storage,
)

// 添加URL到队列,可设置优先级
q.AddURL("https://example.com/important", queue.Priority{Level: 1})
q.AddURL("https://example.com/normal", queue.Priority{Level: 5})

// 添加请求到队列
request, _ := colly.NewRequest("GET", "https://example.com", nil)
request.Headers.Set("X-Custom-Header", "value")
q.AddRequest(request, queue.Priority{Level: 1})

队列的优势在于:

  • 支持请求优先级
  • 可以暂停和恢复爬取
  • 便于实现分布式爬虫
  • 可以持久化存储请求队列,防止程序崩溃导致任务丢失

2.4 克隆Collector扩展功能

在某些场景下,我们需要为不同域名或不同类型的页面使用不同的配置。Colly支持克隆Collector实现这一需求:

// 创建主收集器
mainCollector := colly.NewCollector(
    colly.AllowedDomains("example.com"),
    colly.Async(true),
)

// 克隆一个子收集器,用于处理不同的域名
productCollector := mainCollector.Clone()
productCollector.AllowedDomains = append(
    productCollector.AllowedDomains, 
    "products.example.com",
)

// 克隆另一个子收集器,用于处理图片
imageCollector := mainCollector.Clone()
imageCollector.AllowedDomains = append(
    imageCollector.AllowedDomains, 
    "images.example.com",
)
imageCollector.DetectCharset = false // 图片无需字符集检测

// 在主收集器中发现产品链接时,交给产品收集器处理
mainCollector.OnHTML("a.product", func(e *colly.HTMLElement) {
    productCollector.Visit(e.Attr("href"))
})

// 在产品收集器中发现图片链接时,交给图片收集器处理
productCollector.OnHTML("img", func(e *colly.HTMLElement) {
    imageCollector.Visit(e.Attr("src"))
})

// 启动爬取
mainCollector.Visit("https://example.com")
mainCollector.Wait()
productCollector.Wait()
imageCollector.Wait()

这种方法特别适合:

  • 处理跨域名的复杂爬取逻辑
  • 对不同类型的资源应用不同的处理策略
  • 实现层次化的爬取结构

五、错误处理与重试机制

5.1 识别和分类常见错误

爬虫开发中常见的错误可分为以下几类:

  1. 网络错误:连接超时、DNS解析失败等
  2. HTTP错误:4xx、5xx状态码
  3. 解析错误:HTML格式错误、选择器不匹配
  4. 资源错误:内存不足、并发过高等

Colly提供了OnError回调来处理这些错误:

c.OnError(func(r *colly.Response, err error) {
    url := r.Request.URL.String()
    statusCode := r.StatusCode
    
    // 网络错误判断
    if statusCode == 0 {
        if strings.Contains(err.Error(), "timeout") {
            log.Printf("连接超时: %s - %v\n", url, err)
        } else if strings.Contains(err.Error(), "no such host") {
            log.Printf("DNS解析失败: %s - %v\n", url, err)
        } else {
            log.Printf("网络错误: %s - %v\n", url, err)
        }
        return
    }
    
    // HTTP错误判断
    switch {
    case statusCode >= 400 && statusCode < 500:
        log.Printf("客户端错误: %s - %d\n", url, statusCode)
    case statusCode >= 500:
        log.Printf("服务器错误: %s - %d\n", url, statusCode)
    default:
        log.Printf("未知错误: %s - %d - %v\n", url, statusCode, err)
    }
})

5.2 高级重试策略

简单的重试可能不足以应对复杂场景,下面是一个更完善的重试策略实现:

// 高级重试中间件
type RetryPolicy struct {
    // 最大重试次数
    MaxRetries int
    // 退避系数(重试间隔倍数)
    BackoffFactor float64
    // 初始重试延迟
    InitialDelay time.Duration
    // 最大重试延迟
    MaxDelay time.Duration
    // 重试的错误类型
    RetryableErrors []string
    // 重试的HTTP状态码
    RetryableStatusCodes []int
    
    // 内部状态
    retryCount map[string]int
    mu         sync.Mutex
}

// 创建默认的重试策略
func NewRetryPolicy() *RetryPolicy {
    return &RetryPolicy{
        MaxRetries:          3,
        BackoffFactor:       2.0,
        InitialDelay:        1 * time.Second,
        MaxDelay:            60 * time.Second,
        RetryableErrors:     []string{"timeout", "connection refused", "no such host", "EOF"},
        RetryableStatusCodes: []int{408, 429, 500, 502, 503, 504},
        retryCount:          make(map[string]int),
    }
}

// 应用重试策略到收集器
func (rp *RetryPolicy) Apply(c *colly.Collector) {
    c.OnError(func(r *colly.Response, err error) {
        rp.mu.Lock()
        defer rp.mu.Unlock()
        
        url := r.Request.URL.String()
        statusCode := r.StatusCode
        
        // 检查是否应该重试
        shouldRetry := false
        
        // 检查HTTP状态码
        for _, code := range rp.RetryableStatusCodes {
            if statusCode == code {
                shouldRetry = true
                break
            }
        }
        
        // 检查错误信息
        if !shouldRetry && err != nil {
            errMsg := err.Error()
            for _, retryableErr := range rp.RetryableErrors {
                if strings.Contains(errMsg, retryableErr) {
                    shouldRetry = true
                    break
                }
            }
        }
        
        // 如果不应该重试,直接返回
        if !shouldRetry {
            log.Printf("不可重试的错误: %s - %d - %v\n", url, statusCode, err)
            return
        }
        
        // 检查重试次数
        count := rp.retryCount[url]
        if count >= rp.MaxRetries {
            log.Printf("达到最大重试次数: %s (%d次)\n", url, count)
            return
        }
        
        // 计算退避延迟
        delay := rp.InitialDelay * time.Duration(math.Pow(rp.BackoffFactor, float64(count)))
        if delay > rp.MaxDelay {
            delay = rp.MaxDelay
        }
        
        // 更新重试计数
        rp.retryCount[url] = count + 1
        
        log.Printf("重试 %s (%d/%d) 延迟 %v\n", 
            url, count+1, rp.MaxRetries, delay)
        
        // 延迟后重试
        time.AfterFunc(delay, func() {
            // 克隆原始请求
            req := r.Request.Clone()
            // 清除一些可能导致重试失败的头信息
            req.Headers.Del("If-None-Match")
            req.Headers.Del("If-Modified-Since")
            c.Request("GET", url, nil, r.Request.Ctx, req.Headers)
        })
    })
}

使用示例:

func main() {
    c := colly.NewCollector(colly.Async(true))
    
    // 应用重试策略
    retryPolicy := NewRetryPolicy()
    retryPolicy.Apply(c)
    
    // ... 其他爬虫逻辑 ...
}

5.3 健壮的错误恢复机制

针对严重错误和不可恢复的情况,我们可以实现崩溃恢复和状态保存:

// 崩溃恢复中间件
func RecoveryMiddleware(c *colly.Collector) {
    // 为每个goroutine添加恢复逻辑
    c.OnRequest(func(r *colly.Request) {
        go func(req *colly.Request) {
            // 延迟处理panic
            defer func() {
                if r := recover(); r != nil {
                    log.Printf("从panic恢复: %v\n请求: %s\n堆栈: %s\n", 
                        r, req.URL, debug.Stack())
                }
            }()
            
            // 这里可以添加其他操作
        }(r)
    })
}

// 状态保存和恢复功能
type CrawlerState struct {
    PendingURLs []string          `json:"pending_urls"`
    VisitedURLs map[string]bool   `json:"visited_urls"`
    Data        map[string]interface{} `json:"data"`
}

func SaveCrawlerState(state *CrawlerState, filename string) error {
    data, err := json.MarshalIndent(state, "", "  ")
    if err != nil {
        return err
    }
    return os.WriteFile(filename, data, 0644)
}

func LoadCrawlerState(filename string) (*CrawlerState, error) {
    data, err := os.ReadFile(filename)
    if err != nil {
        return nil, err
    }
    
    var state CrawlerState
    err = json.Unmarshal(data, &state)
    if err != nil {
        return nil, err
    }
    
    return &state, nil
}

5.4 监控与日志记录

良好的日志记录对于诊断问题和优化爬虫至关重要:

// 详细日志记录器
func DetailedLogger(c *colly.Collector, logFile string) {
    // 创建或打开日志文件
    file, err := os.OpenFile(logFile, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666)
    if err != nil {
        log.Fatal("无法创建日志文件:", err)
    }
    
    // 创建多输出的日志器
    logger := log.New(io.MultiWriter(os.Stdout, file), "", log.LstdFlags)
    
    // 记录请求
    c.OnRequest(func(r *colly.Request) {
        logger.Printf("[请求] %s %s\n", r.Method, r.URL)
    })
    
    // 记录响应
    c.OnResponse(func(r *colly.Response) {
        logger.Printf("[响应] %s %d %d字节 %.2fs\n", 
            r.Request.URL, r.StatusCode, len(r.Body), 
            time.Since(r.Request.Ctx.GetAny("startTime").(time.Time)).Seconds())
    })
    
    // 记录错误
    c.OnError(func(r *colly.Response, err error) {
        logger.Printf("[错误] %s %v\n", r.Request.URL, err)
    })
    
    // 记录完成的页面
    c.OnScraped(func(r *colly.Response) {
        logger.Printf("[完成] %s\n", r.Request.URL)
    })
    
    // 记录请求开始时间
    c.OnRequest(func(r *colly.Request) {
        r.Ctx.Put("startTime", time.Now())
    })
}

六、应对反爬虫技术

6.1 常见反爬虫技术及应对策略

现代网站采用各种技术来防止爬虫,以下是一些常见技术及应对方法:

反爬虫技术原理应对策略
User-Agent检测识别爬虫使用的默认User-Agent使用随机真实的浏览器User-Agent
IP频率限制限制单IP的请求频率使用代理池、控制请求速率
Cookie/Session验证要求有效的会话状态保存并使用Cookie,模拟登录流程
验证码通过人机验证阻止自动访问接入验证码识别服务或人工处理
JavaScript渲染内容通过JS动态加载使用无头浏览器(如Chromedp)处理
蜜罐陷阱设置隐藏链接诱导爬虫点击分析网页结构,避免访问蜜罐链接
网页指纹识别分析浏览器行为特征模拟真实用户行为和浏览模式

6.2 模拟浏览器指纹

某些网站使用高级指纹识别来检测爬虫,需要模拟更完整的浏览器特征:

// 添加真实的浏览器指纹
func BrowserFingerprint(c *colly.Collector) {
    c.OnRequest(func(r *colly.Request) {
        // 设置常见的请求头
        r.Headers.Set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/96.0.4664.110 Safari/537.36")
        r.Headers.Set("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8")
        r.Headers.Set("Accept-Language", "zh-CN,zh;q=0.9,en-US;q=0.8,en;q=0.7")
        r.Headers.Set("Accept-Encoding", "gzip, deflate, br")
        r.Headers.Set("Connection", "keep-alive")
        r.Headers.Set("Upgrade-Insecure-Requests", "1")
        r.Headers.Set("Cache-Control", "max-age=0")
        
        // 设置一些常见的客户端提示头
        r.Headers.Set("Sec-Ch-Ua", "\"Not A;Brand\";v=\"99\", \"Chromium\";v=\"96\", \"Google Chrome\";v=\"96\"")
        r.Headers.Set("Sec-Ch-Ua-Mobile", "?0")
        r.Headers.Set("Sec-Ch-Ua-Platform", "\"Windows\"")
        r.Headers.Set("Sec-Fetch-Dest", "document")
        r.Headers.Set("Sec-Fetch-Mode", "navigate")
        r.Headers.Set("Sec-Fetch-Site", "none")
        r.Headers.Set("Sec-Fetch-User", "?1")
    })
}

6.3 集成无头浏览器处理JavaScript

对于需要JavaScript渲染的网站,Colly可以与Chromedp集成:

package main

import (
    "context"
    "log"
    "time"

    "github.com/chromedp/chromedp"
    "github.com/gocolly/colly/v2"
)

// 使用Chromedp获取渲染后的HTML
func GetRenderedHTML(url string) (string, error) {
    // 创建上下文
    ctx, cancel := chromedp.NewContext(
        context.Background(),
        // 可选:设置调试端口
        // chromedp.WithDebugf(log.Printf),
    )
    defer cancel()

    // 设置超时
    ctx, cancel = context.WithTimeout(ctx, 30*time.Second)
    defer cancel()

    // 保存HTML的变量
    var html string

    // 运行任务
    err := chromedp.Run(ctx,
        chromedp.Navigate(url),
        // 等待页面加载完成
        chromedp.WaitVisible("body", chromedp.ByQuery),
        // 可选:等待特定元素出现
        // chromedp.WaitVisible("#content", chromedp.ByID),
        // 等待一点时间让JS执行完成
        chromedp.Sleep(2*time.Second),
        // 获取HTML
        chromedp.OuterHTML("html", &html),
    )

    if err != nil {
        return "", err
    }

    return html, nil
}

func main() {
    // 创建收集器
    c := colly.NewCollector()
    
    // 设置常规回调
    c.OnHTML("a[href]", func(e *colly.HTMLElement) {
        link := e.Attr("href")
        log.Println("Found link:", link)
    })
    
    // 处理JS渲染的页面
    url := "https://example.com/js-rendered-page"
    
    // 获取渲染后的HTML
    html, err := GetRenderedHTML(url)
    if err != nil {
        log.Fatal(err)
    }
    
    // 使用渲染后的HTML
    err = c.Request("GET", url, strings.NewReader(html), nil, nil)
    if err != nil {
        log.Fatal(err)
    }
}

6.4 绕过Web应用防火墙(WAF)

一些网站使用防火墙来阻止爬虫,以下是一些常见绕过技术:

// 添加关键头部来绕过一些WAF
func BypassWAF(c *colly.Collector) {
    c.OnRequest(func(r *colly.Request) {
        // 添加Referer,模拟从合法网站点击进入
        r.Headers.Set("Referer", "https://www.google.com/")
        
        // 添加X-Forwarded-For头,可能绕过某些基于IP的限制
        // 注意:这种方法在现代WAF面前可能不太有效
        r.Headers.Set("X-Forwarded-For", "66.249.66.1") // Google爬虫的IP
        
        // 设置DNT (Do Not Track)
        r.Headers.Set("DNT", "1")
        
        // 模拟真实浏览器的其他特征
        r.Headers.Set("Accept-Charset", "UTF-8")
    })
    
    // 处理CF-Challenge等防护
    c.OnResponse(func(r *colly.Response) {
        // 检测是否遇到Cloudflare等WAF的挑战页面
        if r.StatusCode == 403 || r.StatusCode == 503 {
            if bytes.Contains(r.Body, []byte("Cloudflare")) || 
               bytes.Contains(r.Body, []byte("challenge")) {
                log.Println("检测到WAF挑战页面,尝试使用无头浏览器解决...")
                
                // 这里可以调用之前的GetRenderedHTML函数
                // 或者其他专门解决CF挑战的库
            }
        }
    })
}

📝 实战案例:构建健壮的新闻爬虫

让我们将学到的所有技术整合到一个实际案例中——一个健壮、高效的新闻爬虫:

package main

import (
    "encoding/json"
    "fmt"
    "log"
    "math/rand"
    "os"
    "os/signal"
    "strings"
    "sync"
    "syscall"
    "time"

    "github.com/gocolly/colly/v2"
    "github.com/gocolly/colly/v2/queue"
)

// 新闻文章结构
type Article struct {
    Title     string    `json:"title"`
    URL       string    `json:"url"`
    Author    string    `json:"author,omitempty"`
    Date      time.Time `json:"date,omitempty"`
    Content   string    `json:"content"`
    Category  string    `json:"category,omitempty"`
    Source    string    `json:"source"`
}

// 爬虫状态
type CrawlerState struct {
    Articles   []Article         `json:"articles"`
    VisitedURLs map[string]bool  `json:"visited_urls"`
    PendingURLs []string         `json:"pending_urls"`
}

func main() {
    // 初始化随机数生成器
    rand.Seed(time.Now().UnixNano())
    
    // 创建爬虫状态
    state := &CrawlerState{
        Articles:    make([]Article, 0),
        VisitedURLs: make(map[string]bool),
        PendingURLs: make([]string, 0),
    }
    
    // 加载之前的状态(如果存在)
    stateFile := "crawler_state.json"
    if _, err := os.Stat(stateFile); err == nil {
        log.Println("加载之前的爬虫状态...")
        data, err := os.ReadFile(stateFile)
        if err == nil {
            json.Unmarshal(data, state)
        }
    }
    
    // 创建收集器,配置高级选项
    c := colly.NewCollector(
        colly.AllowedDomains("news.example.com"),
        colly.MaxDepth(3),
        colly.Async(true),
    )
    c.Parallelism = 3
    
    // 设置限速
    err := c.Limit(&colly.LimiterRule{
        DomainGlob:  "*",
        Delay:       1 * time.Second,
        RandomDelay: 1 * time.Second,
        Parallelism: 3,
    })
    if err != nil {
        log.Fatal("设置限速器失败:", err)
    }
    
    // 应用中间件
    RandomUserAgent(c)
    retryPolicy := NewRetryPolicy()
    retryPolicy.Apply(c)
    DetailedLogger(c, "crawler.log")
    
    // 数据锁,防止并发修改状态
    var mu sync.Mutex
    
    // 处理文章列表页
    c.OnHTML("article.news-item", func(e *colly.HTMLElement) {
        // 提取文章链接
        link := e.ChildAttr("a.read-more", "href")
        if link == "" {
            return
        }
        
        // 确保是绝对URL
        link = e.Request.AbsoluteURL(link)
        
        // 检查是否已访问
        mu.Lock()
        if state.VisitedURLs[link] {
            mu.Unlock()
            return
        }
        mu.Unlock()
        
        // 访问文章详情页
        detailCollector := c.Clone()
        detailCollector.Visit(link)
    })
    
    // 处理文章详情页
    c.OnHTML("div.article-detail", func(e *colly.HTMLElement) {
        url := e.Request.URL.String()
        
        // 提取文章信息
        article := Article{
            Title:    strings.TrimSpace(e.ChildText("h1.title")),
            URL:      url,
            Content:  strings.TrimSpace(e.ChildText("div.content")),
            Author:   strings.TrimSpace(e.ChildText("span.author")),
            Category: strings.TrimSpace(e.ChildText("span.category")),
            Source:   "news.example.com",
        }
        
        // 解析日期
        dateStr := strings.TrimSpace(e.ChildText("time.published"))
        if dateStr != "" {
            if t, err := time.Parse("2006-01-02 15:04", dateStr); err == nil {
                article.Date = t
            }
        }
        
        // 保存文章
        mu.Lock()
        state.Articles = append(state.Articles, article)
        state.VisitedURLs[url] = true
        mu.Unlock()
        
        log.Printf("保存文章: %s\n", article.Title)
    })
    
    // 发现分页链接
    c.OnHTML("a.pagination[href]", func(e *colly.HTMLElement) {
        link := e.Attr("href")
        if link != "" {
            e.Request.Visit(link)
        }
    })
    
    // 定期保存状态
    ticker := time.NewTicker(1 * time.Minute)
    go func() {
        for range ticker.C {
            saveState(state, stateFile)
        }
    }()
    
    // 捕获终止信号,确保优雅退出
    sigChan := make(chan os.Signal, 1)
    signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM)
    go func() {
        <-sigChan
        log.Println("接收到终止信号,正在保存状态...")
        saveState(state, stateFile)
        os.Exit(0)
    }()
    
    // 开始爬取
    startUrls := []string{
        "https://news.example.com/latest",
        "https://news.example.com/politics",
        "https://news.example.com/technology",
    }
    
    // 如果有未完成的URL,优先处理它们
    if len(state.PendingURLs) > 0 {
        startUrls = state.PendingURLs
    }
    
    for _, url := range startUrls {
        c.Visit(url)
    }
    
    // 等待所有爬取完成
    c.Wait()
    
    // 保存最终状态
    saveState(state, stateFile)
    
    // 保存文章到JSON文件
    articlesJson, _ := json.MarshalIndent(state.Articles, "", "  ")
    os.WriteFile("articles.json", articlesJson, 0644)
    
    log.Printf("爬取完成,共保存 %d 篇文章\n", len(state.Articles))
}

// 保存爬虫状态
func saveState(state *CrawlerState, filename string) {
    // 添加剩余的待爬取URL
    // 这里简化处理,实际中应该从队列中提取
    
    data, err := json.MarshalIndent(state, "", "  ")
    if err != nil {
        log.Println("保存状态失败:", err)
        return
    }
    
    err = os.WriteFile(filename, data, 0644)
    if err != nil {
        log.Println("写入状态文件失败:", err)
        return
    }
    
    log.Println("爬虫状态已保存")
}

💡 小结

在本文中,我们深入探讨了Colly框架的高级特性和并发控制技术,覆盖了构建高效、稳定爬虫系统所需的关键知识:

  1. 高级配置: 学习了如何精细调整Collector的参数、自定义HTTP客户端、配置存储后端等
  2. 并发控制: 掌握了异步爬取、并发限制、请求队列管理等技术
  3. 中间件开发: 实现了多种实用中间件,如随机User-Agent、Cookie管理、重试策略等
  4. 限速防御: 学习了如何设置智能限速策略,避免被网站封禁
  5. 错误处理: 构建了健壮的错误处理机制和恢复策略
  6. 反爬应对: 了解了常见反爬技术及有效的应对策略

这些技术和策略相互配合,能够帮您构建出适应各种复杂场景的爬虫系统。记住,爬虫开发既是技术挑战,也有伦理边界 - 尊重网站的robots.txt和服务条款,合理控制爬取频率,做一名负责任的爬虫开发者。

下篇预告

在下一篇文章中,我们将探讨爬虫架构设计与实现,包括大规模分布式爬虫架构、数据存储与处理管道、监控告警系统、调度策略等高级话题,帮助您构建企业级爬虫解决方案。敬请期待!

👨‍💻 关于作者与Gopher部落

"Gopher部落"专注于Go语言技术分享,提供从入门到精通的完整学习路线。

🌟 为什么关注我们?

  1. 系统化学习路线:本系列12篇文章循序渐进,带你完整掌握Go开发
  2. 实战驱动教学:理论结合实践,每篇文章都有可操作的代码示例
  3. 持续更新内容:定期分享最新Go生态技术动态与大厂实践经验
  4. 专业技术社区:加入我们的技术交流群,与众多Go开发者共同成长

📱 关注方式

  1. 微信公众号:搜索 “Gopher部落”“GopherTribe”
  2. CSDN专栏:点击页面右上角"关注"按钮

💡 读者福利

关注公众号回复 “Go爬虫” 即可获取:

  • 完整Go爬虫学习资料
  • 本系列示例代码
  • 项目实战源码

期待与您在Go语言的学习旅程中共同成长!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Gopher部落

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值