采集脚本写完只是开始。真正把这条链路撑住的是那份契约:每条记录都要能回答“我是从哪个源、哪天、用哪个解析器版本、以什么状态被记下来的”。
01主键里没有商品,只有快照
契约的主键是 (source, source_sku_id, snapshot_date)。
注意它不含商品身份。同一个源里同一个页面条目,每天是一行新记录。跨源、跨日的“这是同一个商品”属于下游实体对齐的职责,不属于采集层。
这个划分让我躲开了一个大坑:如果在采集层就试图认出商品,那解析器的每次改动都会悄悄改变商品身份,而这件事没有任何地方能审计。现在采集层只对“页面当时的样子”负责,责任边界清楚。
02price_status:价格字段最要紧的一件事
价格不能是单纯的数值列。一个空的 price 可能意味着四种完全不同的事:
| 状态 | 含义 | 下游该怎么用 |
|---|---|---|
parsed | 正常解析出价格 | 可参与统计 |
missing | 页面上根本没有价格 | 不计入价格口径 |
negotiation | 面议 / 议价 / 询价 | 不是零价,是“无报价” |
unavailable | 暂无 / 缺货 / 下架 / 停产 / 概念产品 | 供给侧状态,可单独分析 |
suspect | 解析出值但明显不合理 | 归入异常,不进指标 |
分类来自文案词表:面议一类进 negotiation,缺货停产一类进 unavailable。区间价另设 price_low / price_high / is_range,不强行折成一个数——把区间中点当价格,等于凭空造了一个页面上不存在的数。低于类目价格下限的一律判 suspect 并把价格置空;带“万”的做倍率折算而不是当作小数值。
price_status 的价值在聚合时体现:算价格可用率必须把 negotiation 和 unavailable 从分母里摘出去,否则“这个类目价格缺失严重”和“这个类目大量无报价”会被混成同一句话。这两件事的处置方式完全不同——前者是解析问题,后者是市场事实。
03schema_version 与 parser_version 分开
两个版本号管两件不同的事:
schema_version:字段结构变了。加列、改语义、改主键都要升版。parser_version:解析实现变了,结构没变。
只有后者的变化,意味着同一张表里不同批次的数据来自不同精确度的解析器。想比较“改版前后覆盖率是否真的提升”,就必须按 parser_version 切开看,否则会把解析改进误读成市场变化。
再配两个指纹:content_hash(行级)与 page_hash(页面级)。行级用来做跨页去重和变更检测,页面级用来定位“这一批行是同一次抓取的产物”。
04原始页面留档:把“重放”变成可能
data/raw_http/ 存原始响应,data/raw/ 存解析结果。留档带来的能力是:canonical 数据可以从页面重建,不必重新采集。
这件事在解析器改版后价值巨大。新解析器上线,只要对着历史页面重放,就能在同一批输入上比较两版解析结果的差异——这是一次真正的回归测试,而且完全不碰源站。反过来,如果只存解析结果,改版后你只有两种坏选择:拿新数据赌它更好,或者承认无法验证。
留档也要有代价意识:原始 HTML 比 JSON 大得多,所以要压缩并保留窗口,配合每日归档清单,别让它无限膨胀。
05目录结构就是 ODS 的形状
存储用 Hive 式分区路径:
data/raw/{source}/{category}/dt=YYYY-MM-DD/part-*.jsonl采集侧直接按这个布局落盘,入湖就是整目录搬运,不需要任何再分区。ODS 层的形状在写爬虫那天就定下来了,这是“契约先于建模”最实际的收益。
管道顺序也固定:补元数据 → 校验 → 跨页去重 → 写 JSONL。校验放在去重之前,是因为去重会掩盖问题——如果重复率异常高,那本身就是分页或回绕出错的信号,被去重悄悄抹掉反而坏事。
06事故变成测试用例
契约不是写完就完。每次线上事故,回归到采集层的都补进单测:兜底页判定、编码解码、价格文案分类、分页回绕。有一份事故回归用例在,重构解析器时才敢动。
07相关文章
- 源站的脾气:两个源的脾气:编码、兜底页、分页回绕和 503 死循环
- 波动怎么归因:采集量骤降的归因:先怀疑自己,再怀疑源站