㊙️本期内容已收录至专栏《Python爬虫实战》,持续完善知识体系与项目实战,建议先订阅收藏,后续查阅更方便~持续更新中!
㊗️爬虫难度指数:⭐⭐⭐
🚫声明:本数据&代码仅供学习交流,严禁用于商业用途、倒卖数据或违反目标站点的服务条款等,一切后果皆由使用者本人承担。公开榜单数据一般允许访问,但请务必遵守"君子协议",技术无罪,责任在人。

全文目录:
-
- [🌟 开篇语](#🌟 开篇语)
- [📝 摘要(Abstract)](#📝 摘要(Abstract))
- [🎯 背景与需求(Why)](#🎯 背景与需求(Why))
- [⚖️ 合规与注意事项(必读)](#⚖️ 合规与注意事项(必读))
- [🛠️ 技术选型与整体流程(What/How)](#🛠️ 技术选型与整体流程(What/How))
- [📦 环境准备与依赖安装](#📦 环境准备与依赖安装)
- [🔧 核心实现:预处理层(Preprocessor)](#🔧 核心实现:预处理层(Preprocessor))
- [🎯 核心实现:规则引擎层(Rule Engine)](#🎯 核心实现:规则引擎层(Rule Engine))
- [🔢 核心实现:相似度计算层(Similarity Scorer)](#🔢 核心实现:相似度计算层(Similarity Scorer))
-
- 多维度相似度计算器
- [1. 编辑距离(Levenshtein Distance)](#1. 编辑距离(Levenshtein Distance))
- [2. Jaccard相似度](#2. Jaccard相似度)
- [3. 拼音相似度](#3. 拼音相似度)
- [4. 语义相似度(BERT)](#4. 语义相似度(BERT))
- [🔗 核心实现:实体匹配器(Entity Matcher)](#🔗 核心实现:实体匹配器(Entity Matcher))
- [💾 核心实现:知识库管理(Knowledge Base)](#💾 核心实现:知识库管理(Knowledge Base))
- [🚀 主程序与运行示例](#🚀 主程序与运行示例)
- [📚 总结与延伸](#📚 总结与延伸)
- [🌟 文末](#🌟 文末)
-
- [📌 专栏持续更新中|建议收藏 + 订阅](#📌 专栏持续更新中|建议收藏 + 订阅)
- [✅ 互动征集](#✅ 互动征集)
🌟 开篇语
哈喽,各位小伙伴们你们好呀~我是【喵手】。
运营社区: C站 / 掘金 / 腾讯云 / 阿里云 / 华为云 / 51CTO
欢迎大家常来逛逛,一起学习,一起进步~🌟
我长期专注 Python 爬虫工程化实战 ,主理专栏 《Python爬虫实战》:从采集策略 到反爬对抗 ,从数据清洗 到分布式调度 ,持续输出可复用的方法论与可落地案例。内容主打一个"能跑、能用、能扩展 ",让数据价值真正做到------抓得到、洗得净、用得上。
📌 专栏食用指南(建议收藏)
- ✅ 入门基础:环境搭建 / 请求与解析 / 数据落库
- ✅ 进阶提升:登录鉴权 / 动态渲染 / 反爬对抗
- ✅ 工程实战:异步并发 / 分布式调度 / 监控与容错
- ✅ 项目落地:数据治理 / 可视化分析 / 场景化应用
📣 专栏推广时间 :如果你想系统学爬虫,而不是碎片化东拼西凑,欢迎订阅/关注专栏👉《Python爬虫实战》👈
💕订阅后更新会优先推送,按目录学习更高效💯~
📝 摘要(Abstract)
本文将完整演示如何处理电商平台中的同名实体消歧问题,以店铺名/公司名为例,通过规则引擎、相似度算法、机器学习等技术手段,构建一套生产级的实体标准化系统。最终输出包括标准化映射表、实体知识库、以及可用于实时消歧的决策引擎。
读完本文你将获得:
- 掌握实体消歧的完整技术栈(规则+算法+模型)
- 学会构建生产级实体标准化Pipeline(预处理→规则匹配→相似度计算→人工审核)
- 获得可直接运行的完整项目代码(包含增量更新、冲突检测、审核系统)
🎯 背景与需求(Why)
为什么需要实体消歧?
在电商、企业信息、地图POI等场景中,同名实体消歧是数据清洗的核心难题。一个看似简单的店铺名,背后可能隐藏着多重复杂性:
典型问题场景:
-
同一实体的多种表述
- "Apple官方旗舰店" = "Apple旗舰店" = "苹果官方店" = "Apple Store官方"
- 这些都指向同一个店铺,但文本完全不同
-
不同实体的相似名称
- "小米之家(北京国贸店)" ≠ "小米之家(上海南京路店)"
- 虽然都叫"小米之家",但是不同的物理店铺
-
官方与授权的区分
- "华为官方旗舰店" ≠ "华为授权专卖店"
- 需要识别官方认证与第三方授权
-
缩写与全称的对应
- "阿里巴巴集团控股有限公司" = "阿里巴巴" = "Alibaba Group"
- 同一公司的不同书写形式
-
特殊字符与空格的差异
- "HUAWEI Mate50" = "Huawei Mate 50" = "华为Mate50"
- 大小写、空格、中英文混杂
实际业务影响
如果不解决实体消歧问题,会导致:
- ❌ 数据统计错误:同一店铺被计算成多个店铺,销售额统计失真
- ❌ 推荐系统失效:无法识别用户历史购买的店铺,推荐不准确
- ❌ 知识图谱断裂:实体关系混乱,无法构建准确的商业关系网络
- ❌ 搜索召回不全:用户搜"苹果官方"找不到"Apple官方店"的商品
- ❌ 风控判断失误:同一商家用不同名称开店逃避监管
目标站点与字段清单
本文以电商平台店铺数据为例,需要处理的字段包括:
| 字段名 | 数据类型 | 说明 | 示例值 |
|---|---|---|---|
shop_id |
String | 店铺唯一ID | "shop_12345678" |
shop_name |
String | 店铺名称(原始) | "Apple官方旗舰店" |
shop_name_std |
String | 标准化名称(目标) | "Apple官方旗舰店" |
platform |
String | 所属平台 | "taobao/jd/pdd" |
shop_type |
String | 店铺类型 | "official/authorized/personal" |
address |
String | 店铺地址 | "北京市朝阳区..." |
entity_id |
String | 实体ID(消歧后) | "entity_apple_official" |
aliases |
List[String] | 所有别名 | ["Apple官方店", "苹果旗舰店"] |
confidence |
Float | 消歧置信度 | 0.95 |
verified_at |
Datetime | 人工审核时间 | "2024-01-15 14:32:00" |
⚖️ 合规与注意事项(必读)
数据来源合规性
本文涉及的数据处理需遵守以下原则:
✅ 仅处理公开数据 :店铺名称、地址等公开信息
✅ 不涉及用户隐私 :不采集买家信息、交易记录
✅ 商业用途需授权 :企业使用需获得平台数据使用许可
✅ 遵守反不正当竞争法:不用于恶意竞争分析
算法公平性
实体消歧算法可能影响商家权益,需要:
- 🔒 透明化规则:向商家公开消歧逻辑
- 🔄 人工复核机制:高风险决策需人工确认
- 📊 错误率监控:定期评估误判率,持续优化
- 🛡️ 申诉通道:商家可申诉错误的实体合并
🛠️ 技术选型与整体流程(What/How)
技术选型决策
实体消歧是典型的混合任务,需要规则、算法、模型的协同工作:
| 技术类型 | 适用场景 | 准确率 | 覆盖率 | 开发成本 |
|---|---|---|---|---|
| 规则引擎 | 确定性模式(如"官方"标识) | ⭐⭐⭐⭐⭐ | ⭐⭐ | ⭐ |
| 编辑距离 | 拼写错误、轻微差异 | ⭐⭐⭐ | ⭐⭐⭐ | ⭐ |
| 拼音匹配 | 中文同音字混淆 | ⭐⭐⭐ | ⭐⭐ | ⭐⭐ |
| 字形匹配 | 形近字混淆 | ⭐⭐ | ⭐ | ⭐⭐⭐ |
| 语义向量 | 语义相似但文本差异大 | ⭐⭐⭐⭐ | ⭐⭐⭐⭐ | ⭐⭐⭐⭐ |
本文采用的技术栈:
- 规则层:正则表达式、关键词匹配(Python re)
- 字符串相似度:编辑距离(Levenshtein)、Jaccard相似度
- 拼音处理:pypinyin(处理中文同音字)
- 语义相似度:sentence-transformers(BERT模型)
- 实体链接:自建知识库 + 模糊匹配索引
整体流程架构
json
┌─────────────────────────────────────────────────────────────────┐
│ 原始数据输入 │
│ (店铺列表、交易数据、用户搜索词...) │
└───────────────────────┬─────────────────────────────────────────┘
│
v
┌─────────────────────────────────────────────────────────────────┐
│ 步骤1: 数据预处理 (Preprocessor) │
│ ├─ 清洗无效字符(特殊符号、emoji) │
│ ├─ 统一大小写、空格归一化 │
│ ├─ 提取结构化信息(店铺类型、地域) │
│ └─ 生成多种变体(全称、缩写、拼音、首字母) │
└───────────────────────┬─────────────────────────────────────────┘
│
v
┌─────────────────────────────────────────────────────────────────┐
│ 步骤2: 精确匹配 (Exact Matcher) │
│ ├─ 完全相同(包含大小写、空格标准化后) │
│ ├─ ID直接对应(如已有shop_id映射) │
│ └─ 快速通道:90%+ 数据在此解决 │
└───────────────────────┬─────────────────────────────────────────┘
│
v
┌─────────────────────────────────────────────────────────────────┐
│ 步骤3: 规则引擎 (Rule Engine) │
│ ├─ 官方标识识别("官方"、"旗舰店") │
│ ├─ 地域信息提取("北京店"、"上海分店") │
│ ├─ 品牌归属判断("Apple" vs "苹果") │
│ └─ 黑白名单过滤(已知相同/不同的实体对) │
└───────────────────────┬─────────────────────────────────────────┘
│
v
┌─────────────────────────────────────────────────────────────────┐
│ 步骤4: 相似度计算 (Similarity Scorer) │
│ ├─ 编辑距离(Levenshtein Distance) │
│ ├─ Jaccard相似度(字符集重合度) │
│ ├─ 拼音相似度(pypinyin) │
│ ├─ 语义向量相似度(BERT sentence-transformers) │
│ └─ 加权综合得分(根据业务场景调整权重) │
└───────────────────────┬─────────────────────────────────────────┘
│
v
┌─────────────────────────────────────────────────────────────────┐
│ 步骤5: 候选实体生成 (Candidate Generator) │
│ ├─ 根据相似度阈值筛选候选(如 > 0.7) │
│ ├─ 排序:置信度高的排在前面 │
│ ├─ 冲突检测:一个新实体匹配到多个已知实体 │
│ └─ 输出待审核列表 │
└───────────────────────┬─────────────────────────────────────────┘
│
v
┌─────────────────────────────────────────────────────────────────┐
│ 步骤6: 人工审核 (Human Review) │
│ ├─ 高置信度(>0.9):自动通过 │
│ ├─ 中置信度(0.7-0.9):人工复核 │
│ ├─ 低置信度(<0.7):创建新实体 │
│ └─ 冲突解决:由领域专家决策 │
└───────────────────────┬─────────────────────────────────────────┘
│
v
┌─────────────────────────────────────────────────────────────────┐
│ 步骤7: 知识库更新 (Knowledge Base) │
│ ├─ 更新实体-别名映射表 │
│ ├─ 记录消歧决策日志(可追溯) │
│ ├─ 构建倒排索引(快速查询) │
│ └─ 生成标准化映射文件(用于下游系统) │
└─────────────────────────────────────────────────────────────────┘
为什么这样设计?
- 分层过滤:先用低成本的精确匹配和规则处理90%数据,再用计算密集的相似度算法处理剩余10%
- 人机协同:机器处理简单case,人工处理边界case,既保证效率又保证准确性
- 可追溯性:每个决策都有日志,方便后续审计和优化
- 增量更新:新数据只需与已有知识库比对,不需要重新计算所有数据
📦 环境准备与依赖安装
Python版本要求
bash
Python >= 3.8 # 需要支持 f-string 和 typing
依赖安装
bash
# 创建虚拟环境(推荐)
python -m venv venv
source venv/bin/activate # Windows: venv\Scripts\activate
# 核心依赖
pip install pandas==2.1.4 # 数据处理
pip install numpy==1.24.3 # 数值计算
pip install tqdm==4.66.1 # 进度条
pip install loguru==0.7.2 # 日志管理
# 字符串处理依赖
pip install python-Levenshtein==0.23.0 # 编辑距离
pip install pypinyin==0.50.0 # 中文拼音
pip install jieba==0.42.1 # 中文分词
# 语义相似度(可选,需要较大内存)
pip install sentence-transformers==2.2.2 # BERT语义向量
pip install torch==2.1.0 # PyTorch(sentence-transformers依赖)
# 数据库支持
pip install sqlalchemy==2.0.25 # ORM
pip install sqlite3 # 轻量级数据库(Python内置)
# 可视化与监控
pip install matplotlib==3.8.2 # 图表绘制
pip install seaborn==0.13.0 # 统计可视化
项目结构(推荐目录)
json
entity_disambiguation/
│
├── config/
│ ├── __init__.py
│ ├── settings.py # 配置文件(阈值、权重)
│ └── rules.json # 规则库(JSON格式)
│
├── core/
│ ├── __init__.py
│ ├── preprocessor.py # 预处理器
│ ├── rule_engine.py # 规则引擎
│ ├── similarity.py # 相似度计算
│ ├── matcher.py # 实体匹配器
│ └── knowledge_base.py # 知识库管理
│
├── models/
│ ├── __init__.py
│ ├── entity.py # 实体数据模型
│ └── schema.py # 数据库Schema
│
├── utils/
│ ├── __init__.py
│ ├── logger.py # 日志配置
│ ├── metrics.py # 评估指标
│ └── visualization.py # 可视化工具
│
├── data/
│ ├── raw/ # 原始数据
│ ├── processed/ # 处理后数据
│ ├── knowledge_base/ # 知识库文件
│ └── review/ # 待审核数据
│
├── tests/
│ ├── test_preprocessor.py
│ ├── test_similarity.py
│ └── test_integration.py
│
├── main.py # 主入口
├── batch_process.py # 批量处理脚本
├── incremental_update.py # 增量更新脚本
├── review_interface.py # 人工审核界面
├── requirements.txt # 依赖清单
└── README.md # 项目说明
🔧 核心实现:预处理层(Preprocessor)
文本标准化器
python
# core/preprocessor.py
import re
from typing import Dict, List, Set, Optional
from loguru import logger
import jieba
from pypinyin import lazy_pinyin, Style
class TextNormalizer:
"""
文本标准化器
功能:
- 清洗无效字符
- 统一大小写、空格
- 提取结构化信息
- 生成多种变体
"""
def __init__(self):
# 编译常用正则表达式(性能优化)
# 1. 特殊字符清理
self.special_char_pattern = re.compile(
r'[^\w\s\u4e00-\u9fff()()\[\]【】\-_&]'
)
# 2. 多空格归一化
self.whitespace_pattern = re.compile(r'\s+')
# 3. 店铺类型标识
self.shop_type_patterns = {
'official': re.compile(r'(官方|official|flagship|旗舰)'),
'authorized': re.compile(r'(授权|authorized|专卖|专营)'),
'personal': re.compile(r'(个人|店主|小店)')
}
# 4. 地域信息提取
self.location_pattern = re.compile(
r'(北京|上海|广州|深圳|杭州|成都|武汉|西安|'
r'[京沪粤浙苏鲁川]|'
r'\(.*?[市区县店]\)|'
r'【.*?[市区县店]】)'
)
# 5. 品牌关键词(示例)
self.brand_keywords = {
'apple': ['apple', '苹果', 'iphone', 'ipad', 'mac'],
'huawei': ['huawei', '华为', 'mate', 'p系列', 'nova'],
'xiaomi': ['xiaomi', '小米', 'mi', 'redmi', '红米'],
'samsung': ['samsung', '三星', 'galaxy'],
'oppo': ['oppo', 'reno', 'find'],
'vivo': ['vivo', 'iqoo', 'x系列']
}
# 6. 常见缩写映射
self.abbreviation_map = {
'ltd': '有限公司',
'co': '公司',
'corp': '集团',
'inc': '股份有限公司',
'有限': '有限公司',
'旗舰': '旗舰店'
}
def normalize(self, text: str) -> str:
"""
基础标准化流程
:param text: 原始文本
:return: 标准化后的文本
"""
if not text or not isinstance(text, str):
return ""
# 1. 转小写(保留中文)
text = text.lower()
# 2. 去除特殊字符(保留中文、英文、数字、常用符号)
text = self.special_char_pattern.sub('', text)
# 3. 空格归一化
text = self.whitespace_pattern.sub(' ', text).strip()
# 4. 移除首尾常见的无意义词
text = text.strip('店铺商城专卖店旗舰店官方店 ')
return text
def extract_shop_info(self, text: str) -> Dict[str, any]:
"""
提取店铺结构化信息
返回示例:
{
'shop_type': 'official', # 官方/授权/个人
'location': '北京', # 地域
'brand': 'apple', # 品牌
'normalized_name': 'apple official store', # 标准化名称
'core_name': 'apple store' # 核心名称(去除修饰词)
}
"""
original_text = text
normalized_text = self.normalize(text)
info = {
'original_name': original_text,
'normalized_name': normalized_text,
'shop_type': 'unknown',
'location': None,
'brand': None,
'core_name': normalized_text
}
# 1. 识别店铺类型
for shop_type, pattern in self.shop_type_patterns.items():
if pattern.search(normalized_text):
info['shop_type'] = shop_type
break
# 2. 提取地域信息
location_match = self.location_pattern.search(normalized_text)
if location_match:
info['location'] = location_match.group(0).strip('()()【】')
# 3. 识别品牌
normalized_lower = normalized_text.lower()
for brand, keywords in self.brand_keywords.items():
if any(kw in normalized_lower for kw in keywords):
info['brand'] = brand
break
# 4. 生成核心名称(去除地域和类型修饰词)
core_name = normalized_text
# 移除地域信息
if info['location']:
core_name = core_name.replace(info['location'], '').strip()
# 移除类型标识
type_keywords = ['官方', 'official', '旗舰', 'flagship',
'授权', 'authorized', '专卖', '专营']
for kw in type_keywords:
core_name = core_name.replace(kw, '').strip()
# 清理剩余空格
core_name = self.whitespace_pattern.sub(' ', core_name).strip()
info['core_name'] = core_name
return info
def generate_variants(self, text: str) -> Dict[str, str]:
"""
生成文本的多种变体
用于模糊匹配:同一实体可能有多种书写方式
返回:
{
'original': '原始文本',
'normalized': '标准化文本',
'no_space': '无空格版本',
'pinyin': '拼音',
'pinyin_abbr': '拼音首字母',
'no_special': '去除所有特殊字符',
'lower': '全小写',
'tokens': '分词后的词列表'
}
"""
variants = {
'original': text,
'normalized': self.normalize(text),
}
# 无空格版本
variants['no_space'] = variants['normalized'].replace(' ', '')
# 拼音变体(仅中文)
# lazy_pinyin: ["Apple", "官", "方", "旗", "舰", "店"]
pinyin_list = lazy_pinyin(text, style=Style.NORMAL)
variants['pinyin'] = ''.join(pinyin_list)
# 拼音首字母
# lazy_pinyin with FIRST_LETTER: ["A", "g", "f", "q", "j", "d"]
pinyin_abbr = lazy_pinyin(text, style=Style.FIRST_LETTER)
variants['pinyin_abbr'] = ''.join(pinyin_abbr)
# 去除所有非字母数字字符
variants['no_special'] = re.sub(r'[^a-z0-9\u4e00-\u9fff]', '',
variants['normalized'])
# 全小写(已在normalize中处理,但显式保留)
variants['lower'] = variants['normalized'].lower()
# 分词(用于后续的集合相似度计算)
# jieba.cut: ['Apple', '官方', '旗舰店']
variants['tokens'] = list(jieba.cut(variants['normalized']))
return variants
def expand_abbreviations(self, text: str) -> str:
"""
展开常见缩写
例如:
"Apple Ltd." -> "Apple 有限公司"
"华为旗舰" -> "华为旗舰店"
"""
expanded = text
for abbr, full_form in self.abbreviation_map.items():
# 使用正则确保只匹配完整的词
pattern = r'\b' + re.escape(abbr) + r'\b'
expanded = re.sub(pattern, full_form, expanded, flags=re.IGNORECASE)
return expanded
def is_valid_shop_name(self, text: str) -> bool:
"""
校验店铺名是否有效
过滤规则:
- 长度过短(<3)或过长(>100)
- 纯数字
- 包含敏感词
- 明显的测试数据
"""
if not text or len(text) < 3 or len(text) > 100:
return False
# 纯数字
if text.strip().isdigit():
return False
# 测试数据标识
test_keywords = ['test', '测试', 'demo', 'temp', '临时']
if any(kw in text.lower() for kw in test_keywords):
return False
return True
def batch_normalize(self, texts: List[str]) -> List[Dict]:
"""
批量标准化
:param texts: 文本列表
:return: 标准化结果列表,每项包含原始文本和处理结果
"""
results = []
for text in texts:
if not self.is_valid_shop_name(text):
logger.warning(f"⚠️ 无效店铺名,已跳过: {text}")
continue
result = {
'original': text,
'shop_info': self.extract_shop_info(text),
'variants': self.generate_variants(text)
}
results.append(result)
logger.info(f"✅ 批量标准化完成 | 输入: {len(texts)} -> 有效: {len(results)}")
return results
# 使用示例
if __name__ == "__main__":
normalizer = TextNormalizer()
# 测试案例
test_names = [
"Apple官方旗舰店",
"apple OFFICIAL Store",
"苹果旗舰店(北京国贸)",
"华为授权专卖店-上海",
"小米之家 杭州西湖店",
"SAMSUNG官方店",
" OPPO 旗舰 店 ",
"test123" # 应该被过滤
]
for name in test_names:
print(f"\n原始: {name}")
# 基础标准化
normalized = normalizer.normalize(name)
print(f"标准化: {normalized}")
# 提取信息
info = normalizer.extract_shop_info(name)
print(f"店铺类型: {info['shop_type']}")
print(f"地域: {info['location']}")
print(f"品牌: {info['brand']}")
print(f"核心名称: {info['core_name']}")
# 生成变体
variants = normalizer.generate_variants(name)
print(f"拼音: {variants['pinyin']}")
print(f"拼音首字母: {variants['pinyin_abbr']}")
print(f"分词: {variants['tokens']}")
使用说明与注意事项
代码详细解析:
-
normalize()方法:- 作用:基础文本清洗,统一格式
- 处理步骤:小写转换 → 去特殊字符 → 空格归一化 → 去首尾无意义词
- 关键点 :使用正则表达式预编译(
re.compile)提升性能,避免每次调用都重新编译
-
extract_shop_info()方法:-
作用:从店铺名中提取结构化信息
-
提取内容:店铺类型(官方/授权)、地域、品牌、核心名称
-
应用场景:
- 相同品牌不同地域的店铺需要区分(小米之家北京店 ≠ 小米之家上海店)
- 官方与授权店需要区分(Apple官方 ≠ Apple授权)
-
技术细节:
python# 使用命名捕获组提取信息 self.location_pattern = re.compile(r'(北京|上海|...)') match = pattern.search(text) if match: location = match.group(0) # 获取匹配的文本
-
-
generate_variants()方法:-
作用:生成多种文本变体用于模糊匹配
-
为什么需要变体?
- 用户输入可能有拼写错误、空格差异
- 同一实体在不同平台可能有不同写法
- 拼音匹配可以处理同音字问题("苹果" vs "平果")
-
变体类型:
python{ 'original': 'Apple官方旗舰店', 'normalized': 'apple官方旗舰店', 'no_space': 'apple官方旗舰店', 'pinyin': 'appleguanfangqijiandian', 'pinyin_abbr': 'agfqjd', 'no_special': 'apple官方旗舰店', 'tokens': ['apple', '官方', '旗舰店'] } -
应用场景:
pinyin:处理"Apple官方"能匹配"苹果官方"pinyin_abbr:快速初筛("agfqjd" vs "pgqjd")tokens:计算集合相似度(Jaccard)
-
-
is_valid_shop_name()方法:-
作用:过滤无效数据,避免污染知识库
-
过滤规则:
- 长度检查(太短可能是缩写,太长可能是描述)
- 纯数字过滤("12345"不是有效店铺名)
- 测试数据识别(包含"test"、"测试"等)
-
为什么重要?
- 垃圾数据会导致误匹配
- 测试店铺名会影响生产数据质量
- 提前过滤可以减少后续计算量
-
性能优化建议:
python
# 1. 正则表达式预编译(已实现)
# 不要在循环中重复编译正则
# ❌ 错误做法
for text in texts:
re.sub(r'\s+', ' ', text) # 每次都编译
# ✅ 正确做法
pattern = re.compile(r'\s+')
for text in texts:
pattern.sub(' ', text) # 复用编译后的对象
# 2. 批量处理
# 对于大数据量,使用 multiprocessing 并行处理
from multiprocessing import Pool
def process_chunk(texts):
normalizer = TextNormalizer()
return normalizer.batch_normalize(texts)
# 将数据分块并行处理
with Pool(processes=4) as pool:
chunks = [texts[i:i+1000] for i in range(0, len(texts), 1000)]
results = pool.map(process_chunk, chunks)
# 3. 缓存机制
# 相同文本不需要重复处理
from functools import lru_cache
class CachedNormalizer(TextNormalizer):
@lru_cache(maxsize=10000)
def normalize(self, text: str) -> str:
return super().normalize(text)
🎯 核心实现:规则引擎层(Rule Engine)
python
# core/rule_engine.py
import re
import json
from typing import Dict, List, Tuple, Optional
from loguru import logger
from pathlib import Path
class EntityRuleEngine:
"""
实体消歧规则引擎
功能:
- 基于规则判断两个实体是否相同
- 管理黑白名单(确定相同/不同的实体对)
- 提供规则优先级管理
"""
def __init__(self, rules_config_path: Optional[str] = None):
"""
初始化规则引擎
:param rules_config_path: 规则配置文件路径(JSON格式)
"""
self.rules_config_path = rules_config_path or "config/rules.json"
# 加载规则配置
self.rules = self._load_rules()
# 黑白名单
self.whitelist: Dict[Tuple[str, str], float] = {} # {(entity1, entity2): confidence}
self.blacklist: Set[Tuple[str, str]] = set() # {(entity1, entity2)}
# 规则统计
self.rule_stats = {
'total_matches': 0,
'rule_hits': {}, # 每条规则的命中次数
'whitelist_hits': 0,
'blacklist_hits': 0
}
def _load_rules(self) -> Dict:
"""
加载规则配置
规则格式示例:
{
"same_entity_rules": [
{
"name": "official_store_rule",
"description
"conditions": {
"brand_must_match": true,
"shop_type_must_match": true,
"location_must_differ": false
},
"confidence": 0.95
}
],
"different_entity_rules": [...]entity_rules": [...]
}
"""
if not Path(self.rules_config_path).exists():
logger.warning(f"⚠️ 规则文件不存在: {self.rules_config_path},使用默认规则")
return self._get_default_rules()
try:
with open(self.rules_config_path, 'r', encoding='utf-8') as f:
rules = json.load(f)
logger.info(f"✅ 规则加载成功: {self.rules_config_path}")
return rules
except Exception as e:
logger.error(f"❌ 规则加载失败: {str(e)}")
return self._get_default_rules()
def _get_default_rules(self) -> Dict:
"""
默认规则集
当规则文件不存在时使用
"""
return {
"same_entity_rules": [
{
"name": "exact_core_name_match",
"description": "核心名称完全匹配",
"priority": 100,
"confidence": 1.0,
"conditions": {
"core_name_exact_match": True
}
},
{
"name": "official_store_same_brand",
"description": "同品牌官方店",
"priority": 90,
"confidence": 0.95,
"conditions": {
"brand_must_match": True,
"shop_type": "official",
"location_can_differ": True
}
},
{
"name": "same_brand_authorized_same_location",
"description": "同品牌同地域授权店",
"priority": 80,
"confidence": 0.85,
"conditions": {
"brand_must_match": True,
"shop_type": "authorized",
"location_must_match": True
}
}
],
"different_entity_rules": [
{
"name": "different_brand",
"description": "不同品牌必定不同实体",
"priority": 100,
"confidence": 1.0,
"conditions": {
"brand_must_differ": True
}
},
{
"name": "official_vs_authorized",
"description": "官方店与授权店不同",
"priority": 90,
"confidence": 0.9,
"conditions": {
"shop_type_must_differ": True,
"one_is_official": True
}
}
]
}
def check_rule(
self,
entity1_info: Dict,
entity2_info: Dict,
rule: Dict
) -> bool:
"""
检查单条规则是否满足
:param entity1_info: 实体1的结构化信息(来自TextNormalizer.extract_shop_info)
:param entity2_info: 实体2的结构化信息
:param rule: 规则配置
:return: 是否满足规则
"""
conditions = rule.get('conditions', {})
# 核心名称完全匹配
if conditions.get('core_name_exact_match'):
if entity1_info['core_name'] != entity2_info['core_name']:
return False
# 品牌必须匹配
if conditions.get('brand_must_match'):
if entity1_info['brand'] != entity2_info['brand']:
return False
# 品牌必须不同
if conditions.get('brand_must_differ'):
if entity1_info['brand'] == entity2_info['brand']:
return False
# 店铺类型必须匹配
if conditions.get('shop_type_must_match'):
if entity1_info['shop_type'] != entity2_info['shop_type']:
return False
# 店铺类型必须不同
if conditions.get('shop_type_must_differ'):
if entity1_info['shop_type'] == entity2_info['shop_type']:
return False
# 其中一个是官方店
if conditions.get('one_is_official'):
if not (entity1_info['shop_type'] == 'official' or
entity2_info['shop_type'] == 'official'):
return False
# 地域必须匹配
if conditions.get('location_must_match'):
# 都有地域信息,且不相同
if entity1_info['location'] and entity2_info['location']:
if entity1_info['location'] != entity2_info['location']:
return False
# 地域必须不同
if conditions.get('location_must_differ'):
# 都有地域信息,且相同
if entity1_info['location'] and entity2_info['location']:
if entity1_info['location'] == entity2_info['location']:
return False
# 指定店铺类型
if 'shop_type' in conditions:
required_type = conditions['shop_type']
if (entity1_info['shop_type'] != required_type and
entity2_info['shop_type'] != required_type):
return False
return True
def match_entities(
self,
entity1_info: Dict,
entity2_info: Dict
) -> Optional[Dict]:
"""
使用规则引擎判断两个实体是否相同
:return: None表示规则无法判断,需要进入相似度计算
Dict表示规则可以判断,包含判断结果和置信度
{
'is_same': True/False,
'confidence': 0.95,
'matched_rule': 'rule_name',
'reason': '匹配原因'
}
"""
self.rule_stats['total_matches'] += 1
# 1. 优先检查白名单(强制相同)
entity1_name = entity1_info['normalized_name']
entity2_name = entity2_info['normalized_name']
whitelist_key = tuple(sorted([entity1_name, entity2_name]))
if whitelist_key in self.whitelist:
self.rule_stats['whitelist_hits'] += 1
logger.debug(f"✅ 白名单命中: {entity1_name} = {entity2_name}")
return {
'is_same': True,
'confidence': self.whitelist[whitelist_key],
'matched_rule': 'whitelist',
'reason': '白名单强制匹配'
}
# 2. 检查黑名单(强制不同)
if whitelist_key in self.blacklist:
self.rule_stats['blacklist_hits'] += 1
logger.debug(f"❌ 黑名单命中: {entity1_name} ≠ {entity2_name}")
return {
'is_same': False,
'confidence': 1.0,
'matched_rule': 'blacklist',
'reason': '黑名单强制区分'
}
# 3. 检查"不同实体"规则(优先级高,避免误判)
different_rules = sorted(
self.rules.get('different_entity_rules', []),
key=lambda x: x.get('priority', 0),
reverse=True
)
for rule in different_rules:
if self.check_rule(entity1_info, entity2_info, rule):
rule_name = rule['name']
self.rule_stats['rule_hits'][rule_name] = \
self.rule_stats['rule_hits'].get(rule_name, 0) + 1
logger.debug(
f"❌ 不同实体规则命中: {rule_name} | "
f"{entity1_name} ≠ {entity2_name}"
)
return {
'is_same': False,
'confidence': rule.get('confidence', 0.9),
'matched_rule': rule_name,
'reason': rule.get('description', '规则匹配')
}
# 4. 检查"相同实体"规则
same_rules = sorted(
self.rules.get('same_entity_rules', []),
key=lambda x: x.get('priority', 0),
reverse=True
)
for rule in same_rules:
if self.check_rule(entity1_info, entity2_info, rule):
rule_name = rule['name']
self.rule_stats['rule_hits'][rule_name] = \
self.rule_stats['rule_hits'].get(rule_name, 0) + 1
logger.debug(
f"✅ 相同实体规则命中: {rule_name} | "
f"{entity1_name} = {entity2_name}"
)
return {
'is_same': True,
'confidence': rule.get('confidence', 0.9),
'matched_rule': rule_name,
'reason': rule.get('description', '规则匹配')
}
# 5. 所有规则都不匹配,返回None进入相似度计算
logger.debug(
f"⚠️ 规则未命中,进入相似度计算: "
f"{entity1_name} vs {entity2_name}"
)
return None
def add_to_whitelist(
self,
entity1: str,
entity2: str,
confidence: float = 1.0
):
"""
添加到白名单
用途:人工审核确认的相同实体对
"""
key = tuple(sorted([entity1, entity2]))
self.whitelist[key] = confidence
logger.info(f"✅ 添加到白名单: {entity1} = {entity2} (置信度: {confidence})")
def add_to_blacklist(self, entity1: str, entity2: str):
"""
添加到黑名单
用途:人工审核确认的不同实体对
"""
key = tuple(sorted([entity1, entity2]))
self.blacklist.add(key)
logger.info(f"❌ 添加到黑名单: {entity1} ≠ {entity2}")
def save_lists(self, output_dir: str = "data/knowledge_base"):
"""
保存黑白名单到文件
便于版本控制和增量更新
"""
Path(output_dir).mkdir(parents=True, exist_ok=True)
# 保存白名单
whitelist_data = [
{
'entity1': k[0],
'entity2': k[1],
'confidence': v
}
for k, v in self.whitelist.items()
]
whitelist_path = Path(output_dir) / "whitelist.json"
with open(whitelist_path, 'w', encoding='utf-8') as f:
json.dump(whitelist_data, f, ensure_ascii=False, indent=2)
# 保存黑名单
blacklist_data = [
{'entity1': k[0], 'entity2': k[1]}
for k in self.blacklist
]
blacklist_path = Path(output_dir) / "blacklist.json"
with open(blacklist_path, 'w', encoding='utf-8') as f:
json.dump(blacklist_data, f, ensure_ascii=False, indent=2)
logger.info(
f"💾 黑白名单已保存 | "
f"白名单: {len(self.whitelist)} | 黑名单: {len(self.blacklist)}"
)
def load_lists(self, input_dir: str = "data/knowledge_base"):
"""
从文件加载黑白名单
"""
# 加载白名单
whitelist_path = Path(input_dir) / "whitelist.json"
if whitelist_path.exists():
with open(whitelist_path, 'r', encoding='utf-8') as f:
whitelist_data = json.load(f)
for item in whitelist_data:
key = tuple(sorted([item['entity1'], item['entity2']]))
self.whitelist[key] = item.get('confidence', 1.0)
# 加载黑名单
blacklist_path = Path(input_dir) / "blacklist.json"
if blacklist_path.exists():
with open(blacklist_path, 'r', encoding='utf-8') as f:
blacklist_data = json.load(f)
for item in blacklist_data:
key = tuple(sorted([item['entity1'], item['entity2']]))
self.blacklist.add(key)
logger.info(
f"📂 黑白名单已加载 | "
f"白名单: {len(self.whitelist)} | 黑名单: {len(self.blacklist)}"
)
def get_statistics(self) -> Dict:
"""获取规则引擎统计信息"""
total = self.rule_stats['total_matches']
if total == 0:
return {
'总匹配次数': 0,
'规则命中率': '0%',
'白名单命中率': '0%',
'黑名单命中率': '0%',
'规则命中详情': {}
}
rule_hits_total = sum(self.rule_stats['rule_hits'].values())
return {
'总匹配次数': total,
'规则命中率': f"{(rule_hits_total / total * 100):.2f}%",
'白名单命中率': f"{(self.rule_stats['whitelist_hits'] / total * 100):.2f}%",
'黑名单命中率': f"{(self.rule_stats['blacklist_hits'] / total * 100):.2f}%",
'规则命中详情': self.rule_stats['rule_hits']
}
# 使用示例
if __name__ == "__main__":
from core.preprocessor import TextNormalizer
# 初始化
normalizer = TextNormalizer()
rule_engine = EntityRuleEngine()
# 测试案例
test_cases = [
("Apple官方旗舰店", "apple official store"), # 应该相同
("Apple官方旗舰店", "Apple授权专卖店"), # 应该不同
("小米之家(北京)", "小米之家(上海)"), # 应该不同
("华为旗舰店", "HUAWEI FLAGSHIP STORE"), # 应该相同
]
for name1, name2 in test_cases:
# 提取结构化信息
info1 = normalizer.extract_shop_info(name1)
info2 = normalizer.extract_shop_info(name2)
# 规则匹配
result = rule_engine.match_entities(info1, info2)
if result:
print(f"\n{name1} vs {name2}")
print(f"判断: {'相同' if result['is_same'] else '不同'}")
print(f"置信度: {result['confidence']}")
print(f"匹配规则: {result['matched_rule']}")
print(f"原因: {result['reason']}")
else:
print(f"\n{name1} vs {name2}")
print("规则无法判断,需要相似度计算")
# 打印统计信息
print("\n规则引擎统计:")
stats = rule_engine.get_statistics()
for key, value in stats.items():
print(f"{key}: {value}")
代码关键点解析:
-
规则优先级系统:
python# 为什么需要优先级? # 场景:两个实体同时满足多条规则,应该用哪条? # 示例: # 实体A: Apple官方旗舰店 # 实体B: Apple授权专卖店 # 满足规则1:品牌相同(Apple = Apple) # 满足规则2:店铺类型不同(官方 ≠ 授权) # 解决方案:优先级高的规则优先匹配 # "不同实体规则"的优先级 > "相同实体规则" # 因为区分不同实体比合并实体更重要(避免误合并) -
黑白名单机制:
python# 为什么需要黑白名单? # 白名单:人工确认的相同实体 # 例如:"Apple Store" = "苹果商店" (规则难以覆盖的中英文对应) # 黑名单:人工确认的不同实体 # 例如:"小" (虽然都叫小米,但是不同业务) # 优势: # 1. 人工审核结果可以持久化保存 # 2. 新数据可以直接利用历史经验 # 3. 避免重复审核相同的实体对 -
规则配置化:
python# 为什么用JSON配置而不是硬编码? # 硬编码问题: # - 修改规则需要改代码、重新部署 # - 非技术人员无法调整规则 # - 难以做AB测试 # JSON配置优势: # - 运营人员可以直接修改规则 # - 可以动态加载,不需要重启服务 # - 版本控制友好,易于回滚 # - 可以针对不同行业/平台配置不同规则
🔢 核心实现:相似度计算层(Similarity Scorer)
多维度相似度计算器
python
# core/similarity.py
import Levenshtein
from typing import Dict, List, Set
import numpy as np
from loguru import logger
class SimilarityCalculator:
"""
多维度相似度计算器
支持的相似度算法:
- 编辑距离(Levenshtein Distance)
- Jaccard相似度
- 拼音相似度
- 字形相似度(可选)
- 语义向量相似度(可选,需要BERT模型)
"""
def __init__(self, use_semantic: bool = False):
"""
:param use_semantic: 是否启用语义相似度(需要加载BERT模型,较慢)
"""
self.use_semantic = use_semantic
# 权重配置(可根据业务场景调整)
self.weights = {
'levenshtein': 0.25, # 编辑距离权重
'jaccard': 0.25, # 集合相似度权重
'pinyin': 0.20, # 拼音相似度权重
'core_name_boost': 0.15, # 核心名称匹配加成
'semantic': 0.15 # 语义相似度权重(如果启用)
}
# 如果启用语义相似度,加载模型
if self.use_semantic:
self._load_semantic_model()
# 统计信息
self.stats = {
'total_comparisons': 0,
'avg_similarity': 0.0,
'high_similarity_count': 0 # > 0.8
}
def _load_semantic_model(self):
"""
加载语义相似度模型
使用sentence-transformers预训练模型
"""
try:
from sentence_transformers import SentenceTransformer
# 使用中文BERT模型
# paraphrase-multilingual-MiniLM-L12-v2: 轻量级多语言模型
self.semantic_model = SentenceTransformer(
'paraphrase-multilingual-MiniLM-L12-v2'
)
logger.info("✅ 语义相似度模型加载成功")
except Exception as e:
logger.warning(f"⚠️ 语义模型加载失败: {str(e)},将禁用语义相似度")
self.use_semantic = False
self.weights['semantic'] = 0
# 重新分配权重
total = sum(v for k, v in self.weights.items() if k != 'semantic')
for k in self.weights:
if k != 'semantic':
self.weights[k] = self.weights[k] / total
def levenshtein_similarity
编辑距离相似度
原理:
- 计算将str1转换为str2需要的最少编辑次数
- 归一化到[0, 1]区间
适用场景:
- 拼写错误:Aplpe -> Apple
- 轻微差异:Apple Store -> Apple store
:return: 相似度分数,1.0表示完全相同
"""
if not str1 or not str2:
return 0.0
# 计算编辑距离
distance = Levenshtein.distance(str1, str2)
# 归一化:1 - (distance / max_length)
max_len = max(len(str1), len(str2))
similarity = 1 - (distance / max_len)
return max(0.0, similarity) # 确保非负
def jaccard_similarity(self, tokens1: List[str], tokens2: List[str]) -> float:
"""
Jaccard相似度(集合相似度)
原理:
- 计算两个集合的交集大小 / 并集大小
- Jaccard = |A ∩ B| / |A ∪ B|
适用场景:
- 词序不同但词官方 旗舰店" vs "旗舰店 Apple 官方"
- 部分词重叠:"小米手机专卖店" vs "小米手机旗舰店"
示例:
tokens1 = ['apple', '官方', '旗舰店']
tokens2 = ['apple', '旗舰店', '官方']
交集 = {'apple', '官方', '旗舰店'} -> 3个
并集 = {'apple', '官方', '旗舰店'} -> 3个
Jaccard = 3/3 = 1.0
:param tokens1: 分词后的词列表1
:param tokens2: 分词后的词列表2
:return: Jaccard相似度
"""
if not tokens1 or not tokens2:
return 0.0
set1 = set(tokens1)
set2 = set(tokens2)
# 交集
intersection = set1 & set2
# 并集
union = set1 | set2
if len(union) == 0:
return 0.0
return len(intersection) / len(union)
def pinyin_similarity(self, pinyin1: str, pinyin2: str) -> float:
"""
拼音相似度
原理:
- 对比两个字符串的拼音表示
- 用于处理同音字混淆
适用场景:
- 同音字:"苹果" vs "平果"
- 中英混合:"Apple苹果" vs "Apple平果"
:param pinyin1: 字符串1的拼音
:param pinyin2: 字符串2的拼音
:return: 拼音相似度
"""
if not pinyin1 or not pinyin2:
return 0.0
# 使用编辑距离计算拼音相似度
return self.levenshtein_similarity(pinyin1, pinyin2)
def core_name_match_score(self, info1: Dict, info2: Dict) -> float:
"""
核心名称匹配分数
原理:
- 核心名称是去除修饰词后的主体部分
- 如果核心名称相同,给予高分加成
示例:
"Apple官方旗舰店(北京)" -> 核心名称: "apple"
"Apple旗舰店" -> 核心名称: "apple"
核心名称相同 -> 高分
:param info1: 实体1的结构化信息
:param info2: 实体2的结构化信息
:return: 匹配分数 [0, 1]
"""
core1 = info1.get('core_name', '')
core2 = info2.get('core_name', '')
if not core1 or not core2:
return 0.0
# 完全匹配
if core1 == core2:
return 1.0
# 部分匹配(一个包含另一个)
if core1 in core2 or core2 in core1:
return 0.8
# 使用编辑距离计算相似度
return self.levenshtein_similarity(core1, core2)
def semantic_similarity(self, text1: str, text2: str) -> float:
"""
语义相似度(基于BERT)
原理:
- 使用预训练的BERT模型将文本编码为向量
- 计算两个向量的余弦相似度
适用场景:
- 语义相似但文本差异大:"Apple官方店" vs "苹果正品专卖"
- 同义词替换:"手机" vs "移动电话"
优势:
- 能捕捉深层语义
- 对同义词、近义词敏感
劣势:
- 计算慢(需要模型推理)
- 需要GPU加速(对于大规模数据)
:param text1: 文本1
:param text2: 文本2
:return: 语义相似度 [0, 1]
"""
if not self.use_semantic:
return 0.0
if not text1 or not text2:
return 0.0
try:
# 编码为向量
embeddings = self.semantic_model.encode([text1, text2])
# 计算余弦相似度
# cosine_similarity = dot(v1, v2) / (||v1|| * ||v2||)
vec1 = embeddings[0]
vec2 = embeddings[1]
dot_product = np.dot(vec1, vec2)
norm1 = np.linalg.norm(vec1)
norm2 = np.linalg.norm(vec2)
if norm1 == 0 or norm2 == 0:
return 0.0
similarity = dot_product / (norm1 * norm2)
# 归一化到[0, 1]
# 余弦相似度范围是[-1, 1],但实际文本通常在[0, 1]
return max(0.0, min(1.0, (similarity + 1) / 2))
except Exception as e:
logger.warning(f"⚠️ 语义相似度计算失败: {str(e)}")
return 0.0
def calculate_similarity(
self,
entity1_info: Dict,
entity2_info: Dict,
entity1_variants: Dict,
entity2_variants: Dict
) -> Dict:
"""
综合计算两个实体的相似度
:param entity1_info: 实体1的结构化信息
:param entity2_info: 实体2的结构化信息
:param entity1_variants: 实体1的文本变体
:param entity2_variants: 实体2的文本变体
:return: 相似度结果
{
'total_score': 0.85, # 综合得分
'scores': { # 各维度得分
'levenshtein': 0.8,
'jaccard': 0.9,
'pinyin': 0.85,
'core_name': 1.0,
'semantic': 0.75
},
'weighted_score': 0.85 # 加权后的最终得分
}
"""
self.stats['total_comparisons'] += 1
scores = {}
# 1. 编辑距离相似度
lev_score = self.levenshtein_similarity(
entity1_variants['normalized'],
entity2_variants['normalized']
)
scores['levenshtein'] = lev_score
# 2. Jaccard相似度
jaccard_score = self.jaccard_similarity(
entity1_variants['tokens'],
entity2_variants['tokens']
)
scores['jaccard'] = jaccard_score
# 3. 拼音相似度
pinyin_score = self.pinyin_similarity(
entity1_variants['pinyin'],
entity2_variants['pinyin']
)
scores['pinyin'] = pinyin_score
# 4. 核心名称匹配分数
core_score = self.core_name_match_score(entity1_info, entity2_info)
scores['core_name'] = core_score
# 5. 语义相似度(如果启用)
if self.use_semantic:
semantic_score = self.semantic_similarity(
entity1_info['normalized_name'],
entity2_info['normalized_name']
)
scores['semantic'] = semantic_score
else:
scores['semantic'] = 0.0
# 6. 计算加权总分
weighted_score = sum(
scores[key] * self.weights[key]
for key in scores.keys()
)
# 更新统计
self.stats['avg_similarity'] = (
(self.stats['avg_similarity'] * (self.stats['total_comparisons'] - 1) +
weighted_score) / self.stats['total_comparisons']
)
if weighted_score > 0.8:
self.stats['high_similarity_count'] += 1
return {
'total_score': weighted_score,
'scores': scores,
'weighted_score': weighted_score,
'details': {
'entity1': entity1_info['original_name'],
'entity2': entity2_info['original_name']
}
}
def batch_calculate(
self,
entity_list: List[Dict],
threshold: float = 0.7
) -> List[Tuple[int, int, float]]:
"""
批量计算实体间相似度
:param entity_list: 实体列表,每项包含info和variants
:param threshold: 相似度阈值,只返回超过阈值的pair
:return: [(idx1, idx2, similarity), ...]
"""
results = []
n = len(entity_list)
# 两两比较(n^2复杂度)
for i in range(n):
for j in range(i + 1, n):
similarity_result = self.calculate_similarity(
entity_list[i]['info'],
entity_list[j]['info'],
entity_list[i]['variants'],
entity_list[j]['variants']
)
score = similarity_result['weighted_score']
if score >= threshold:
results.append((i, j, score))
# 按相似度降序排序
results.sort(key=lambda x: x[2], reverse=True)
logger.info(
f"✅ 批量相似度计算完成 | "
f"总对比: {n*(n-1)//2} | 超过阈值({threshold}): {len(results)}"
)
return results
def get_statistics(self) -> Dict:
"""获取相似度计算统计信息"""
total = self.stats['total_comparisons']
if total == 0:
return {
'总对比次数': 0,
'平均相似度': 0.0,
'高相似度占比': '0%'
}
return {
'总对比次数': total,
'平均相似度': f"{self.stats['avg_similarity']:.4f}",
'高相似度数量': self.stats['high_similarity_count'],
'高相似度占比': f"{(self.stats['high_similarity_count'] / total * 100):.2f}%"
}
# 使用示例
if __name__ == "__main__":
from core.preprocessor import TextNormalizer
# 初始化
normalizer = TextNormalizer()
calculator = SimilarityCalculator(use_semantic=False) # 暂不使用语义相似度
# 测试案例
test_pairs = [
("Apple官方旗舰店", "apple official store"),
("Apple官方旗舰店", "苹果旗舰店"),
("小米之家(北京)", "小米之家(上海)"),
("华为Mate50", "HUAWEI mate 50"),
]
for name1, name2 in test_pairs:
# 预处理
info1 = normalizer.extract_shop_info(name1)
info2 = normalizer.extract_shop_info(name2)
variants1 = normalizer.generate_variants(name1)
variants2 = normalizer.generate_variants(name2)
# 计算相似度
result = calculator.calculate_similarity(info1, info2, variants1, variants2)
print(f"\n{name1} vs {name2}")
print(f"综合得分: {result['weighted_score']:.4f}")
print("各维度得分:")
for key, value in result['scores'].items():
print(f" {key}: {value:.4f}")
# 打印统计
print("\n相似度计算统计:")
stats = calculator.get_statistics()
for key, value in stats.items():
print(f"{key}: {value}")
相似度算法详细解析:
1. 编辑距离(Levenshtein Distance)
算法原理:
json
将字符串A转换为字符串B所需的最少编辑操作次数
操作类型:
- 插入一个字符
- 删除一个字符
- 替换一个字符
示例:
"kitten" -> "sitting"
1. kitten -> sitten (替换k为s)
2. sitten -> sittin (替换e为i)
3. sittin -> sitting (插入g)
编辑距离 = 3
为什么有效?
- 拼写错误通常编辑距离小(如"Aplpe" vs "Apple",距离=2)
- 能处理字符增删改的差异
- 计算快速(动态规划,O(mn)复杂度)
局限性:
- 对词序敏感:"Apple Store" vs "Store Apple"距离很大
- 无法理解语义:"好" vs "坏"距离=1,但意思相反
2. Jaccard相似度
算法原理:
Jaccard = |A ∩ B| / |A ∪ B|
示例:
A = {"apple", "官方", "旗舰店"}
B = {"apple", "旗舰店", "官方"}
交集 = {"apple", "官方", "旗舰店"} = 3个
并集 = {"apple", "官方", "旗舰店"} = 3个
Jaccard = 3/3 = 1.0
为什么有效?
- 对词序不敏感
- 能处理部分词重叠的情况
- 计算简单,适合大规模数据
应用技巧:
python
# 对于短文本,可以用字符级Jaccard
def char_jaccard(str1, str2):
set1 = set(str1)
set2 = set(str2)
return len(set1 & set2) / len(set1 | set2)
# 示例:
char_jaccard("apple", "aple") # 缺少一个p
# set1 = {'a', 'p', 'l', 'e'}
# set2 = {'a', 'p', 'l', 'e'}
# 结果 = 4/4 = 1.0(因为集合会去重)
# 这个例子说明字符级Jaccard对重复字符不敏感
# 更好的做法是用多重集(Counter)
from collections import Counter
def multiset_jaccard(str1, str2):
counter1 = Counter(str1)
counter2 = Counter(str2)
intersection = sum((counter1 & counter2).values())
union = sum((counter1 | counter2).values())
return intersection / union if union > 0 else 0
multiset_jaccard("apple", "aple") # 考虑重复
# counter1 = {'a':1, 'p':2, 'l':1, 'e':1}
# counter2 = {'a':1, 'p':1, 'l':1, 'e':1}
# intersection = 1+1+1+1 = 4
# union = 1+2+1+1 = 5
# 结果 = 4/5 = 0.8(更合理)
3. 拼音相似度
为什么需要?
json
问题:中文同音字混淆
"苹果" vs "平果" # 发音相同,但字不同
"华为" vs "花为" # 打字错误
解决:转为拼音后比较
"苹果" -> "pingguo"
"平果" -> "pingguo"
拼音相同 -> 高相似度
实现细节:
python
from pypinyin import lazy_pinyin, Style
# 多种拼音风格
text = "苹果"
# 1. 普通风格(带声调)
lazy_pinyin(text, style=Style.TONE)
# ['píng', 'guǒ']
# 2. 不带声调
lazy_pinyin(text, style=Style.NORMAL)
# ['ping', 'guo']
# 3. 首字母
lazy_pinyin(text, style=Style.FIRST_LETTER)
# ['p', 'g']
# 推荐:使用不带声调的拼音
# 因为声调在输入错误中经常被忽略
适用场景:
- 输入法输错字:"在线" vs "再线"
- 形近字误用:"己" vs "已"
- 方言影响:"前面" vs "钱面"(某些方言前/钱不分)
4. 语义相似度(BERT)
为什么需要?
json
字符串相似度无法理解语义:
"苹果官方店" vs "苹果正品专卖"
- 编辑距离大(很多字不同)
- Jaccard低(共同词少)
- 但语义相似(都是卖苹果产品的官方渠道)
BERT可以捕捉语义:
- "官方" ≈ "正品"
- "店" ≈ "专卖"
如何工作?
json
1. 预训练阶段(已完成):
BERT在海量文本上学习语言表示
学到了词语间的语义关系
2. 推理阶段(我们使用):
输入文本 -> BERT -> 768维向量
"苹果官方店" -> [0.2, 0.5, ..., 0.8]
"苹果正品专卖" -> [0.3, 0.4, ..., 0.7]
3. 相似度计算:
cosine_similarity(vec1, vec2)
性能考虑:
python
# BERT推理慢,需要优化
# 方法1:批量编码
texts = ["text1", "text2", ..., "text1000"]
embeddings = model.encode(texts, batch_size=32) # 批量处理
# 方法2:缓存向量
# 对于重复查询的实体,缓存其向量
cache = {}
def get_embedding(text):
if text not in cache:
cache[text] = model.encode([text])[0]
return cache[text]
# 方法3:使用更小的模型
# paraphrase-multilingual-MiniLM-L12-v2(本文使用)
# vs
# bert-base-chinese(更大但更准)
# 方法4:降维
# 768维太大,可以用PCA降到128维
from sklearn.decomposition import PCA
pca = PCA(n_components=128)
embeddings_reduced = pca.fit_transform(embeddings)
🔗 核心实现:实体匹配器(Entity Matcher)
python
# core/matcher.py
from typing import List, Dict, Optional, Set, Tuple
from loguru import logger
from core.preprocessor import TextNormalizer
from core.rule_engine import EntityRuleEngine
from core.similarity import SimilarityCalculator
class EntityMatcher:
"""
实体匹配器
功能:
- 协调规则引擎和相似度计算
- 生成候选实体
- 管理匹配决策
"""
def __init__(
self,
rule_engine: EntityRuleEngine,
similarity_calculator: SimilarityCalculator,
normalizer: TextNormalizer,
similarity_threshold: float = 0.7,
high_confidence_threshold: float = 0.9
):
"""
:param similarity_threshold: 相似度阈值,超过此值认为可能相同
:param high_confidence_threshold: 高置信度阈值,超过此值自动合并
"""
self.rule_engine = rule_engine
self.similarity_calculator = similarity_calculator
self.normalizer = normalizer
self.similarity_threshold = similarity_threshold
self.high_confidence_threshold = high_confidence_threshold
# 统计信息
self.stats = {
'total_matches': 0,
'rule_decided': 0,
'similarity_decided': 0,
'need_review': 0,
'auto_merged': 0
}
def match(
self,
entity1_name: str,
entity2_name: str
) -> Dict:
"""
判断两个实体是否相同
:return: 匹配结果
{
'is_same': True/False/None, # None表示需要人工审核
'confidence': 0.85,
'method': 'rule/similarity',
'details': {...}
}
"""
self.stats['total_matches'] += 1
# 1. 预处理
info1 = self.normalizer.extract_shop_info(entity1_name)
info2 = self.normalizer.extract_shop_info(entity2_name)
# 2. 尝试规则匹配
rule_result = self.rule_engine.match_entities(info1, info2)
if rule_result is not None:
# 规则给出了明确结论
self.stats['rule_decided'] += 1
# 高置信度自动合并
if rule_result['confidence'] >= self.high_confidence_threshold:
self.stats['auto_merged'] += 1
return {
'is_same': rule_result['is_same'],
'confidence': rule_result['confidence'],
'method': 'rule',
'details': {
'rule_name': rule_result['matched_rule'],
'reason': rule_result['reason']
}
}
# 3. 规则无法判断,使用相似度计算
variants1 = self.normalizer.generate_variants(entity1_name)
variants2 = self.normalizer.generate_variants(entity2_name)
similarity_result = self.similarity_calculator.calculate_similarity(
info1, info2, variants1, variants2
)
score = similarity_result['weighted_score']
self.stats['similarity_decided'] += 1
# 4. 根据相似度阈值判断
if score >= self.high_confidence_threshold:
# 高置信度:自动判定为相同
self.stats['auto_merged'] += 1
return {
'is_same': True,
'confidence': score,
'method': 'similarity',
'details': similarity_result
}
elif score >= self.similarity_threshold:
# 中等置信度:需要人工审核
self.stats['need_review'] += 1
return {
'is_same': None, # 待定
'confidence': score,
'method': 'similarity',
'details': similarity_result,
'action': 'need_review'
}
else:
# 低置信度:判定为不同
return {
'is_same': False,
'confidence': 1 - score, # 不同的置信度
'method': 'similarity',
'details': similarity_result
}
def find_candidates(
self,
new_entity: str,
existing_entities: List[str],
top_k: int = 5
) -> List[Dict]:
"""
为新实体查找候选匹配
:param new_entity: 新实体名称
:param existing_entities: 已存在的实体列表
:param top_k: 返回前K个候选
:return: 候选列表,按相似度降序
[
{
'entity': 'Apple官方店',
'confidence': 0.92,
'method': 'rule',
'is_same': True
},
...
]
"""
candidates = []
for existing_entity in existing_entities:
result = self.match(new_entity, existing_entity)
candidates.append({
'entity': existing_entity,
'confidence': result['confidence'],
'method': result['method'],
'is_same': result['is_same'],
'details': result.get('details', {})
})
# 按置信度降序排序
candidates.sort(key=lambda x: x['confidence'], reverse=True)
# 返回top K
return candidates[:top_k]
def batch_match(
self,
entity_names: List[str]
) -> List[List[int]]:
"""
批量匹配:将相似实体聚类
:param entity_names: 实体名称列表
:return: 聚类结果,每个聚类是索引列表
[
[0, 3, 5], # 实体0、3、5是同一个
[1, 4], # 实体1、4是同一个
[2], # 实体2是独立的
...
]
"""
n = len(entity_names)
# 使用并查集(Union-Find)进行聚类
parent = list(range(n))
def find(x):
if parent[x] != x:
parent[x] = find(parent[x]) # 路径压缩
return parent[x]
def union(x, y):
px, py = find(x), find(y)
if px != py:
parent[px] = py
# 两两比较
for i in range(n):
for j in range(i + 1, n):
result = self.match(entity_names[i], entity_names[j])
# 如果判定为相同,合并
if result['is_same'] is True:
union(i, j)
logger.debug(
f"✅ 合并: {entity_names[i]} = {entity_names[j]} "
f"(置信度: {result['confidence']:.2f})"
)
# 构建聚类结果
clusters = {}
for i in range(n):
root = find(i)
if root not in clusters:
clusters[root] = []
clusters[root].append(i)
# 转为列表
cluster_list = list(clusters.values())
# 按聚类大小降序排序
cluster_list.sort(key=len, reverse=True)
logger.info(
f"✅ 批量匹配完成 | "
f"实体数: {n} | 聚类数: {len(cluster_list)}"
)
return cluster_list
def get_statistics(self) -> Dict:
"""获取匹配统计信息"""
total = self.stats['total_matches']
if total == 0:
return {
'总匹配次数': 0,
'规则决策占比': '0%',
'相似度决策占比': '0%',
'需要审核占比': '0%',
'自动合并数': 0
}
return {
'总匹配次数': total,
'规则决策数': self.stats['rule_decided'],
'相似度决策数': self.stats['similarity_decided'],
'规则决策占比': f"{(self.stats['rule_decided'] / total * 100):.2f}%",
'相似度决策占比': f"{(self.stats['similarity_decided'] / total * 100):.2f}%",
'需要审核数': self.stats['need_review'],
'需要审核占比': f"{(self.stats['need_review'] / total * 100):.2f}%",
'自动合并数': self.stats['auto_merged']
}
# 使用示例
if __name__ == "__main__":
# 初始化各组件
normalizer = TextNormalizer()
rule_engine = EntityRuleEngine()
similarity_calculator = SimilarityCalculator(use_semantic=False)
matcher = EntityMatcher(
rule_engine=rule_engine,
similarity_calculator=similarity_calculator,
normalizer=normalizer,
similarity_threshold=0.7,
high_confidence_threshold=0.9
)
# 测试批量匹配
test_entities = [
"Apple官方旗舰店",
"apple official store",
"苹果旗舰店",
"Apple授权专卖店",
"小米之家(北京)",
"小米之家(上海)",
"华为Mate50官方店",
"HUAWEI Mate 50 Official"
]
clusters = matcher.batch_match(test_entities)
print("\n聚类结果:")
for i, cluster in enumerate(clusters):
print(f"\n聚类 {i+1}:")
for idx in cluster:
print(f" - {test_entities[idx]}")
# 打印统计
print("\n匹配统计:")
stats = matcher.get_statistics()
for key, value in stats.items():
print(f"{key}: {value}")
💾 核心实现:知识库管理(Knowledge Base)
python
# core/knowledge_base.py
import json
from pathlib import Path
from typing import Dict, List, Optional, Set
from loguru import logger
class EntityKnowledgeBase:
"""
实体知识库
功能:
- 管理实体-别名映射
- 提供快速查询接口
- 支持增量更新
"""
def __init__(self, kb_dir: str = "data/knowledge_base"):
self.kb_dir = Path(kb_dir)
self.kb_dir.mkdir(parents=True, exist_ok=True)
# 实体库:{entity_id: {标准名称, 别名列表, 元数据}}
self.entities: Dict[str, Dict] = {}
# 倒排索引:{别名: entity_id}
self.alias_index: Dict[str, str] = {}
# 加载已有知识库
self.load()
def add_entity(
self,
entity_id: str,
standard_name: str,
aliases: List[str],
metadata: Optional[Dict] = None
):
"""添加实体到知识库"""
self.entities[entity_id] = {
'standard_name': standard_name,
'aliases': list(set(aliases)), # 去重
'metadata': metadata or {}
}
# 更新倒排索引
for alias in aliases:
self.alias_index[alias] = entity_id
logger.info(f"✅ 添加实体: {entity_id} | 别名数: {len(aliases)}")
def query(self, name: str) -> Optional[str]:
"""查询实体ID"""
return self.alias_index.get(name)
def get_standard_name(self, name: str) -> Optional[str]:
"""获取标准名称"""
entity_id = self.query(name)
if entity_id:
return self.entities[entity_id]['standard_name']
return None
def save(self):
"""保存知识库到文件"""
kb_file = self.kb_dir / "entities.json"
with open(kb_file, 'w', encoding='utf-8') as f:
json.dump(self.entities, f, ensure_ascii=False, indent=2)
logger.info(f"💾 知识库已保存 | 实体数: {len(self.entities)}")
def load(self):
"""从文件加载知识库"""
kb_file = self.kb_dir / "entities.json"
if not kb_file.exists():
logger.warning("⚠️ 知识库文件不存在,使用空知识库")
return
with open(kb_file, 'r', encoding='utf-8') as f:
self.entities = json.load(f)
# 重建倒排索引
for entity_id, data in self.entities.items():
for alias in data['aliases']:
self.alias_index[alias] = entity_id
logger.info(f"📂 知识库已加载 | 实体数: {len(self.entities)}")
🚀 主程序与运行示例
python
# main.py
import pandas as pd
from loguru import logger
from tqdm import tqdm
from core.preprocessor import TextNormalizer
from core.rule_engine import EntityRuleEngine
from core.similarity import SimilarityCalculator
from core.matcher import EntityMatcher
from core.
def main():
"""主流程:实体消歧"""
# 配置日志
logger.add("logs/disambiguation_{time}.log", rotation="500 MB")
logger.info("🚀 开始实体消歧处理...")
# 1. 初始化组件
normalizer = TextNormalizer()
rule_engine = EntityRuleEngine()
rule_engine.load_lists() # 加载黑白名单
similarity_calculator = SimilarityCalculator(use_semantic=False)
matcher = EntityMatcher(
rule_engine=rule_engine,
similarity_calculator=similarity_calculator,
normalizer=normalizer,
similarity_threshold=0.7,
high_confidence_threshold=0.9
)
knowledge_base = EntityKnowledgeBase()
# 2. 加载原始数据
logger.info("📂 加载原始数据...")
df = pd.read_csv("data/raw/shop_names.csv")
shop_names = df['shop_name'].tolist()
logger.info(f"✅ 加载完成 | 店铺数: {len(shop_names)}")
# 3. 批量匹配与聚类
logger.info("🔄 开始实体聚类...")
clusters = matcher.batch_match(shop_names)
# 4. 生成标准化映射
logger.info("📝 生成标准化映射...")
mapping = []
for cluster_id, cluster_indices in enumerate(tqdm(clusters, desc="处理聚类")):
# 选择聚类中最常见的名称作为标准名称
cluster_names = [shop_names[i] for i in cluster_indices]
standard_name = max(set(cluster_names), key=cluster_names.count)
entity_id = f"entity_{cluster_id:06d}"
# 添加到知识库
knowledge_base.add_entity(
entity_id=entity_id,
standard_name=standard_name,
aliases=cluster_names
)
# 记录映射关系
for idx in cluster_indices:
mapping.append({
'original_name': shop_names[idx],
'standard_name': standard_name,
'entity_id': entity_id,
'cluster_size': len(cluster_indices)
})
# 5. 保存结果
logger.info("💾 保存结果...")
# 保存映射表
mapping_df = pd.DataFrame(mapping)
mapping_df.to_csv("data/processed/entity_mapping.csv", index=False)
# 保存知识库
knowledge_base.save()
# 保存黑白名单
rule_engine.save_lists()
# 6. 统计报告
logger.info("\n📊 处理完成!统计报告:")
logger.info(f" 原始实体数: {len(shop_names)}")
logger.info(f" 聚类数: {len(clusters)}")
logger.info(f" 压缩率: {(1 - len(clusters)/len(shop_names))*100:.2f}%")
# 打印各组件统计
print("\n规则引擎统计:")
print(rule_engine.get_statistics())
print("\n相似度计算统计:")
print(similarity_calculator.get_statistics())
print("\n匹配器统计:")
print(matcher.get_statistics())
logger.info("🎉 实体消歧完成!")
if __name__ == "__main__":
main()
运行命令
bash
# 1. 准备数据
# 将原始店铺名放在 data/raw/shop_names.csv
# 2. 运行主程序
python main.py
# 3. 查看结果
# data/processed/entity_mapping.csv - 实体映射表
# data/knowledge_base/entities.json - 知识库
# logs/ - 运行日志
输出示例
控制台输出:
json
🚀 开始实体消歧处理...
📂 加载原始数据...
✅ 加载完成 | 店铺数: 5000
🔄 开始实体聚类...
处理聚类: 100%|██████████| 3200/3200
📝 生成标准化映射...
💾 保存结果...
📊 处理完成!统计报告:
原始实体数: 5000
聚类数: 3200
压缩率: 36.00%
规则引擎统计:
{'总匹配次数': 12497500, '规则命中率': '15.32%', ...}
🎉 实体消歧完成!
entity_mapping.csv示例:
| original_name | standard_name | entity_id | cluster_size |
|---|---|---|---|
| Apple官方旗舰店 | Apple官方旗舰店 | entity_000001 | 5 |
| apple official store | Apple官方旗舰店 | entity_000001 | 5 |
| 苹果旗舰店 | Apple官方旗舰店 | entity_000001 | 5 |
📚 总结与延伸
本文完整实现了一个生产级实体消歧系统,核心亮点包括:
✅ 多层过滤架构 :规则(15%) → 相似度(85%) → 人工审核
✅ 多维度相似度 :编辑距离 + Jaccard + 拼音 + 语义
✅ 工程化设计 :黑白名单、增量更新、统计监控
✅ 可扩展性:支持自定义规则、动态阈值调整
下一步优化方向
- 性能优化:使用Faiss进行向量索引加速
- 主动学习:利用人工反馈持续优化模型
- 跨语言支持:处理中英混合、多语言实体
- 实时服务化:封装为API服务供下游调用
希望这篇文章对你有帮助!🎉
🌟 文末
好啦~以上就是本期 《Python爬虫实战》的全部内容啦!如果你在实践过程中遇到任何疑问,欢迎在评论区留言交流,我看到都会尽量回复~咱们下期见!
小伙伴们在批阅的过程中,如果觉得文章不错,欢迎点赞、收藏、关注哦~
三连就是对我写作道路上最好的鼓励与支持! ❤️🔥
📌 专栏持续更新中|建议收藏 + 订阅
专栏 👉 《Python爬虫实战》,我会按照"入门 → 进阶 → 工程化 → 项目落地"的路线持续更新,争取让每一篇都做到:
✅ 讲得清楚(原理)|✅ 跑得起来(代码)|✅ 用得上(场景)|✅ 扛得住(工程化)
📣 想系统提升的小伙伴:强烈建议先订阅专栏,再按目录顺序学习,效率会高很多~

✅ 互动征集
想让我把【某站点/某反爬/某验证码/某分布式方案】写成专栏实战?
评论区留言告诉我你的需求,我会优先安排更新 ✅
⭐️ 若喜欢我,就请关注我叭~(更新不迷路)
⭐️ 若对你有用,就请点赞支持一下叭~(给我一点点动力)
⭐️ 若有疑问,就请评论留言告诉我叭~(我会补坑 & 更新迭代)

免责声明:本文仅用于学习与技术研究,请在合法合规、遵守站点规则与 Robots 协议的前提下使用相关技术。严禁将技术用于任何非法用途或侵害他人权益的行为。技术无罪,责任在人!!!