采集脚本写完只是开始。真正把这条链路撑住的是那份契约:每条记录都要能回答“我是从哪个源、哪天、用哪个解析器版本、以什么状态被记下来的”。

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 式分区路径:

text
data/raw/{source}/{category}/dt=YYYY-MM-DD/part-*.jsonl

采集侧直接按这个布局落盘,入湖就是整目录搬运,不需要任何再分区。ODS 层的形状在写爬虫那天就定下来了,这是“契约先于建模”最实际的收益。

管道顺序也固定:补元数据 → 校验 → 跨页去重 → 写 JSONL。校验放在去重之前,是因为去重会掩盖问题——如果重复率异常高,那本身就是分页或回绕出错的信号,被去重悄悄抹掉反而坏事。

06事故变成测试用例

契约不是写完就完。每次线上事故,回归到采集层的都补进单测:兜底页判定、编码解码、价格文案分类、分页回绕。有一份事故回归用例在,重构解析器时才敢动。

07相关文章