同一台设备的重量有四种写法:抽取数值的单位归一与量纲校验

发布时间:2026/10/28 23:43:29
同一台设备的重量有四种写法:抽取数值的单位归一与量纲校验

公开资料里抓出来的数值,写法不一致是常态。同一款设备的整机重量,说明书里是「12.5 kg」,官网参数栏写「12.5 千克」,经销商页面写「12500g」,另一篇评测文章写「约 12.5 KG(不含包装)」。

四个字符串两两不相等。把它们直接当成四个候选值丢进比对逻辑,结果只有两种:要么判成"数据冲突"报一堆假问题,要么取最后一条把冲突掩盖掉。

要让这类数值可比,得先把它们压成同一个内部表示:数值、量纲、规范单位、以及一条"这个数带不带限定"的标记。下面记录用标准库实现的做法,重点不在正则技巧,而在失败时怎么留痕。

一、词典按量纲分组,不按单位字符串平铺

第一种实现是把所有单位放进一张表:kg → 1、g → 0.001、斤 → 0.5、mm → 1、cm → 10。看起来能用,写到第三批资料就会出问题:同一批数据里既出现「12.5 kg」又出现「125 mm」,如果换算表是平铺的,比对时就无法回答"这两个数是不是在说同一件事"。

正确的结构是先分组再换算:量纲是桶,单位在桶里带一个到基准单位的因子。

DIMENSIONS = { "mass": { "基准": ("kg", 1.0), "单位": {"kg": 1.0, "千克": 1.0, "公斤": 1.0, "g": 0.001, "克": 0.001, "斤": 0.5, "吨": 1000.0}, }, "length": { "基准": ("mm", 1.0), "单位": {"mm": 1.0, "毫米": 1.0, "cm": 10.0, "厘米": 10.0, "m": 1000.0, "米": 1000.0, "英寸": 25.4, "inch": 25.4}, }, "power": { "基准": ("kW", 1.0), "单位": {"W": 0.001, "w": 0.001, "瓦": 0.001, "kW": 1.0, "千瓦": 1.0, "马力": 0.7355}, }, }

单位名要大小写各存一条。资料里KW、Kw、kw都出现过,M与m在功率与长度里还各指一种量纲。归一时统一转小写查表,比强行规定"输入必须符合某种大小写"要稳。

二、解析:先剥限定词,再拆数字与单位

「约 12.5 KG(不含包装)」这一条里有三层信息:数值 12.5、单位 kg、以及两个限定标记(约、条件说明)。限定词不能当垃圾丢掉,它决定了这条记录能不能用来做等值判断。

import re QUALIFIERS = {"约": "approx", "左右": "approx", "±": "tol", "不含": "exclude", "含": "include", "以上": "lower_bound", "以下": "upper_bound", "最大": "peak", "额定": "rated"} ALL_UNITS = [u for cfg in DIMENSIONS.values() for u in cfg["单位"]] def parse(text): raw = (text or "").strip() flags = [tag for word, tag in QUALIFIERS.items() if word in raw] m = re.search(r"-?\d+(?:\.\d+)?", raw) if not m: return {"raw": raw, "ok": False, "value": None, "unit": None, "flags": flags} tail = raw[m.end():].strip(" ,,。.;;()()") low = tail.lower() unit = max((u for u in ALL_UNITS if u.lower() in low), key=len, default="") return {"raw": raw, "ok": True, "value": float(m.group()), "unit": unit, "flags": flags}

取单位这一步用的是「词典里最长匹配」,而不是把数字后面的一串字符整段当单位收上来。差别很实际:约 12.5 KG(不含包装)里,贪婪收法会把「KG(不含包装」一起收进单位字段,查表必然落空;最长匹配拿到KG,剩下的部分只作为限定标记参与后续判断。

这也是为什么归一函数要区分两种失败:一类是单位不认识(词典没覆盖),一类是单位认识但量纲不对(质量字段里出现M)。

def normalize(parsed, expected_dim): if not parsed["ok"]: return {"status": "no_number", "detail": parsed["raw"]} bucket = DIMENSIONS[expected_dim]["单位"] unit = parsed["unit"].lower() for name, factor in bucket.items(): if name.lower() == unit: return {"status": "ok", "value": parsed["value"] * factor, "unit": DIMENSIONS[expected_dim]["基准"][0], "flags": parsed["flags"]} other = [d for d, cfg in DIMENSIONS.items() if unit in {u.lower() for u in cfg["单位"]}] return {"status": "wrong_dim" if other else "unknown_unit", "detail": "%s 出现在 %s 字段" % (parsed["unit"], expected_dim)}

wrong_dim与unknown_unit分开,是为了让规则扩展有依据。前者说明词典是全的、这一条数据本身有问题,应该回原文核对;后者说明这批资料的写法超出了当前覆盖,应该补词典。两种失败混在一个状态里,跑一个月之后谁也不会回头看。

四、比对阶段的三个陷阱

| 陷阱 | 典型输入 | 后果与对策 |
| --- | --- | --- |
| 把近似当相等 | 「约 12.5 kg」与「12.48 kg」 | 带approx标记的候选不参与等值判断,只参与范围判断 |
| 忽略口径差异 | 「12.5 kg(不含包装)」与「14 kg」 | 条件说明留成字段,比对时按同口径配对 |
| 精度信息丢失 | 「12.5」与「12.500」 | 原字符串留在结构里,归一值只用于比较 |

第二条最容易漏。设备重量按不含包装写、竞品按含包装写,两个数放一起就成了"竞品更重"的结论。这类错误在数值完全正确的前提下发生,事后很难通过抽查发现,因为它不是抽取错,是配对错。

第三条涉及一个取舍:归一值用浮点数,比对时用绝对差或相对差阈值。直接拿12.5与12.50做字符串相等判断,会漏掉真正需要人工确认的口径差异;把它们都当成同一个数,又会把"三位小数精度来自检测报告"这条线索抹掉。原字符串留在结构里,成本只是多存一列。

五、什么时候不该做归一

两类场景建议停在抽取层。一是资料本身没有单位,比如参数表里只有「12.5」一列,单位写在表外的注释段落里——这种要先解决表头与注释的归属关系,再谈单位归一。二是同名单位在不同量纲里含义不同,比如M在长度与功率语境里指向两个东西;这时归一函数会返回wrong_dim,正确的做法是回到原文补上下文,而不是猜一个。

数值可比性这件事,规则会随资料写法一直长。真正稳的做法是把失败留痕做扎实:哪一条没解析出来、以什么原因没解析出来,都能回查。有了这份留痕,词典与口径规则才有扩充的依据;没有留痕,只能在结论出来后回头怀疑所有数据。

(本文由一支长期做企业内容抽取的技术团队整理,欢迎同行交流指正。)