㊗️本期内容已收录至专栏《Python爬虫实战》,持续完善知识体系与项目实战,建议先订阅收藏,后续查阅更方便~
㊙️本期爬虫难度指数:⭐⭐⭐
🉐福利: 一次订阅后,专栏内的所有文章可永久免费看,持续更新中,保底1000+(篇)硬核实战内容。

全文目录:
-
-
- [🌟 开篇语](#🌟 开篇语)
- [1️⃣ 摘要(Abstract)](#1️⃣ 摘要(Abstract))
- [2️⃣ 背景与需求(Why)](#2️⃣ 背景与需求(Why))
- [3️⃣ 合规与注意事项(必写)](#3️⃣ 合规与注意事项(必写))
- [4️⃣ 技术选型与整体流程(What/How)](#4️⃣ 技术选型与整体流程(What/How))
- [5️⃣ 环境准备与依赖安装(可复现)](#5️⃣ 环境准备与依赖安装(可复现))
- [6️⃣ 核心实现:名称归一化层(The Normalizer)](#6️⃣ 核心实现:名称归一化层(The Normalizer))
- [7️⃣ 核心实现:模糊匹配与图构建(Matcher & Graph)](#7️⃣ 核心实现:模糊匹配与图构建(Matcher & Graph))
- [8️⃣ 运行方式与结果展示(必写)](#8️⃣ 运行方式与结果展示(必写))
- [9️⃣ 常见问题与排错(FAQ)](#9️⃣ 常见问题与排错(FAQ))
- [🔟 进阶优化(工业级玩法)](#🔟 进阶优化(工业级玩法))
- [1️⃣1️⃣ 总结与延伸阅读](#1️⃣1️⃣ 总结与延伸阅读)
- [🌟 文末](#🌟 文末)
-
- [✅ 专栏持续更新中|建议收藏 + 订阅](#✅ 专栏持续更新中|建议收藏 + 订阅)
- [✅ 互动征集](#✅ 互动征集)
- [✅ 免责声明](#✅ 免责声明)
-
🌟 开篇语
哈喽,各位小伙伴们你们好呀~我是【喵手】。
运营社区: C站 / 掘金 / 腾讯云 / 阿里云 / 华为云 / 51CTO
欢迎大家常来逛逛,一起学习,一起进步~🌟
我长期专注 Python 爬虫工程化实战 ,主理专栏 《Python爬虫实战》:从采集策略 到反爬对抗 ,从数据清洗 到分布式调度 ,持续输出可复用的方法论与可落地案例。内容主打一个"能跑、能用、能扩展 ",让数据价值真正做到------抓得到、洗得净、用得上。
📌 专栏食用指南(建议收藏)
- ✅ 入门基础:环境搭建 / 请求与解析 / 数据落库
- ✅ 进阶提升:登录鉴权 / 动态渲染 / 反爬对抗
- ✅ 工程实战:异步并发 / 分布式调度 / 监控与容错
- ✅ 项目落地:数据治理 / 可视化分析 / 场景化应用
📣 专栏推广时间 :如果你想系统学爬虫,而不是碎片化东拼西凑,欢迎订阅专栏👉《Python爬虫实战》👈,一次订阅后,专栏内的所有文章可永久免费阅读,持续更新中。
💕订阅后更新会优先推送,按目录学习更高效💯~
1️⃣ 摘要(Abstract)
本文将解决"多源数据合并"中的核心痛点:同一实体在不同来源写法不一致。我们将构建一个三层处理漏斗:规则清洗层 (去除无关噪音)、模糊匹配层 (基于 Levenshtein 距离计算相似度)、以及图聚类层 (利用 NetworkX 的连通分量算法实现传递性合并)。最终产出一份"实体映射表(Mapping Table)",将混乱的原始名称统一映射为标准 ID。
读完本文,你将获得:
- 🧹 清洗黑魔法:利用正则去除"分公司"、"有限公司"、"(北京)"等对实体识别无意义的噪音。
- 📏 相似度度量 :学会使用
thefuzz库计算字符串的 Token Sort Ratio,解决乱序匹配问题。 - 🕸️ 图论应用 :掌握一个高级技巧------利用图的连通分量 (Connected Components)解决 A=B, B=C → \rightarrow → A=B=C 的传递性难题。
2️⃣ 背景与需求(Why)
场景痛点:
你从美团爬了餐饮数据,又从高德爬了 POI。
- 数据 A:
海底捞火锅(三里屯店) - 数据 B:
海底捞 Haidilao Hotpot - 数据 C:
海底捞餐饮股份有限公司
如果不做处理,直接 GroupBy,你会得到 3 条独立记录。我们的目标是将它们识别为同一个 Group。
目标产出:
一张映射表 (Mapping Table):
raw_name: 原始杂乱名称standard_id: 归一化后的唯一 IDstandard_name: 选出的标准名称(通常是最短或出现频率最高的那个)
3️⃣ 合规与注意事项(必写)
在处理企业/店铺数据时:
- 数据准确性风险 :消歧算法是基于概率的,没有 100% 的准确率 。在金融征信或法律合同场景,必须人工复核,不能全信算法。我们的算法适用于商业分析、市场调研等容错率稍高的场景。
- 隐私边界 :我们处理的是公开的商业实体 (店铺/公司),严禁对自然人姓名进行强制消歧(因为世界上重名的人太多了,张伟和张伟可能真的不是同一个人)。
- 商用授权:如果你的数据源来自第三方(如天眼查),请仔细阅读其数据使用协议,不要将清洗后的数据转售。
4️⃣ 技术选型与整体流程(What/How)
为什么不用简单的 ==?
因为 星巴克 != 星巴克咖啡。
为什么不用 ML(机器学习)?
对于中小规模数据(< 百万级),训练 BERT 模型成本太高且不可解释。规则 + 模糊逻辑 + 图算法 是性价比最高、且极其稳健的方案。
整体流程:
Image of pipeline: Raw Data -\> Normalizer (Regex) -\> Similarity Matrix (Fuzz) -\> Graph Building (NetworkX) -\> Connected Components -\> Canonical Output
-
Normalize : 预处理。把
(北京)有限公司这种词删掉,统一全半角。 -
Pairwise Compare: 两两比对。计算相似度分数(0-100)。
-
Graph Clustering: 这是一个高级技巧。
- 如果 A 和 B 相似度 > 90,在 A、B 间连一条边。
- 如果 B 和 C 相似度 > 90,在 B、C 间连一条边。
- 利用图算法,找出所有连在一起的节点,它们就是一个家族(Cluster)。
-
Standardize: 在家族里选一个名字作为族长(标准名)。
5️⃣ 环境准备与依赖安装(可复现)
Python 版本: 3.8+
依赖安装:
我们需要 thefuzz(原 fuzzywuzzy* 3.8+
依赖安装:
我们需要 thefuzz(原 fuzzywuzzy 的加速版)来处理字符串相似度,networkx 来处理图关系。
bash
# python-Levenshtein 是选装的,但能让 thefuzz 快 10 倍,强烈建议安装
pip install pandas thefuzz networkx python-Levenshtein
目录结构:
text
entity_linker/
├── data/
│ └── raw_shops.csv # 脏数据
├── output/
│ └── entity_map.csv # 结果映射表
└── linker.py # 主程序
6️⃣ 核心实现:名称归一化层(The Normalizer)
清洗是地基。如果噪音太多,相似度计算就会失真。
python
import re
import unicodedata
class NameNormalizer:
def __init__(self):
# 定义需要移除的噪音词(后缀、行政区划、通用词)
# 实际项目中,这个列表需要根据业务不断维护
self.noise_words = [
r"有限公司", r"责任公司", r"股份", r"集团", r"分公司", r"总公司",
r"\(.*?\)", r"(.*?)", # 去除括号及内容,如 (北京店)
r"店$", r"旗舰店$", r"专营店$", # 去除店铺后缀
r"餐饮", r"管理", r"发展", r"科技", r"贸易" # 行业通用词
]
# 编译正则:将列表合并为 (A|B|C) 的形式
self.noise_pattern = re.compile("|".join(self.noise_words))
def normalize(self, text):
"""
标准化流程:
1. 转小写
2. 全角转半角 (NFKC)
3. 去除特殊符号
4. 去除噪音词
"""
if not isinstance(text, str):
return ""
# 1. Unicode 归一化 (解决全角空格、特殊字符问题)
text = unicodedata.normalize('NFKC', text)
# 2. 转小写
text = text.lower()
# 3. 移除噪音词 (核心逻辑)
# 注意:这里是替换为空字符串
text = self.noise_pattern.sub('', text)
# 4. 移除所有非中英文字符 (只保留核心名字)
text = re.sub(r'[^\w\u4e00-\u9fa5]', '', text)
return text.strip()
7️⃣ 核心实现:模糊匹配与图构建(Matcher & Graph)
这是系统的核心引擎。我们将利用图论解决传递性问题。
python
from thefuzz import fuzz
import networkx as nx
from itertools import combinations
import pandas as pd
class EntityResolver:
def __init__(self, threshold=80):
self.threshold = threshold
self.normalizer = NameNormalizer()
def build_similarity_graph(self, names_list):
"""
构建相似度图
:param names_list: 原始名称列表
:return: NetworkX Graph 对象
"""
# 1. 预处理所有名称
# 存储结构: { '原始名': '清洗后名' }
clean_map = {name: self.normalizer.normalize(name) for name in set(names_list)}
# 过滤掉清洗后为空的名称(比如只剩"有限公司"这种)
valid_names = [n for n, c in clean_map.items() if len(c) > 1]
G = nx.Graph()
# 将所有有效名称添加为节点
G.add_nodes_from(valid_names)
print(f"🧩 Start pairwise comparison for {len(valid_names)} entities...")
# 2. 两两比对 (Combinations)
# 注意:如果数据量 > 5000,这里需要使用 Blocking 技术优化(见后文)
for name_a, name_b in combinations(valid_names, 2):
clean_a = clean_map[name_a]
clean_b = clean_map[name_b]
# 优化:如果清洗后完全一样,直接 100 分
if clean_a == clean_b:
score = 100
else:
# 使用 token_sort_ratio:
# 它会忽略词序,"星巴克咖啡" 和 "咖啡星巴克" 得分很高
score = fuzz.token_sort_ratio(clean_a, clean_b)
# 如果相似度超过阈值,加一条边
if score >= self.threshold:
G.add_edge(name_a, name_b, weight=score)
# print(f" 🔗 Link: {name_a} <--> {name_b} (Score: {score})")
return G
def resolve_entities(self, G):
"""
解析连通分量,生成映射表
"""
# 核心算法:connected_components
# 只要 A-B 连通,B-C 连通,它们就在同一个 component 里
components = list(nx.connected_components(G))
print(f"✅ Found {len(components)} unique entity clusters.")
results = []
for cluster_id, cluster_nodes in enumerate(components):
# 策略:选择该组中最短的名称作为"标准名"(Standard Name)
# 或者也可以选最长的,视业务需求而定
standard_name = min(cluster_nodes, key=len)
for raw_name in cluster_nodes:
results.append({
'raw_name': raw_name,
'standard_id': f"E{cluster_id:05d}", # 生成 E00001 格式的 ID
'standard_name': standard_name
})
return pd.DataFrame(results)
8️⃣ 运行方式与结果展示(必写)
为了模拟真实世界的脏乱差,我构造了一个非常有代表性的数据集。
python
# --- 入口主逻辑 ---
if __name__ == "__main__":
# 模拟脏数据:包含中文、英文、括号、错别字、后缀等问题
MOCK_DATA = [
"星巴克咖啡",
"星巴克(中国)有限公司",
"STARBUCKS COFFEE", # 英文目前 fuzzywuzzy 处理一般,通常需要翻译或转拼音,这里仅演示
"星巴克Starbucks",
"北京星巴克咖啡有限公司三里屯店",
"腾讯科技",
"腾讯(深圳)有限公司",
"Tencent腾讯",
"海底捞火锅",
"海底捞餐饮",
"海底捞(王府井店)",
"沙县小吃", # 干扰项
"兰州拉面" # 干扰项
]
print("🚀 Entity-Linker Started...\n")
resolver = EntityResolver(threshold=75) # 设定相似度阈值
# 1. 建图
graph = resolver.build_similarity_graph(MOCK_DATA)
# 2. 聚类消歧
df_result = resolver.resolve_entities(graph)
# 3. 展示结果
print("\n📊 Mapping Table (Top 10 rows):")
# 按 ID 排序展示,方便看聚类效果
print(df_result.sort_values('standard_id').to_string(index=False))
# 4. 导出
df_result.to_csv("entity_mapping.csv", index=False, encoding='utf-8-sig')
运行结果预演与解析:
(注意:由于 Starbucks 和 `星巴克 在字面上完全不同,纯字符串匹配无法关联。在实际代码中,除非引入翻译层,否则它们被分成两组。下面的结果展示了代码能处理的中文变体情况)
text
🚀 Entity-Linker Started...
🧩 Start pairwise comparison for 13 entities...
✅ Found 5 unique entity clusters.
📊 Mapping Table (Top 10 rows):
raw_name standard_id standard_name
星巴克咖啡 E00000 星巴克咖啡
星巴克(中国)有限公司 E00000 星巴克咖啡
北京星巴克咖啡有限公司三里屯店 E00000 星巴克咖啡
星巴克Starbucks E00000 星巴克咖啡
腾讯科技 E00001 腾讯科技
腾讯(深圳)有限公司 E00001 腾讯科技
Tencent腾讯 E00001 腾讯科技
海底捞火锅 E00002 海底捞火锅
海底捞餐饮 E00002 海底捞火锅
海底捞(王府井店) E00002 海底捞火锅
沙县小吃 E00003 沙县小吃
兰州拉面 E00004 兰州拉面
分析:
- 星巴克家族 :尽管"北京星巴克..."很长,但经过
NameNormalizer去掉"北京"、"有限公司"、"店"后,核心词剩下"星巴克咖啡",与"星巴克咖啡"完全匹配。 - 腾讯家族:通过部分匹配,成功将这几个归为一类。
- 干扰项:沙县小吃和兰州拉面虽然都是小吃,但相似度低,被正确分成了独立实体。
9️⃣ 常见问题与排错(FAQ)
-
O(N^2) 性能爆炸?
-
问题:两两比对的复杂度是 N 的平方。如果数据有 10 万条,计算量是 100亿次,程序会跑好几天。
-
解法 :必须使用 Blocking (分块) 技术。
- 只比对"首字相同"的企业。
- 或者利用 ElasticSearch 的全文检索,先召回 Top 10 相似候选项,只对这 110 个算 fuzz score。
-
-
误判(False Positives)?
-
现象:"中国移动" 和 "中国联通"。清洗后剩下"移动"和"联通",相似度可能不高。但如果是"山西刀削面"和"山东刀削面",因为字形相近且清洗后只差一字,极易被误判为同一个。
-
解法:提高阈值(如到 90);或者引入 TF-IDF 权重,给"刀削面"这种通用词极低的权重,给"山西/山东"这种特征词高权重。
-
-
英文和中文对齐?
- 问题 :
Apple和苹果。 - 解:这不是字符串匹配能解决的。必须引入 同义词库 或 翻译API*。将所有英文翻译成中文后再比对。
- 问题 :
🔟 进阶优化(工业级玩法)
如果你要构建企业级的中台系统:
-
LSH (Locality Sensitive Hashing):
- 使用
MinashLSH算法。它可以将相似的文本 Hash 到同一个桶里。我们只需要在桶内进行比对,将复杂度从 O ( N 2 ) O(N^2) O(N2) 降到 O ( N ) O(N) O(N)。这是处理百万级数据的唯一出路。
- 使用
-
语义向量 (Semantic Embeddings):
- 使用
Sentence-BERT将公司名转为 768 维向量,然后计算 Cosine Similarity。 - 这能解决
ICBC和工商银行的匹配问题(因为模型在大量语料中见过它们共现)。
- 使用
-
人机结合 (Active Learning):
- 让算法算出相似度在 70-85 分之间的"纠结区",推给人工审核。人工标几个,模型学一点,越来越准。
1️⃣1️⃣ 总结与延伸阅读
复盘:
我们完成了一次数据的**洗礼。
- Cleaner: 剥离了无意义的行政修饰词。
- Matcher: 用模糊逻辑跨越了字面差异。
- Graph: 用图论连接了孤岛。
下一步:
现在你已经有了统一的 standard_id。
- 试试看:把你之前爬的"大众点评评分"和"高德地图位置",通过这个 ID 关联起来!
- 想象一下:你可以在地图上画出"星巴克 vs 瑞幸"的势力范围图,且数据是精准去重过的。
这才是大数据的核心价值------连接 (Linking)。🔗✨
🌟 文末
好啦~以上就是本期的全部内容啦!如果你在实践过程中遇到任何疑问,欢迎在评论区留言交流,我看到都会尽量回复~咱们下期见!
小伙伴们在批阅的过程中,如果觉得文章不错,欢迎点赞、收藏、关注哦~
三连就是对我写作道路上最好的鼓励与支持! ❤️🔥
✅ 专栏持续更新中|建议收藏 + 订阅
墙裂推荐订阅专栏 👉 《Python爬虫实战》,本专栏秉承着以"入门 → 进阶 → 工程化 → 项目落地"的路线持续更新,争取让每一期内容都做到:
✅ 讲得清楚(原理)|✅ 跑得起来(代码)|✅ 用得上(场景)|✅ 扛得住(工程化)
📣 想系统提升的小伙伴 :强烈建议先订阅专栏 《Python爬虫实战》,再按目录大纲顺序学习,效率十倍上升~

✅ 互动征集
想让我把【某站点/某反爬/某验证码/某分布式方案】等写成某期实战?
评论区留言告诉我你的需求,我会优先安排实现(更新)哒~
⭐️ 若喜欢我,就请关注我叭~(更新不迷路)
⭐️ 若对你有用,就请点赞支持一下叭~(给我一点点动力)
⭐️ 若有疑问,就请评论留言告诉我叭~(我会补坑 & 更新迭代)
✅ 免责声明
本文爬虫思路、相关技术和代码仅用于学习参考,对阅读本文后的进行爬虫行为的用户本作者不承担任何法律责任。
使用或者参考本项目即表示您已阅读并同意以下条款:
- 合法使用: 不得将本项目用于任何违法、违规或侵犯他人权益的行为,包括但不限于网络攻击、诈骗、绕过身份验证、未经授权的数据抓取等。
- 风险自负: 任何因使用本项目而产生的法律责任、技术风险或经济损失,由使用者自行承担,项目作者不承担任何形式的责任。
- 禁止滥用: 不得将本项目用于违法牟利、黑产活动或其他不当商业用途。
- 使用或者参考本项目即视为同意上述条款,即 "谁使用,谁负责" 。如不同意,请立即停止使用并删除本项目。!!!
