colly不适合大规模生产场景,因其默认newcollector无并发控制、url全入内存队列、缺去重限速及连接复用调优,易oom或被封ip;裸写net/http更可控。

为什么不用 Colly 直接写裸 net/http?
Colly 确实封装了常见流程,但大规模场景下它容易成为瓶颈:默认的 colly.NewCollector() 不控制并发数,c.Visit() 会把所有 URL 塞进内存队列,没去重、没限速、没连接复用调优。真跑 10 万 URL,轻则 OOM,重则被目标站拉黑 IP。
裸写 net/http 反而更可控——你清楚每个 goroutine 干什么、每条连接怎么复用、每个超时在哪设。Colly 适合快速验证逻辑,不是生产级调度器。
- 别在
OnHTML里开 goroutine:Colly 的回调是串行执行的,加go不提升吞吐,只增加竞态风险 - 禁用
colly.AllowedDomains的宽松模式:它只过滤 host,不校验 path 或 query,容易漏掉非法跳转 - 自定义
http.Transport必须手动注入:Colly 的WithTransport不继承你设的MaxIdleConnsPerHost,得自己 patch
http.Client 超时必须拆成三段设
只设一个 Timeout 是掩耳盗铃。DNS 解析卡住、TLS 握手慢、响应头迟迟不来——这些阶段都可能让 goroutine 永久挂起。生产环境必须分设:
-
DialTimeout:控制 TCP 连接建立时间,建议 ≤ 5s -
TLSHandshakeTimeout:HTTPS 握手超时,建议 ≤ 8s -
ResponseHeaderTimeout:从发完请求到收到响应头的时间,建议 ≤ 10s
别用 http.DefaultClient,也别用 http.Get —— 它们不支持分级超时。正确写法:
client := &http.Client{
Transport: &http.Transport{
DialContext: (&net.Dialer{
Timeout: 5 * time.Second,
KeepAlive: 30 * time.Second,
}).DialContext,
TLSHandshakeTimeout: 8 * time.Second,
ResponseHeaderTimeout: 10 * time.Second,
MaxIdleConns: 100,
MaxIdleConnsPerHost: 50,
IdleConnTimeout: 30 * time.Second,
},
}
并发控制不能只靠 sem := make(chan struct{}, N)
信号量能防爆,但解决不了根本问题:URL 队列无界增长、域名间争抢连接、突发流量打垮目标站。真正的大规模调度要分层:
- 按域名隔离 worker 池:每个 host 单独配
chan string和rate.Limiter,遵守robots.txt的crawl-delay - 用带缓冲 channel 做背压:比如
queue := make(chan string, 1000),配合select非阻塞写入,满时丢弃或降级 - worker 数 ≠ CPU 核数 × 10:IO 密集型任务建议设为
runtime.NumCPU() * 2 ~ 3,再根据目标站响应延迟动态调(比如平均 2s 响应,24 核机器跑 32 个 worker 已顶格)
goquery 解析中文网页前必须转编码
goquery.NewDocumentFromReader(resp.Body) 默认按 UTF-8 解码,遇到 GBK/GB2312 页面直接乱码,导致 .Find() 找不到元素——这不是 selector 写错,是字节流根本对不上。
正确做法是先用 charset.NewReaderLabel 自动识别编码,再转 UTF-8:
bodyReader, err := charset.NewReaderLabel(resp.Body, resp.Header.Get("Content-Type"))
if err != nil {
return nil, err
}
doc, err := goquery.NewDocumentFromReader(bodyReader)
// 后续 .Find() 才可靠
注意:resp.Header.Get("Content-Type") 可能为空,此时 NewReaderLabel 会 fallback 到 HTML meta 标签检测,但不如显式传入准确。
真正麻烦的是那些没声明 charset 也没 meta 的老站——得 fallback 到 golang.org/x/text/encoding/simplifiedchinese.GB18030.NewDecoder().Bytes() 强解,但这会丢数据。大规模采集时,这类页面建议单独标记、跳过或走备用渲染方案。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











