资讯动态

Python实战:实体消歧与店铺公司名称标准化合并方案

发布时间:2026/9/12 9:06:11 来源:尧图企业网站定制
做爬虫的朋友大概都遇过这样的事明明是一家店在美团上叫“老张牛肉面”大众点评上叫“老张牛肉面(人民路店)”饿了么上又叫“老张牛腩面馆”。数据单独看都没问题一旦混在一起入库麻烦就来了重复统计、客户档案混乱、分析结果失真。这个话题在数据工程里有个正经名字——实体消歧Entity Resolution说白了就是搞清多个记录到底是不是同一个实体。我最近在做一批店铺/公司数据的采集和清洗把全过程和踩坑记录整理出来给同样被同名实体、写法差异折磨的人做个参考。这篇文章不谈高深算法重点讲怎么用 Python 实现对店铺/公司名的规则合并与标准化。内容包括为什么先标准化再比较、哪些规则最好用、相似度阈值怎么定、批量数据怎么跑得快以及我真实踩过的坑。无论你是爬虫工程师、数据分析师还是要做竞品监控的人这篇文章都能给你一套直接落地的思路和代码。1. 为什么要做同名实体消歧爬虫数据的真实痛点1.1 数据源复杂带来的实体识别难题爬虫采集的数据通常来自多个平台、多个页面、多个时间点。平台方对商家名称的录入规则完全不同有的强制带行政区划有的允许自己随便填还有的会把分店名写成“XX店”、“XX二店”。这就导致同一个实体在数据库里可能有四五条记录而且每条都长得很像但又不完全一样。举个真实的例子。我采集某餐饮平台的商家数据时同一家连锁咖啡店在不同合作渠道的记录分别是瑞幸咖啡(中关村大街店)瑞幸咖啡中关村店luckin coffee 中关村大街店瑞幸咖啡中关村店如果只靠数据库主键或者精确匹配这四条记录会被当成四家店。实体消歧要解决的就是这类问题把这些不同写法的记录识别成同一个实体合并成一条干净的数据然后归档到统一的库里。这个问题不只发生在中文平台。英文、日文数据同样存在大小写、空格、冠词、简称的差异。只是中文因为分词、繁简、全半角、行政区划词缀的问题处理起来更加琐碎光靠通用查重工具很难解决。1.2 消歧在数据工程链路中的价值我最早对实体消歧不以为然觉得多做一步清洗而已。直到一次统计项目里同一个公司因为在 excel 表里出现了“XX科技有限公司”、“XX科技股份有限公司”、“XX科技深圳有限公司”三个版本导致去重结果误差超过 30%才意识到这个环节有多重要。从整个数据链路来看实体消歧位于“采集 → 清洗 → 结构化 → 入库”里的清洗阶段。它的质量直接决定下游所有统计口径是否可靠。举几个典型场景竞品分析统计某个品牌的门店数、覆盖城市、开店趋势如果同名实体没合并数量直接翻倍客户管理同一客户的多个联系人、多个订单会因为名称不同而被拆成多个客户严重影响后续销售策略数据仓库建设多张表通过名称字段关联时名称不统一会直接导致 join 失败或重复行风控与反作弊批量注册的小号经常使用相似的公司名或店铺名标准化后更容易发现关联关系所以实体消歧不是锦上添花而是数据可用性的底线。如果你的数据只是自己用、一次性的那手工排重也能凑合。但只要数据要反复使用或者要交给团队其他人做分析尽早把名称标准化和消歧做好会省出数倍的时间。2. 核心思路拆解规则合并与标准化的基本框架2.1 先标准化再比较最后合并做实体消歧最忌讳一上来就计算字符串相似度。两串文本如果本身没有经过规范化相似度计算会被各种噪声因素干扰。比如“科技有限公司”里的全角括号、繁体字、首尾空格都会让距离变大或变小但这些跟实体本身关系不大。我推荐的流程是三步走标准化Normalize→ 分块Block→ 合并Merge。标准化是把数据“擦干净”统一字符、格式、词序让后续比较不受表面噪声干扰。分块是找到可能相同的候选组避免全量两两比较。合并是在候选组内部做相似度判断决定是否合并以及以哪条记录为主。这个流程可以类比成两个人比身份证信息先确认身份证号长度、出生日期格式是否一致再去比对姓名拼音最后才判定是不是同一人。如果一开始就把格式差异算进去比较结果会失真。2.2 从“字符串匹配”到“业务语义匹配”刚开始做消歧时我满脑子都是字符串相似度算法编辑距离、Jaccard、余弦相似度……跑了一轮后发现问题很大。“北京老张牛肉面”和“老张牛肉面北京店”的字符串重叠度很高但实际上可能是同一家店“老张牛肉面”和“张三牛肉面”只差一个字相似度也很高却是两家完全无关的店。这说明只靠纯字符相似度解决不了业务语义层面的问题。更好的做法是结合规则体系先把公司名或店铺名拆成业务要素比如“主体词 行业词 行政区域 组织形式”再对每个部分做归一化。这样“北京老张牛肉面”会被拆成“北京 | 老张 | 牛肉面”“老张牛肉面北京店”会被拆成“北京 | 老张 | 牛肉面 | 店”核心要素一致就能用规则判定是同一个实体。规则合并的本质是建立一个“业务知识字典”哪些词是行政区划、哪些词是品牌名、哪些词只是通用后缀。把这些字典用好准确率能轻松超过纯算法。当然规则合并也有短板面对没见过的写法时可能失效。所以实战中我通常用规则做粗筛用相似度做细判两层结合。2.3 店铺名和公司名的处理差异店铺名和公司名虽然都要做标准化但处理逻辑差别很大我踩过不少坑。公司名结构相对规范通常包含地名、字号、行业、组织形式四部分比如“深圳市腾讯计算机系统有限公司”。处理时重点是“有限公司”、“有限责任公司”、“股份有限公司”这些组织形式的归一以及“深圳市”和“深圳”这类地名省略的匹配。店铺名则自由得多可能只有“老张面馆”四个字也可能带分店标识“老张面馆(第二分店)”。店铺名里行业词往往是核心业务标签比如“面馆”“烧烤”“美容”这些词不能轻易删否则“张记烧烤”和“张记美容”会被误合并。这个区别我在最初设计规则时没有意识到导致第一批消歧结果里出现了“老王修脚”和“老王足疗”合并成一个实体的乌龙。所以设计规则前先想清楚处理的是公司名还是店铺名还是两者混合。如果混合建议先启用类型标记字段再按类型走不同规则。3. 实操过程店铺/公司名标准化的完整实现3.1 原始数据预处理与清洗标准化的第一步是处理字符层面的脏数据。这一步虽然简单但漏掉任何一个细节后面都可能引发连锁错误。我一般按以下顺序处理去首尾空白压缩中间连续空格繁体转简体全角转半角统一英文大小写去除标点符号、括号、特殊符号统一品牌名缩写/中英文别名直接上代码import re import opencc from unicodedata import normalize converter opencc.OpenCC(t2s) def clean_text(raw: str) - str: 基础清洗流程所有名称进入后续处理前先过一遍 if not raw or not raw.strip(): return s raw.strip() # 繁体转简体 s converter.convert(s) # 全角转半角NFKC会把全角字母、数字、标点统一转半角 s normalize(NFKC, s) # 压缩空白\s 包含空格、制表符、换行 s re.sub(r\s, , s) # 移除常见的分隔符和括号避免干扰后续分词 s re.sub(r[·•・,.。;:!?、()\[\]【】《》“”\\\-—_~], , s) return s.lower()这段代码里有两个容易被忽略的细节NFKC全角转半角很多人会漏掉。全角括号、全角空格在中文网页里非常常见不转换的话“北京老张面馆”和“北京老张面馆东门店”即便只差括号也会造成不必要的误差。s.lower()是为了对付英文品牌名“Luckin Coffee”和“luckin coffee”实测不转成小写相似度会偏低一截。顺带提醒opencc 的t2s只做繁体转简体不会处理日文汉字变体。如果你的数据里有“株式會社”这类日文写法最好单独建一个替代映射表。3.2 核心规则规范化公司类型词和品牌词的统一做完基础清洗就要动“业务词”了。这部分是整个消歧规则里最有含金量的地方也是我花时间最多的地方。首先处理公司组织形式词。现实里“深圳市腾讯计算机系统有限公司”和“深圳市腾讯计算机系统有限责任公司”指同一家公司“XX股份公司”和“XX股份有限公司”也极为可能是同一家。我把常见词归成标准化标签company_type_map { 有限公司: 有限公司, 有限责任公司: 有限公司, 股份有限公司: 股份公司, 股份公司: 股份公司, 集团有限公司: 集团, 集团: 集团, 合伙企业: 合伙企业, 工作室: 工作室, 个体户: 个体户, 商行: 商行, 门市部: 门市部, 诊所: 诊所, }应用规则前需要先把业务词从名称里摘出来避免误替换。比如“有限公司”必须出现在名称尾部才算组织形式词出现在“公司简介”这类干扰字段里要忽略。用正则做边界判断比较稳妥def normalize_company_type(name: str) - tuple[str, str]: 返回 (去掉组织形式的核心名称, 标准化组织形式) # 优先匹配最长的组织形式词 for key in sorted(company_type_map.keys(), keylen, reverseTrue): pattern re.escape(key) r$ if re.search(pattern, name): core re.sub(pattern, , name) return core, company_type_map[key] return name, 用长度倒序匹配是因为“有限责任公司”比“有限公司”长如果不先匹配一个“XX有限责任公司”会被误切成“XX责任”“有限公司”这就不对了。然后是品牌词和行业词的统一。品牌词需要根据业务领域单独维护字典比如“瑞幸咖啡”和“luckin coffee”要映射成同一个 token。这种字典没有捷径只能在数据清洗阶段人工积累。行业词则可以用分词的词性标注来辅助但中文分词本身有误差我更推荐先用自带词典做精准匹配brand_alias { luckin coffee: 瑞幸咖啡, luckin: 瑞幸咖啡, starbucks: 星巴克, 星巴克: 星巴克, kfc: 肯德基, 肯德基: 肯德基, }这里有个容易踩的坑品牌别名映射时如果把“luckin”直接替换成“瑞幸咖啡”那“luckin tea”瑞幸茶饮这类品牌衍生产品也会被误替换。所以替换前建议加一层上下文判断确定关键词是作为独立词出现而不是长词的一部分。3.3 地理词和商圈词的标准化店铺名和公司名里最常出现的就是地名。地名最大的问题是层级混乱同一个地方有的写“北京市海淀区中关村大街”有的写“海淀中关村”有的写“中关村”。地理词标准化我采用两步走。第一步是行政区划归一把省、市、区、街道等行政区划后缀统一格式并建一个常用行政区划对照表。第二步是商圈别名映射比如“中关村”和“中关村大街”在业务上往往指同一片区域可以映射成同一个商圈标签。district_shortcut { 北京: 北京市, 上海: 上海市, 广州: 广州市, 深圳: 深圳市, 海淀: 北京市海淀区, 朝阳: 北京市朝阳区, 浦东: 上海市浦东新区, }真实环境里同一个城市简称可能对应多个城市比如“中山”既是市名也是人名直接在规则里一刀切会误伤。更稳妥的做法是结合上下文如果名称里出现了“市”、“区”、“省”等行政区划关键词再触发替换如果只是孤立的名字宁可保持原样也不硬套。地理词标准化的目标不是把所有地名都换算成完整行政区划而是让同一个地方的写法尽量收敛成同一个版本。比如“北京市海淀区”和“海淀区”如果不收敛后面分块时就会分散到不同分组导致消歧漏掉。3.4 相似度计算与合并策略阈值怎么定完成标准化后就可以做相似度计算了。纯规则能解决的问题大概覆盖 60% 的常见情况剩下 40% 的模糊情况还是要靠相似度兜底。我常用两种相似度字符编辑距离和分词后的 Jaccard 相似度。编辑距离适合发现拼写差异比如“老张牛肉面”和“老张牛腩面”只差一个字Jaccard 适合发现词序打乱的写法比如“北京老张面馆”和“老张面馆北京店”。实际操作中我把两者加权融合import jieba from Levenshtein import ratio def char_similarity(a: str, b: str) - float: 基于编辑距离的相似度python-Levenshtein 里的 ratio 返回 0-1 if not a and not b: return 1.0 if not a or not b: return 0.0 return ratio(a, b) def token_jaccard(a: str, b: str) - float: 分词后 Jaccard 相似度处理词序不一致的情况 tokens_a set(jieba.lcut(a)) tokens_b set(jieba.lcut(b)) if not tokens_a or not tokens_b: return 0.0 inter len(tokens_a tokens_b) union len(tokens_a | tokens_b) return inter / union def combined_similarity(a: str, b: str, char_weight: float 0.6) - float: 综合相似度 字符相似度 * 权重 分词 Jaccard * 权重 return char_weight * char_similarity(a, b) (1 - char_weight) * token_jaccard(a, b)关于权重经验上字符相似度占比高一点会更稳因为分词本身的错误会拖累 Jaccard 的效果。0.6:0.4 是我实测相对平衡的配置你可以根据自己的数据情况调整。阈值设置方面我推荐分两档相似度大于等于 0.85自动合并。这个区间大多是“XX店”和“XX店(分店)”、“XX有限公司”和“XX有限责任公司”这类简单差异相似度在 0.7 到 0.85 之间不自动合并输出到人工审核表。这个区间可能存在真实差异也可能只是写法差异需要人眼判断相似度低于 0.7默认不合并阈值不能拍脑袋定要用业务逻辑校准。我通常的做法是抽 200 条人工标注好的配对数据跑一遍算法调整阈值使 F1 分数最高。这里的核心是不要为了追求召回率而放低阈值否则误杀合并会把两家同名的真店变成一个这种错误比漏并还难修。3.5 批量消歧的完整流程分块 组内计算如果数据量只有几千条上面的相似度计算两两遍历勉强能撑住。但真实爬虫数据动辄几十万条全量两两比较会爆炸。以 10 万条数据为例两两组合是 50 亿对根本跑不完。必须做分块Blocking先通过简单的分组条件让只有可能相似的记录进入同一组再在组内做相似度计算。我常用的分块键是标准化之后的核心名称 行政区划代码。核心名称可以去掉组织形式词、去掉常见后缀后的主体部分。比如“老张牛肉面北京店”和“老张牛肉面(人民路店)”的核心名称都是“老张牛肉面”属于同一块。行政区划代码则能从地域维度排除不相关记录防止“北京老张牛肉面”和“上海老张牛肉面”被拉进同一组。def build_block_keys(standard_name: str, district_code: str ) - list[str]: 生成分块键返回多个候选键 cleaned clean_text(standard_name) core, _ normalize_company_type(cleaned) keys set() if district_code: keys.add(f{district_code}::{core}) # 即使没有行政区划也要留一个兜底键避免漏掉缺失地址的数据 keys.add(fNA::{core}) return list(keys)生成分块键后用字典把相同键的记录聚到一起组内再两两计算相似度。这一步能将计算量从 50 亿对降到几十万对跑批时间从小时级降到分钟级。合并时还有一个细节需要关注传递性。A 和 B 相似、B 和 C 相似A 和 C 不一定相似。比如“老张牛肉面”、“老张牛肉面东门店”、“老张牛腩面”三条记录前两条相似度很高后两条中等相似但第一条和第三条可能低于阈值。如果直接用两两结果合并会形成连锁合并可能导致三家真店被并成一家。更稳妥的做法是先记录所有相似对再用连通图组件connected component来做最终合并并且对组件内部的相似度范围做人工校验。4. 常见问题与排查技巧实录4.1 频繁出现的“误杀与漏并”平衡难题做消歧最痛苦的就是误杀和漏并不可兼得。漏并最多是数据不够全误杀则是直接把两个实体合并成一个后面想拆都拆不干净。我在实际操作中遇到过最典型的一个误杀案例“XX网吧”和“XX网咖”。字符串相似度和分词相似度都很高但两家店实际隔了一条街没有任何从属关系。原因就是行业词“网吧”和“网咖”没有进入行业词字典被当成了同类。解决这类问题我给每条合并记录增加了“来源依据”字段记录是因为哪条规则、哪个相似度分值合并的。遇到可疑数据可以追溯。同时我会定期随机抽样 50 条合并结果做人工复核把误杀率控制在一个可接受的范围。不要相信所有阈值判断都能完美定期抽检才是保证质量的办法。4.2 简称、缩写和跨语言名称带来的挑战中文实体消歧里最难的其实是简称。比如“中国石油天然气股份有限公司”在新闻和第三方平台里通常被写成“中石油”。这种简称与全称之间的相似度非常低单纯靠相似度算法很难识别。要么在品牌别名字典里人工维护简称映射要么结合工商注册号、统一社会信用代码等唯一标识来关联。跨语言名称的问题也不少见。外企在国内注册公司时通常叫“XX中国有限公司”但口碑里大家只叫英文缩写比如“IBM”、“GM”。这类名称如果不用别名映射几乎没有自动消歧的可能。我的建议是在建规则时提前开拓一个“别名配置表”让运营人员可以随时补充而不是每次改代码。这个表通常包含标准名、别名、类型、生效时间。整个消歧流程在跑批前加载这份表就会容易扩展很多。4.3 爬虫数据更新的增量消歧问题爬虫是周期性跑的可能每天、每周都会拉一批新数据。如果每次跑完都做全量消歧算力浪费不说还可能因为规则调整导致历史数据结果漂移让下游报表不一致。我采用的思路是增量合并每次新数据进来先与已有的“实体主表”做匹配能匹配上就并入既有实体匹配不上就新建实体。这样做的好处是历史实体 ID 保持稳定下游关联关系不会动不动就变。增量匹配也有坑就是新数据里的写法是以前没见过的需要定期把“未匹配记录”单独拎出来做分析反向补充规则。否则增量模式下漏并率会逐渐累积。4.4 合并策略与人工复核工具的结合纯自动合并无法保证 100% 正确。一套稳健的实体消歧系统应该在自动合并后保留待审核队列。我会把相似度处于 0.7-0.85 这个“灰色地带”的记录连同两张表的对比信息一起输出成 CSV交给业务人员进行批量审核。审核通过后再回写到主数据表。为了减少人工审核量我还会在 UI 上做一些辅助展示比如并排展示两个名称的标准化版本、分词结果、行政区划、行业标签以及相似度详细得分。审核人员扫一眼就能判断是否同一实体比直接看原始字符串快得多。这个过程虽然费功夫但能显著提升最终数据质量。4.5 批量消歧性能优化的两个小技巧数据量上来以后性能还是需要警惕的。我实测过几个优化手段效果明显值得分享。第一个是提前用“核心名称长度”做过滤。两个实体如果核心名称长度差异超过 50%基本不可能相似可以在分块键生成时就排除掉。比如“老张牛肉面”和“北京老张牛肉面旗舰店”长度差异大但不一定不相关所以这个过滤只能作为初步筛选不能一刀切。第二个是使用多进程并行计算。组内相似度计算是天然彼此独立的用 Python 的concurrent.futures.ProcessPoolExecutor可以轻松把 16 核 CPU 用满实测 50 万条数据从 40 分钟缩短到 8 分钟。from concurrent.futures import ProcessPoolExecutor, as_completed def process_group(group_key, pairs): results [] for pair in pairs: score combined_similarity(pair[0], pair[1]) if score 0.85: results.append((pair, score)) return group_key, results def parallel_similarity(all_groups, max_workers8): all_results {} with ProcessPoolExecutor(max_workersmax_workers) as executor: futures [executor.submit(process_group, key, pairs) for key, pairs in all_groups.items()] for future in as_completed(futures): key, results future.result() all_results[key] results return all_results注意多进程模式下子进程要能正常导入所有已定义的函数和变量所以这段代码最好放在脚本主模块里不要放在交互式环境里直接跑。如果数据量更大可以考虑改用 Spark 或 Dask但中小体量用多进程已经足够。4.6 常见问题速查表把实战里遇到的高频问题整理成一张表方便排查时快速对照问题现象可能原因解决方案同一实体未被合并名称中简称与全称差异过大相似度低于阈值维护简称别名映射表结合统一社会信用代码关联不同实体被误合并行业词、店名后缀未被归一化如“网吧”和“网咖”扩充行业词、商圈词字典落入人工审核队列全角/繁体字符导致匹配失败未做全角转半角和繁体转简体在预处理阶段统一调用normalize(NFKC, s)与 opencc带括号后缀的分店名匹配不上括号内容影响相似度计算标准化时移除括号对核心名与完整名分别计算处理速度过慢全量两两比较O(n²) 复杂度引入分块键与多进程并行计算先分组再组内比较增量数据导致历史结果漂移每次全量重跑实体 ID 不稳定改为增量合并模式保留已有实体主表这个表不是一次性写完的是我在做项目过程中逐步积累下来的。每踩一个坑就把问题和解决方案记进去后续遇到类似问题可以直接查。建议你也按这个思路维护自己的排查手册效果比百度搜索好得多。5. 规则合并的演进方向从规则到模型的平滑过渡5.1 为什么规则合并不是万能的规则合并的优势是透明可控适合数据量不大、名称格式相对规律的场景。但它的天花板也很明显规则需要人工维护每新增一种写法就要补充规则规则之间可能存在冲突面对长尾数据时规则覆盖不足准确率和召回率难以兼得。以连锁品牌为例。很多品牌在不同城市、不同渠道的名字可能差异非常大比如“海底捞火锅”和“Hi 海底捞”。如果规则字典里没有维护自动消歧大概率失败。靠堆规则解决这个问题终究不是长久之计。5.2 规则 模型 人工审核的组合思路我现在更倾向的架构是三层协同规则负责把容易判定的情况快速处理掉模型负责处理模糊样本人工审核负责兜底。这套组合在工程上并不复杂却能显著提升整体效果。大致做法是首先用规则把标准化名称完全一致的记录自动合并这部分通常能处理 70% 以上的样本。剩下的样本生成相似度特征向量输入一个简单的二分类模型比如 XGBoost判断是否同一实体。模型直接输出置信度置信度低于阈值的再进入人工审核队列。这样做的好处是模型能学习到“名称相似但地域不同则大概率是不同实体”这类隐含模式比纯规则灵活得多。而人工审核结果可以回流成训练数据让模型越跑越准。如果你需要跑的数据量很大强烈建议尽早切入这条路线。5.3 什么时候开始引入机器学习很多朋友一听机器学习就发怵其实判断标准很简单如果你的规则需要花大量时间维护或者灰色地带的样本量很大就是时候考虑用模型了。起步阶段不需要复杂模型用 TF-IDF 把名称转成向量接一个逻辑回归就能比纯规则提升不少。我自己是在数据量超过 100 万条、规则维护成本超过每周半天时开始转的。转的过程不复杂关键是先把训练数据准备好也就是那些被人工审核过的消歧结果。没有这些带标签的数据再好的模型也白搭。所以早期就用规则 人工审核模式跑起来其实是在为后面的模型积累弹药。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价