必须手写*http.client:设10秒超时、连接池、ua头、禁自动跳转;html解析用goquery而非正则;并发用信号量限流;url去重+深度控制保命;响应体及时关闭并处理中文编码。

别用 http.Get,必须手写 *http.Client
直接调 http.Get 看似省事,但上线两小时就挂——不是代码错,是默认客户端没超时、没连接池、没 UA,目标站一拦一个准,本地还容易耗尽文件描述符。
- 显式构造
*http.Client,至少设Timeout: 10 * time.Second - 加
Transport控制连接复用:MaxIdleConns: 100和MaxIdleConnsPerHost: 100是安全起点 -
User-Agent必须设,值用常见浏览器字符串,比如"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36",别留空或用 Go 默认标识 - 遇到跳转页被重定向吃掉原始响应(比如 302 后返回登录页),要禁自动跳转:
CheckRedirect: func(req *http.Request, via []*http.Request) error { return http.ErrUseLastResponse }
解析 HTML 别碰正则,goquery 是底线选择
用 regexp 提取 <title></title> 或链接?页面多一个空格、换一行、属性顺序一变,就全失效。这不是写得不够巧,是根本不在同一抽象层上。
- 用
github.com/PuerkitoBio/goquery,它底层调的是golang.org/x/net/html,能处理真实 DOM 结构 -
s.Text()比s.Nodes[0].FirstChild.Data安全得多——前者自动合并文本节点、清理空白,后者一碰到注释或 CDATA 就 panic -
s.Attr("href")返回(string, bool),务必检查第二个值;别用s.Get(0).Attr,属性不存在时直接崩溃 - 相对 URL 要转绝对地址:
url.Join(baseURL, href),否则存下来全是/post/123这种无效链接
并发不是开越多越快,sem := make(chan struct{}, N) 才管用
起 100 个 goroutine 直接 c.Visit(),看着快,实则三秒后卡死:DNS 查询阻塞、TCP 端口占满、目标站返回 429、本地 too many open files 全来了。
使用 uber-go/dig 在 Golang 中实现依赖注入——基于反射容器,提供 Provide/Invoke、dig.In/dig.Out 参数与结果对象,支持命名依赖等
- 用带缓冲的 channel 当信号量:
sem := make(chan struct{}, 4),每次发请求前sem ,结束后 <code> - 别用
time.Sleep模拟人工节奏——它不防并发突增;真要限频,用golang.org/x/time/rate.Limiter - 对同一域名做请求间隔,可用
sync.Map记录上次时间,配合time.Since判断是否可发 - Colly 用户注意:
c.Async = true只是开关,并发数由c.Limit(&colly.LimitRule{Parallelism: 4})控制,不设就是串行
去重和深度控制不是可选项,是保命线
没 URL 去重?爬虫三分钟内就会在两个页面之间反复横跳;不限制深度?从首页开始,a[href] 一递归,几万链接瞬间进队列,内存爆掉只是时间问题。
- 去重 key 必须标准化:
strings.ToLower(u.Scheme + "://" + u.Host + u.EscapedPath()),去掉 fragment,统一大小写 - 用
map[string]struct{}内存去重最快;如果需跨进程或持久化,再考虑 Redis 或 Bloom Filter -
-depth参数建议默认为2,设为0表示只抓当前页;Colly 用户记得配c.MaxDepth = 2 - 发现新链接时,先查重再入队,BFS 模式比 DFS 更可控
最常被忽略的不是怎么写,而是怎么收尾:所有 resp.Body 必须 defer resp.Body.Close(),所有 goquery.Document 不用关,但它的源 io.Reader 如果是文件或自定义流,就得你来关。还有,中文乱码不是编码错了,是没用 charset.NewReaderLabel 套一层响应体。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!










