1.2 选型对比:自研、colly 与托管方案的账
场景:三条路摆在面前
需求拆完,摆在我们面前的有三条路。第一条是买托管采集服务,按量付费,开箱即用;第二条是基于 colly 这类开源爬虫框架做二次开发;第三条是用 Go 标准库自研。团队就四个人,没人想造轮子,但也不想把核心数据链路绑死在别人平台上。
我们最终选了第三条,但不是盲目的自研崇拜,而是把三笔账摆在一起算过之后的结论。这一节把我们当时的对比原样拿出来,给同样在纠结的人一个参照。
方案:三笔账一起算
三条路的选项其实不止三个字。托管服务里还分平台型和服务型,colly 之外还有若干衍生框架,自研也可以分裸 net/http 起步和基于消息队列搭骨架。我们没有穷举,而是先把每条路的代表选出来算一遍,因为选项之间的差异远小于三条路之间的差异。过分纠结于同一赛道内的细微差别,是选型阶段最常见的时间陷阱。
第一笔是功能账。托管服务号称全托管,但我们的采集对象里有几个站需要登录态、有翻页接口签名,这些定制需求托管平台要么不支持,要么得走他们的定制开发,周期不可控。colly 是优秀的单机框架,回调式 API 写起来快,但它的分布式能力要自己补:任务分发、去重指纹库、断点续爬,这些恰恰是我们最重的部分,用 colly 并没省掉多少工作。
功能账里还有一个隐藏项容易被忽略:可观测性。托管平台给的通常是一个任务成功率的仪表盘,失败了只能看到一句任务失败。我们想要的是每个 URL 级别的状态、每次失败的 HTTP 状态码、每台机器的实时吞吐——这些细节决定了排障速度。自研最大的福利其实是这种全链路的透明度,凌晨出告警时你能精准定位到哪台机器的哪个任务卡在哪一步。
补一个具体对比:同样一次任务延迟告警,托管方案下我们只能看到平台给的失败计数,并且只能提工单问客服要详细日志;自研方案下打开 Grafana 一看,是某台 worker 到某个站的 DNS 解析变慢,两分钟定位完。这种排障速度的差异,到了 5.1 节会变成一套完整的可观测性设计。
第二笔是成本账。托管服务按采集页数计费,按我们日均三万页的量估算,每月账单是固定支出且随站点数线性增长;自研的固定成本是几台轻量应用服务器,跑满我们当时的量绰绰有余。这里的关键差异在于边际成本曲线:托管是线性上涨的,自研是阶梯式上涨的——加机器之前,现有机器的余量是免费的。日均三万页只占我们单机处理能力的三成,意味着站点数翻倍之前,自研的边际成本接近于零。粗算对比如下:
| 对比维度 | 托管采集服务 | colly 二次开发 | Go 自研 |
|---|---|---|---|
| 首月投入 | 低,按页计费约两千元 | 零软件费,机器约三百元 | 机器约四百元,人力最高 |
| 量翻三倍后 | 线性上涨至六千元以上 | 机器需扩容,约九百元 | 机器扩容,约一千元 |
| 定制开发 | 排期不可控,走工单 | 完全自主 | 完全自主 |
| 运维负担 | 平台兜底,省心 | 单机尚可,分布式自己补 | 全自己来,但链路透明 |
| 数据归属 | 页面数据过一道第三方 | 全部落在自己云账号 | 全部落在自己云账号 |
第三笔是风险账。采集对象是行业公开页面,有些内容甲方明确要求不能过第三方通道,这条红线直接把托管服务排掉了。剩下 colly 与自研之间,我们评估了团队情况:团队 Go 基础扎实,net/http 加 goquery 的组合我们已经在别的小项目里写过,沉没成本很低;而 colly 的回调模型在长任务里不好做状态机,断点续爬会写得很别扭。最后决定:自研骨架,单机阶段吸收 colly 的设计思路,不引它的包。
风险账里还有一条不太有人提:人员风险。用 colly 意味着团队必须有人吃透它的源码,否则出怪问题只能去 GitHub 翻 issue。四个人里没有这样的人,而自研代码每个人都参与写,随便拉一个人都能排障。技术选型有时候选的不是技术,是团队未来的维护结构。
还有一个隐性因素:自研意味着每一步都能解释。凌晨三点爬起来排故障时,看的是自己写的日志和状态表,而不是给服务商提工单等回复。这个体感在 5.1 节监控告警里会反复出现。
三笔账算完,我们的结论可以压缩成一张量级对照表,后来也有别的团队照着这张表做决策:
| 采集量级 | 我们的建议 | 主要理由 |
|---|---|---|
| 日均一千页以下 | 托管服务或简单脚本 | 调度复杂度不值得自研摊销 |
| 日均一千到十万页 | 框架起步或自研骨架 | 量级适中,自研成本可控 |
| 日均十万页以上 | 自研平台加云存储 | 托管按页计费线性上涨太贵 |
我们在第二档,不上不下最尴尬:托管的固定费率显得贵,全自研又得掂量人力。最后的折中是自研抓取与调度骨架,存储和分发直接用云产品——不重复造基础设施的轮子,这在 1.4 节有完整地图。后来证明这个折中是对的:上线半年量级翻了两番,骨架一行没改,只往里加机器和加 worker 进程。
配置与代码:自研骨架的最小依赖
自研不等于什么都自己写。我们的依赖清单很短,每个库都有明确的替代理由:
| 能力 | 选用的库 | 为什么是它 |
|---|---|---|
| HTTP 客户端 | net/http | 标准库即可,连接池可控 |
| HTML 解析 | goquery | CSS 选择器语义,团队最熟 |
| 持久化队列 | 自建表加 Redis | 状态必须落库,Redis 做内存层 |
| 日志 | zap | 结构化日志,后面接 CLS 方便 |
| 配置 | viper | 多机配置热更新 |
极简的单机抓取核心,大概就这么几行,后面整本书都在往这个骨架上加肉:
func fetch(u string) (*goquery.Document, error) {
req, _ := http.NewRequest("GET", u, nil)
req.Header.Set("User-Agent", ua)
resp, err := client.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode != 200 {
return nil, fmt.Errorf("status %d", resp.StatusCode)
}
return goquery.NewDocumentFromReader(resp.Body)
}
这段代码在 2.1 节还会展开,这里只想说明:自研的起步门槛比想象中低,最难的部分从来不是抓取本身,而是调度、去重和容错——那才是全书的主线。反过来给犹豫自研的人提个醒:如果你看到公司里一个采集系统复杂得吓人,复杂度大概率不在抓取,而在它背后的状态管理和容错机制上。选型时被吓退或被吸引,往往都是算错了复杂度的位置。
踩坑:选型阶段最容易低估的三件事
第一,低估状态管理的复杂度。单机能跑的代码到多机就出问题:两个 worker 抢同一批任务、去重指纹在内存里对不上。如果重来,我们会在第一周就把任务状态表设计好,而不是先跑通再补。状态表的设计在 3.3 节断点续爬里。
第二,高估框架的省力程度。我们最初也在 colly 上花了一周,发现它的回调模型里塞状态机很别扭后回滚。一周时间不算白花,至少证明了对我们这个场景,框架抽象是负收益。选型结论必须拿自己场景的数据说话,别信别人的最佳实践。
第三,忽略退出成本。托管服务的诱人之处是前两周极快,但数据、调度、去重全在平台上沉淀,量越大越离不开。我们认识的一个团队切出自建时,光历史任务配置就迁移了两个月。
第四个坑其实是自己的心态:差点为了用上新框架而选型。团队里有人想把整套调度重写在某个新出现的分布式任务库上,理由是简历好看。被否掉的理由很现实——那个库当时星标两千出头,文档稀薄,出问题没人能帮我们。选型要坚持一个朴素的原则:选被验证过的东西,除非没得选。后来 3.1 节的 master/worker 通信就是基于最普通的 HTTP 加 JSON,稳到写完这篇书都没换过。
顺带一个经验:选型讨论要设定截止时间。我们给这个决定卡了一周,一周内算不完三笔账就按现有信息拍板,拖下去的机会成本比选错的成本更高。小团队的选型会议最怕无限发散,每个人都在追加备选方案。
最后补一句与具体工具无关的话:选型结论要写下来,包括被否掉的方案和否掉的理由。半年后量级变了要重新评估时,这份记录是最有价值的输入——否则大家会把同一批方案重新吵一遍。
小结
选型的本质是回答三句话:功能账决定能不能做,成本账决定值不值,风险账决定敢不敢。对日均三万页、要长期运营、且数据不能过第三方的场景,自研加云产品的组合对我们是对的;如果只是临时采几千页交差,托管服务明显更划算,没必要陪我们造轮子。给同样在纠结的读者一个建议:先量化自己的量级,再对照上面的量级表,答案通常就自己浮出来了。选型定了,下一步是算机器的账,看轻量应用服务器到底够不够,继续看 1.3 节。
