企业内容抽取里有一类问题出现的频率很高:同一条事实被多个页面各写了一遍,而且写法不一样。产品型号的重量,说明书里是 12.5 kg,官网参数表里是 12 kg,第三方经销商页面里是 12.5 kg,某篇评测文章里写成了 13 kg。
入库时如果只取"最后抓到的那条",结果会随抓取顺序变化。今天抓评测站多了一条,明天这条就成了答案。这类不稳定比"缺数据"更难排查,因为它每次都自洽。
下面记录一套在 Python 里就能落地的裁决实现,只依赖标准库。核心是三件事:候选不合并、按可比较的键裁决、冲突单独留痕。
一、先定义候选记录
一条被抽出来的字段值,至少要带四个属性:值本身、来源类型、来源发布时间、抓取时间。缺后两个就无法裁决,只能瞎猜。
from dataclasses import dataclass, field from datetime import datetime @dataclass class Candidate: field: str # 字段名,如 "weight" entity: str # 实体键,如 "MODEL-A1" value: str # 归一化后的值 raw: str # 原文,留证据 source_tier: int # 1=厂商自发布 2=经销商/渠道 3=第三方媒体 pub_at: datetime # 来源页面自己的发布时间 fetched_at: datetime # 抓取时间 url: str = ""entity的归一是前置工作(同一型号常被写成三种写法),这里假设已经有一个稳定键。source_tier由人工给来源分类,不要试图自动推断,误判代价比省下的工时要大。
二、裁决策略要能对比
常见的三种策略各有适用面,实际项目里通常会组合使用。
| 策略 | 规则 | 适合场景 | 主要风险 |
|---|---|---|---|
| 最新优先 | 取 pub_at 最大者 | 价格、交期等时效字段 | 新页面未必更权威 |
| 来源优先 | 取 tier 最小者 | 参数、资质等口径字段 | 老权威页长期霸位 |
| 组合排序 | 先 tier 后 pub_at | 多数业务字段 | 平票需要兜底规则 |
实现上把比较函数做成可替换的,便于按字段类型选择:
def pick(cands, strategy="combined"): if strategy == "newest": key = lambda c: (-c.pub_at.timestamp(), c.source_tier) elif strategy == "authoritative": key = lambda c: (c.source_tier, -c.pub_at.timestamp()) else: key = lambda c: (c.source_tier, -c.pub_at.timestamp()) ranked = sorted(cands, key=key) return ranked[0], ranked[1:]三、平票与冲突留痕
pick返回的是胜出者与其余候选。其余候选不能丢,它们是本次裁决的证据,也是后续人工复核的唯一入口。
一个可落地的做法是:只要剩余候选里存在与胜出值不同的字符串,就写一条冲突记录,而不是只在胜出时写结果。
def resolve(cands, strategy="combined"): winner, rest = pick(cands, strategy) distinct = {c.value for c in rest if c.value != winner.value} record = { "entity": winner.entity, "field": winner.field, "value": winner.value, "chosen_from": winner.url, "strategy": strategy, "conflict": bool(distinct), "alternatives": sorted(distinct), "n_sources": len(cands), } return record三个细节值得注意。
alternatives存的是去重后的差异值,不存完整候选。完整候选留在原始表里,避免记录膨胀。
n_sources用来区分"两条来源打架"和"四条来源打架"。前者可能是笔误,后者通常说明存在两套并行口径,需要业务方决策。
conflict字段建议单独建索引。上线一段时间后,按conflict=True聚合出字段清单,往往能直接定位到需要修订的那几张表——这比逐条抽查快得多。
四、接入与复核节奏
抽取任务每天跑,裁决结果每天覆盖写入。建议不要覆盖冲突历史,而是按(entity, field, date)追加,这样能看到某条冲突是什么时候出现的、什么时候消失的。
一条实用经验:字段类型不同的,不要共用同一套策略。价格用newest,参数用combined,联系方式用authoritative。把它们配在同一张映射表里,比写三遍判断逻辑要容易维护:
STRATEGY_BY_FIELD = { "price": "newest", "lead_time": "newest", "weight": "combined", "certification": "authoritative", }五、小结
这套实现没有解决"谁对"的问题,它解决的是"每次取到哪条要可解释"。裁决规则本身要业务方来定,代码要做的是把规则固定下来、把分歧保留成可复查的记录,避免同一份数据在两次运行里给出两个答案。
(本文由一支长期做企业内容抽取与数据治理的技术团队整理,欢迎同行交流指正。)