📚 原创系列: “Go语言爬虫系列”
🔄 转载说明: 本文最初发布于"Gopher部落"微信公众号,经原作者授权转载。
🔗 关注原创: 欢迎扫描文末二维码,关注"Gopher部落"微信公众号获取第一手Go技术文章。
📑 Go语言爬虫系列导航
🚀 Go爬虫系列:共12篇本文是【Go语言爬虫系列】的第3篇,点击下方链接查看更多文章
- 爬虫入门与Colly框架基础
- HTML解析与Goquery技术详解
- Colly高级特性与并发控制👈 当前位置
- 爬虫架构设计与实现(即将发布)
- 反爬虫策略应对技术(即将发布)
- 模拟登录与会话维持(即将发布)
- 动态网页爬取技术(即将发布)
- 分布式爬虫设计与实现(即将发布)
- 数据存储与处理(即将发布)
- 爬虫性能优化技术(即将发布)
- 爬虫安全与合规性(即将发布)
- 综合项目实战:新闻聚合系统(即将发布)
📖 文章导读
在前两篇文章中,我们学习了爬虫的基本概念、Colly框架的基础用法以及HTML解析技术。本文作为系列的第三篇,将深入探讨Colly框架的高级特性和并发控制技术,重点介绍:
- Colly高级配置与性能调优
- 异步并发爬取与协程控制
- 自定义中间件开发
- 请求限速与防御策略
- 错误处理与重试机制
- 应对反爬虫技术的实用策略
通过本文的学习,您将掌握构建高效、稳定、大规模爬虫系统的核心技术,能够处理更加复杂的爬取任务,同时有效应对各种网站反爬策略。
一、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 识别和分类常见错误
爬虫开发中常见的错误可分为以下几类:
- 网络错误:连接超时、DNS解析失败等
- HTTP错误:4xx、5xx状态码
- 解析错误:HTML格式错误、选择器不匹配
- 资源错误:内存不足、并发过高等
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框架的高级特性和并发控制技术,覆盖了构建高效、稳定爬虫系统所需的关键知识:
- 高级配置: 学习了如何精细调整Collector的参数、自定义HTTP客户端、配置存储后端等
- 并发控制: 掌握了异步爬取、并发限制、请求队列管理等技术
- 中间件开发: 实现了多种实用中间件,如随机User-Agent、Cookie管理、重试策略等
- 限速防御: 学习了如何设置智能限速策略,避免被网站封禁
- 错误处理: 构建了健壮的错误处理机制和恢复策略
- 反爬应对: 了解了常见反爬技术及有效的应对策略
这些技术和策略相互配合,能够帮您构建出适应各种复杂场景的爬虫系统。记住,爬虫开发既是技术挑战,也有伦理边界 - 尊重网站的robots.txt和服务条款,合理控制爬取频率,做一名负责任的爬虫开发者。
下篇预告
在下一篇文章中,我们将探讨爬虫架构设计与实现,包括大规模分布式爬虫架构、数据存储与处理管道、监控告警系统、调度策略等高级话题,帮助您构建企业级爬虫解决方案。敬请期待!
👨💻 关于作者与Gopher部落
"Gopher部落"专注于Go语言技术分享,提供从入门到精通的完整学习路线。
🌟 为什么关注我们?
- 系统化学习路线:本系列12篇文章循序渐进,带你完整掌握Go开发
- 实战驱动教学:理论结合实践,每篇文章都有可操作的代码示例
- 持续更新内容:定期分享最新Go生态技术动态与大厂实践经验
- 专业技术社区:加入我们的技术交流群,与众多Go开发者共同成长
📱 关注方式
- 微信公众号:搜索 “Gopher部落” 或 “GopherTribe”
- CSDN专栏:点击页面右上角"关注"按钮
💡 读者福利
关注公众号回复 “Go爬虫” 即可获取:
- 完整Go爬虫学习资料
- 本系列示例代码
- 项目实战源码
期待与您在Go语言的学习旅程中共同成长!