1.1 需求拆解:要采什么、采多快
场景:周五下午甩过来的表格
业务同学丢过来一份表格:二十三个行业站点,每个站点若干列表页,要求每天把新发布的公开页面采回来,字段就四项——标题、正文、发布时间、来源链接。听起来是个小活,但他补了一句:下周起站点数量要翻三倍,下半年还要接更多行业。
我们当时的采集能力是一台机器上的定时脚本,crontab 每晚跑一轮。问题不在脚本写得不对,而在没人说得清它跑没跑完:没有任务状态、没有失败告警、重跑一次就重复入库。需求压到面前时,我们先停下了键盘——不把需求拆成可量化的指标,写出来的代码一定返工。
开会的时候还有一个小插曲。我们问业务:如果凌晨窗口跑挂了,你们能接受多久之内修好?对方愣了一下说,只要早上九点前数据在看板上,没人关心你们几点跑。这句话后来成了我们整个调度策略的基石:把采集当成离线批处理来做,而不是准实时系统,架构复杂度瞬间降了一个量级。这本书里几乎所有关于凌晨窗口、优先级、断点续爬的设计,都是在给这句话做注脚。
采集需求最怕笼统。我们拉着业务侧开了两个小时会,只问三件事:数据给谁看、能容忍多大延迟、历史数据要不要。这三个答案定下来,架构复杂度的量级就定下来了。这本书里所有看起来过度设计的地方,基本都是被这三个问题的答案逼出来的。
方案:把模糊需求翻译成指标表
我们把需求拆成五项可测量指标,每项都对应一个具体的架构决定。这张表后来贴在墙上,每次加站点都先改表再动代码:
| 需求维度 | 我们填的值 | 对应的架构决定 |
|---|---|---|
| 采集对象 | 23 个行业站、约 1200 个列表页 | 按站点分片,worker 按站点维度分工 |
| 时效要求 | 次日 9 点前出齐即可 | 凌晨窗口集中跑,白天留给回补任务 |
| 页面规模 | 日均新增约 3 万页、总量约 500 万页 | 必须增量抓取,全量只用于首次铺底 |
| 单页字段 | 标题、正文、发布时间、来源链接 | 清洗管道四个阶段足够,不搞万能 DSL |
| 数据用途 | 内部看板加对外数据接口 | 结果落 MySQL,原始快照留 COS 备查 |
这张表最大的作用是挡住伪需求。有人提出要实时秒级采集,我们指着时效那一行说当前是次日级;真要实时就得另起一套流式架构,机器和人力成本都要翻几倍,这个账在 5.3 节的成本复盘里细算。还有人问能不能顺带采图片视频,单页字段一行写死了只采文本,多媒体需求单独立项,不让主链路变重。
体量估算有个粗略公式:站点数乘列表页数乘翻页深度乘日均更新率。我们代入 23 站、1200 个列表页、翻页 10 层、日均更新率百分之八,算出来每天新增约三万页,和后来实际线上跑出来的数字误差在一个百分点以内。比拍脑袋准得多,也比直接拿历史日志外推快——首次铺底之前根本没有历史日志。具体拆开是这样:
| 估算项 | 估算方式 | 我们的取值 | 估算结果 |
|---|---|---|---|
| 日均新增页数 | 站点 × 列表页 × 翻页深度 × 更新率 | 23 × 1200 × 10 × 8% | 约 2.8 万页 |
| 单日下载量 | 新增页数 × 页均大小 | 2.8 万 × 80KB | 约 2.2 GB |
| 首次铺底总量 | 站点数 × 每站历史存量 | 23 × 约 22 万页 | 约 500 万页 |
| 快照年增长 | 日新增 × 365 × 压缩后页均 | 2.8 万 × 365 × 8KB | 约 80 GB |
最后两行是后来加的。最初我们只估了下载量没估存储量,导致 COS 的容量规划晚了半个月才提上日程——快照存 COS 的决策见 2.4 节。这个表格的意义在于:所有架构决定都能往前追溯到某个数字,而不是我觉着行。
增量抓取是另一个需要提前想清楚的点。列表页每天都有新内容,但绝大多数详情页不变。如果每天把 1200 个列表页翻到底再逐页抓,等于每天都在半全量。我们的做法是以列表页为增量入口,详情页按指纹去重,只采列表里新出现的条目。去重的具体实现是 2.2 节的内容,这里只需要知道:增量策略必须在需求拆解阶段就定下来,它是后面所有队列与状态设计的前提。
配置与代码:先把任务定义写下来
指标定稿后,我们最先落地的不是爬虫本体,而是一份任务描述结构。后来整个平台的调度、分片、断点续爬都建立在这个结构上:
type TaskSpec struct {
SiteID string `json:"site_id"`
EntryURL string `json:"entry_url"`
ListRule string `json:"list_rule"`
DetailRule string `json:"detail_rule"`
Depth int `json:"depth"`
IntervalH int `json:"interval_h"`
Priority int `json:"priority"`
UserAgent string `json:"user_agent"`
Enable bool `json:"enable"`
}
其中三个字段是后来加的。Priority 用来给热点站点插队,UserAgent 用来应对 5.2 节讲的封禁,Enable 是给运维留的应急开关——某个站突然抽风时不用改代码重启,直接把任务停掉。这说明需求拆解不可能一次到位,但先把主结构立住,后面加字段的成本就低得多。
优先级我们分三档:P0 是客户点名要的站点,P1 是常规更新,P2 是历史回补。master 按档位分发任务,worker 本地队列里 P0 永远排最前。这套分发机制在 3.1 节展开,这里只强调一点:优先级不是调度器自己的事,是需求阶段就要确认的业务语义。
另外把次日九点这个交付时间也写进了任务结构体之外的地方——一个全局的截止检查器。每天九点前控制台会把未达标任务标红,相关人员手机收到提醒。这个简单的功能让扯皮从每天都发生变成一个月一次。
踩坑:三个没问出口的问题
第一个坑是翻页深度。业务说每个列表页看第一页就行,实际上有的站点新内容藏在翻页深处,我们按一页采,上线第二周漏了将近三成数据,又回头补翻页逻辑。教训:深度参数必须允许按站点覆盖,全局默认值只能兜底。
第二个坑是动态加载。有个站的列表用接口异步渲染,我们按静态 HTML 解析,采到的列表项永远是缓存里那批。上线当天看数据量正常,第二天才发现更新时间全是历史时间。后来加了抓包确认接口地址的前置流程,遇到量异常先怀疑渲染方式,这套排查清单在 5.2 节。
第三个坑是缓存。开发环境每次都请求新页面,测试很顺;上线后发现部分页面走了站点侧 CDN 缓存,采回来的是几小时前的旧内容,发布时间失真。我们的对策是发布时间一律以页面正文解析为准,不信 HTTP 响应头里的时间,同时把响应头的 Last-Modified 记进日志,方便对账。
第四个坑是业务自己也没想清楚。有一次对方说要加六个站点,协议签下来才发现其中两个需要登录才能看列表,等于需求范围直接变了。从那以后加站点必须先过一个小检查单:是否公开可访问、是否有反爬、字段是否在现有规则覆盖范围内、更新频率多少。过不了检查单的站点先进排队,不进开发计划。这个小检查单在 5.4 节的合规护栏里也有。
后来我们把检查单做成了一个小工具:填一个站点地址,自动跑 robots 检查、抓几页样本、预估更新频率、输出一份评估报告。工具虽然粗糙,但让加站点这件事从拍脑袋变成了流程,业务侧也服气。
口径:怎么算采到了
需求拆解还有一个容易漏的产出:验收口径。如果没有明确口径,出了问题双方各执一词——业务说数据少了,我们说脚本跑完了。所以我们把验收标准也写进了指标表的附注:
| 验收项 | 判定标准 | 差了怎么办 |
|---|---|---|
| 完整率 | 列表条目数与详情页入库数误差小于 2% | 先看快照,再决定补采或改规则 |
| 及时率 | 次日 9 点前完成率大于 99% | 查凌晨窗口是否被其他任务挤占 |
| 准确率 | 抽样五十条人工核对,字段错误小于 1% | 触发清洗规则回溯 |
| 去重率 | 重复入库条目占比小于 0.1% | 查指纹库是否漏判 |
口径定好了,后面的监控告警才有阈值可配——5.1 节的告警规则几乎照搬了这张表。这也是为什么我们坚持把验收口径放在需求阶段而不是上线之后:上线后再补口径,历史数据没有基线可比。
小结
需求拆解的产出不是一份文档,而是一张指标表加一份任务结构体:表挡伪需求,结构体定主骨架。没有这两样,后面写多少代码都是在给模糊需求还债。指标确定之后,下一步是算方案的账:自研、用现成框架还是直接买托管服务,这正是 1.2 节的内容;机器要花多少钱在 1.3 节。如果你对整体技术地图更感兴趣,也可以先跳到 1.4 节。
最后送一句给同样在小团队里做采集的人:别急着写第一行抓取代码。我们花在拆需求上的两个小时,是整个项目里回报最高的两个小时。
