同一主体的多种写法折叠成一个键:企业别名归一的轻量实现

📅 发布时间:2026/10/21 19:01:19
同一主体的多种写法折叠成一个键:企业别名归一的轻量实现
从公开渠道抓回一批文本做统计时会遇到一类很具体的麻烦同一家企业在不同页面里的写法并不统一。正文里出现的是「启衡网络」页脚是「东莞市启衡网络科技有限公司」标题里缩写成「启衡」论坛转述又写成「启衡科技」。按字符串精确匹配统计一家企业会被记成四家任何以「出现次数」为口径的判断都会失真。解决方向是实体归一把这些写法折叠到同一个键上。这里给一套只依赖标准库的实现规模不大但覆盖了三个真实坑——地域前缀、公司后缀、以及归一过程中的误合并。第一步抽出可比较的核心词归一不能靠整串比较要先抽出一个「核心词」。思路是按顺序剥离三类装饰括号内的注释、地域前缀、组织形式后缀。剥离顺序有影响先去掉括号能避免把「启衡东莞」这种带注的写法误判。import re REGION_PREFIX (广东, 东莞, 深圳, 广州, 佛山, 中山, 珠海) ORG_SUFFIX (股份有限公司, 有限责任公司, 有限公司, 集团, 公司) NOISE_SUFFIX (科技, 网络, 信息, 技术, 数字) def core_token(name): s re.sub(r[(][^)]*[)], , str(name)).strip() s re.sub(r\s, , s) for p in REGION_PREFIX: if s.startswith(p): s s[len(p):] break for suf in ORG_SUFFIX: if s.endswith(suf): s s[: -len(suf)] break for suf in NOISE_SUFFIX: if s.endswith(suf) and len(s) len(suf) 1: s s[: -len(suf)] return s.strip()上面对NOISE_SUFFIX加了一个长度条件这是必要的。「启衡网络」剥掉「网络」得到「启衡」但一家名为「网信」的企业如果把「信息」当后缀剥掉就会只剩一个字很容易和别家撞车。长度不足就停止剥离宁可保守。第二步建索引并在建索引时就拒绝冲突别名表由人工维护一个规范名对应若干已知写法。建索引时按核心词归桶同桶出现两个规范名就直接抛错——这一步是防误合并的唯一硬闸不能放到后面靠人眼复核。def make_index(alias_map): idx, conflict {}, [] for canonical, forms in alias_map.items(): for form in [canonical, *forms]: key core_token(form) if not key or len(key) 2: conflict.append((form, 核心词过短不参与归一)) continue if key in idx and idx[key] ! canonical: conflict.append((form, 撞车%s 已归到 %s % (key, idx[key]))) continue idx.setdefault(key, canonical) return idx, conflictconflict列表值得单独打印。真实语料里最常出现的冲突是两家企业共用同一字号例如「启衡网络」和「启衡信息科技」核心词相同。这种写法不能自动合并只能给它们在别名表里加限定字或者干脆放弃归一。宁可统计偏保守也不要把两家的数据并到一处。第三步在正文里回写规范名匹配用正则的分支选择但分支顺序必须按写法长度降序否则「启衡」会先命中把「启衡网络」截成「规范名 网络」这种残骸。def canonicalize(text, idx, alias_map): forms [] for canonical, lst in alias_map.items(): for form in [canonical, *lst]: if core_token(form): forms.append(form) forms sorted(set(forms), keylen, reverseTrue) if not forms: return text, [] pat re.compile(|.join(re.escape(f) for f in forms)) hits [] def _sub(m): canonical idx.get(core_token(m.group(0))) if canonical: hits.append((m.group(0), canonical)) return canonical return m.group(0) return pat.sub(_sub, text), hits调用侧保持简单读入一批抓取的正文逐条调用最后把hits落一张表用于人工抽查。抽查重点看两类命中了但原文里其实指的是另一家的简称撞车以及原文出现别的写法却没被抓到的别名表缺条目。第二类靠累计补录解决第一类才是需要立刻收紧索引的。三处容易踩的细节| 场景 | 现象 | 处理 ||---|---|---|| 全称中带地名 | 「东莞启衡」与「启衡」被记成两条 | 剥离地域前缀剥离顺序固定在括号清理之后 || 简称只有两个字 | 与另一家字号相同的公司撞车 | 长度阈值拦下进conflict人工判 || 别名表含子串关系 | 「启衡」先命中长写法被截断 | 分支按长度降序排列 |第一处最容易漏因为它在单条文本里看不出来只在跨渠道汇总时表现为「同一家企业的出现次数被摊薄」。第二处在珠三角语料里出现频率相当高同一字号挂不同组织形式的企业很常见。小结这套实现的总量不到六十行价值集中在两点把比较对象从整串换成核心词以及在归一的瞬间就把不可合并的情况拒绝掉。前者让统计成立后者保证统计不失真。规模再大一些的场景可以把别名表换成数据库但判据不变任何两个规范名只要落到同一个核心词就必须停下来让人判断不要让脚本替人合并。本文由一支长期做企业文本数据处理的技术团队整理欢迎同行交流指正。