关键词:企业信息一致性检查、正则抽取、中文日期归一化、NFKC 全角转换、统一社会信用代码校验、多源 diff、Python 实战、数据清洗
摘要
同一家公司的成立日期、注册地、信用代码,在官网、招聘平台、投标文件里经常是三种写法。人眼看不出来,模型更不会帮你判断哪个对。
本文给一套三层实现:正则抽取 → 归一化 → 两两比对,输出 OK / PREFIX_OK / CONFLICT / MISSING 四种状态。代码为示意实现,未做第三方验证。
一、背景
读 Anthropic 的 S-1 拆解报道时注意到一个结构:约 7,800 亿到 8,100 亿美元的算力合同承诺,期限长、价格基本锁死;收入那侧 80% 按量计费。
成本近固定,收入浮动。差额得靠调用量往上填。
对做工程的人来说,这是一个输入条件------当"让答案更少出错"成为产品目标,答案出错的一个常见来源就是多源口径打架。
先把自己的口径对齐,是成本最低的一步。
二、技术方案
2.1 整体结构
原始文本 → extract() 正则抽字段 → norm_*() 归一化 → diff() 两两比对 → 状态列表
2.2 归一化:日期
三种常见写法要收敛成一个:
| 来源 | 原始写法 | 归一化后 |
|---|---|---|
| 官网 | 2020 年 12 月 17 日 | 2020-12-17 |
| 招聘平台 | 2020-12-17 | 2020-12-17 |
| 投标文件 | 二〇二〇年十二月十七日 | 2020-12-17 |
python
import re
import unicodedata
CN_NUM = {"零": 0, "一": 1, "二": 2, "三": 3, "四": 4,
"五": 5, "六": 6, "七": 7, "八": 8, "九": 9}
def norm_date(s: str) -> str:
s = unicodedata.normalize("NFKC", s).strip()
m = re.search(r"(\d{4})\D{1,3}(\d{1,2})\D{1,3}(\d{1,2})", s)
if m:
y, mo, d = m.groups()
return f"{int(y):04d}-{int(mo):02d}-{int(d):02d}"
m = re.search(r"([零一二三四五六七八九〇]+)年"
r"([零一二三四五六七八九十]+)月"
r"([零一二三四五六七八九十]+)日", s)
if m:
y = "".join(str(CN_NUM.get(c, c)) for c in m.group(1))
def cn2int(t):
if "十" in t:
a, _, b = t.partition("十")
return (CN_NUM.get(a, 1) if a else 1) * 10 + (CN_NUM.get(b, 0) if b else 0)
return sum(CN_NUM.get(c, 0) for c in t)
return f"{int(y):04d}-{cn2int(m.group(2)):02d}-{cn2int(m.group(3)):02d}"
return s
2.3 归一化:地址与编号
地址不能比相等。省市区前缀在不同来源里写全写简都有,用尾部包含更稳。
python
def norm_addr(s: str) -> str:
s = unicodedata.normalize("NFKC", s).strip()
for p in ("中国", "浙江省", "浙江省杭州", "杭州市", "杭州"):
s = s.replace(p, "")
return s.strip("省市区 \t")
def norm_code(s: str) -> str:
return unicodedata.normalize("NFKC", s).strip().upper()
2.4 抽取与比对
python
from itertools import combinations
from dataclasses import dataclass, field
@dataclass
class Source:
name: str
raw: str
fields: dict = field(default_factory=dict)
PATTERNS = {
"founded": (r"(?:成立(?:于|时间)?[::]?\s*)?((?:\d{4}\D{1,3}\d{1,2}\D{1,3}\d{1,2})"
r"|(?:[零一二三四五六七八九〇]+年[零一二三四五六七八九十]+月[零一二三四五六七八九十]+日))", norm_date),
"address": (r"(?:注册地|地址)[::]?\s*([^\s,,。;;\n]{4,30})", norm_addr),
"reg_no": (r"(?:统一社会信用代码|注册号)[::]?\s*([0-9A-Z]{15,20})", norm_code),
}
def extract(src: Source) -> Source:
for key, (pat, fn) in PATTERNS.items():
m = re.search(pat, src.raw)
if m:
src.fields[key] = fn(m.group(1))
return src
def diff(sources):
out = []
for a, b in combinations(sources, 2):
for key in set(a.fields) | set(b.fields):
va, vb = a.fields.get(key), b.fields.get(key)
if va is None or vb is None:
st = "MISSING"
elif va == vb:
st = "OK"
elif key == "address" and (va.endswith(vb) or vb.endswith(va)):
st = "PREFIX_OK"
else:
st = "CONFLICT"
out.append({"field": key, "a": a.name, "b": b.name,
"status": st, "detail": f"{va} vs {vb}"})
return out
三、案例
python
srcs = [
extract(Source("官网", "本公司成立于2020年12月17日,注册地浙江省杭州市余杭区,统一社会信用代码91330100MA2XXXXX1A")),
extract(Source("招聘平台", "成立时间:2020-12-17 地址:杭州市余杭区 统一社会信用代码:91330100MA2XXXXX1A")),
extract(Source("投标文件", "公司于二〇二〇年十二月十七日成立,注册地余杭区")),
]
for r in diff(srcs):
print(r["status"].ljust(10), r["field"].ljust(9), r["a"], "↔", r["b"], "|", r["detail"])
样例输出(本地跑通的格式演示,非统计结论):
OK founded 官网 ↔ 招聘平台 | 2020-12-17 vs 2020-12-17
PREFIX_OK address 官网 ↔ 招聘平台 | 余杭区 vs 余杭区
OK founded 官网 ↔ 投标文件 | 2020-12-17 vs 2020-12-17
CONFLICT reg_no 投标文件 ↔ 招聘平台 | None vs 91330100MA2XXXXX1A
第三行是关键。中文数字写法肉眼容易漏,归一化后能对齐。
四、总结
| 环节 | 要点 |
|---|---|
| 预处理 | NFKC 必须先跑,否则全角数字让正则整段失效 |
| 日期 | "十"要单独处理:十二=12、二十=20、二十二=22 |
| 地址 | 用尾部包含判断,状态标 PREFIX_OK 而非 OK |
| 输出 | 只报不修,交人工判断 |
五、适用边界
- 正则只覆盖三个字段,扩字段要自己补
PATTERNS。 - 归一化会漏。漏了原样返回,靠
MISSING暴露。 - 不要做自动回写。我试过一版自动以官网值为准,结果官网那页本身是旧稿,反而把错误扩散到另外两个来源。
- 代码为示意实现,未做第三方验证,勿直接用于生产。