使用gocolly异步模式/队列解析时结果不一致的原因排查
问题分析与修复方案
你的问题核心是并发场景下的资源竞争未处理,以及错误被忽略导致请求丢失,具体原因和修复如下:
1. 核心问题点
(1) 非线程安全的数据操作
cards切片在多个goroutine中被append,这会导致数据竞争,不仅可能丢失元素,还会破坏切片结构,间接让你误以为visited计数不符合预期(虽然visited用了atomic,但卡片没正确添加会干扰判断)。
(2) 错误被忽略
- 队列模式中,
queue.New和queue.AddURL的错误被直接忽略,可能导致部分初始URL未加入队列; - 异步模式中,
c.Visit的错误未捕获,部分请求可能根本没发出去; - 现有
OnError仅处理收到响应的错误,像DNS失败、连接拒绝这类未收到响应的错误,不会触发OnError,导致你看不到异常。
(3) 队列模式的配置缺失
队列模式下,你没有将Collector与队列绑定,导致通过e.Request.Visit触发的新请求不会进入队列,而是直接并发执行,和队列的调度逻辑冲突,部分请求可能被丢弃。
2. 修复后的代码示例
队列模式修复版
package main import ( "fmt" "strings" "sync" "sync/atomic" "time" "github.com/gocolly/colly/v2" "github.com/gocolly/colly/v2/queue" ) func main() { c := colly.NewCollector() c.SetRequestTimeout(time.Minute * 5) // 捕获队列创建错误 q, err := queue.New(8, &queue.InMemoryQueueStorage{MaxSize: 1000}) if err != nil { fmt.Println("队列创建失败:", err) return } // 将Collector与队列绑定,确保所有请求走队列调度 c.SetQueue(q) var ( visited int64 cards []map[string]string cardMu sync.Mutex // 保护cards的互斥锁 ) c.OnHTML("a.css-rc5s2u", func(e *colly.HTMLElement) { // 捕获触发新请求的错误 if err := e.Request.Visit(e.Attr("href")); err != nil { fmt.Println("触发新请求失败:", err) } }) c.OnError(func(r *colly.Response, err error) { fmt.Println("请求失败 URL:", r.Request.URL, "错误:", err) }) // 增加响应日志,确认请求都收到反馈 c.OnResponse(func(r *colly.Response) { fmt.Println("收到响应 URL:", r.Request.URL) }) c.OnHTML("ul.css-sfcl1s", func(e *colly.HTMLElement) { atomic.AddInt64(&visited, 1) card := make(map[string]string) e.ForEach("p.css-b5m1rv", func(_ int, elem *colly.HTMLElement) { text := strings.Split(elem.Text, ":") if len(text) > 1 { card[text[0]] = text[1] } else { card["type"] = text[0] } }) // 加锁保护cards的append操作 cardMu.Lock() cards = append(cards, card) cardMu.Unlock() }) c.OnRequest(func(r *colly.Request) { fmt.Println("正在访问", r.URL) }) const ( baseURL = "some_url" maxPage = 5 ) for p := 1; p <= maxPage; p++ { urlPath := fmt.Sprintf("%s&page=%d", baseURL, p) // 捕获添加URL到队列的错误 if err := q.AddURL(urlPath); err != nil { fmt.Println("添加URL到队列失败:", urlPath, "错误:", err) } } q.Run(c) fmt.Println("已处理页面数:", visited) fmt.Println("已收集卡片数:", len(cards)) }
异步模式修复版
package main import ( "fmt" "strings" "sync" "sync/atomic" "time" "github.com/gocolly/colly/v2" ) func main() { c := colly.NewCollector(colly.Async(true)) c.SetRequestTimeout(5 * time.Minute) var ( visited int64 cards []map[string]string cardMu sync.Mutex // 保护cards的互斥锁 ) c.OnHTML("a.css-rc5s2u", func(e *colly.HTMLElement) { if err := e.Request.Visit(e.Attr("href")); err != nil { fmt.Println("触发新请求失败:", err) } }) c.OnError(func(r *colly.Response, err error) { fmt.Println("请求失败 URL:", r.Request.URL, "错误:", err) }) c.OnResponse(func(r *colly.Response) { fmt.Println("收到响应 URL:", r.Request.URL) }) c.OnHTML("ul.css-sfcl1s", func(e *colly.HTMLElement) { atomic.AddInt64(&visited, 1) card := make(map[string]string) e.ForEach("li>p.css-b5m1rv", func(_ int, elem *colly.HTMLElement) { text := strings.Split(elem.Text, ":") if len(text) > 1 { card[text[0]] = text[1] } else { card["type"] = text[0] } }) cardMu.Lock() cards = append(cards, card) cardMu.Unlock() }) c.OnRequest(func(r *colly.Request) { fmt.Println("正在访问", r.URL) }) const ( baseURL = "some_url" maxPage = 5 ) for p := 1; p <= maxPage; p++ { urlPath := fmt.Sprintf("%s&page=%d", baseURL, p) // 捕获发起请求的错误 if err := c.Visit(urlPath); err != nil { fmt.Println("发起请求失败:", urlPath, "错误:", err) } } c.Wait() fmt.Println("已处理页面数:", visited) fmt.Println("已收集卡片数:", len(cards)) }
3. 额外优化建议
- 可以给
visited和卡片数加一致性校验,爬取结束后对比两者是否匹配,排查是否有页面未解析到目标元素; - 如果目标网站有反爬,异步/队列模式下并发过高可能被限流,可降低队列并发数(
queue.New第一个参数),或增加请求间隔(c.Limit(&colly.LimitRule{DomainGlob: "*", Delay: 1 * time.Second})); - 若目标页面是动态加载的,gocolly默认无法处理,可结合
colly/headless启用无头浏览器渲染。
内容的提问来源于stack exchange,提问作者Don Draper
相关产品推荐
相关产品推荐

