Python爬虫实战:电商实体消歧完整实战 - 从混乱店铺名到标准化知识库的工程化实现,一文带你搞定!

㊙️本期内容已收录至专栏《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等场景中,同名实体消歧是数据清洗的核心难题。一个看似简单的店铺名,背后可能隐藏着多重复杂性:

典型问题场景:

  1. 同一实体的多种表述

    • "Apple官方旗舰店" = "Apple旗舰店" = "苹果官方店" = "Apple Store官方"
    • 这些都指向同一个店铺,但文本完全不同
  2. 不同实体的相似名称

    • "小米之家(北京国贸店)" ≠ "小米之家(上海南京路店)"
    • 虽然都叫"小米之家",但是不同的物理店铺
  3. 官方与授权的区分

    • "华为官方旗舰店" ≠ "华为授权专卖店"
    • 需要识别官方认证与第三方授权
  4. 缩写与全称的对应

    • "阿里巴巴集团控股有限公司" = "阿里巴巴" = "Alibaba Group"
    • 同一公司的不同书写形式
  5. 特殊字符与空格的差异

    • "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)                   │
│  ├─ 更新实体-别名映射表                                          │
│  ├─ 记录消歧决策日志(可追溯)                                   │
│  ├─ 构建倒排索引(快速查询)                                     │
│  └─ 生成标准化映射文件(用于下游系统)                           │
└─────────────────────────────────────────────────────────────────┘

为什么这样设计?

  1. 分层过滤:先用低成本的精确匹配和规则处理90%数据,再用计算密集的相似度算法处理剩余10%
  2. 人机协同:机器处理简单case,人工处理边界case,既保证效率又保证准确性
  3. 可追溯性:每个决策都有日志,方便后续审计和优化
  4. 增量更新:新数据只需与已有知识库比对,不需要重新计算所有数据

📦 环境准备与依赖安装

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']}")

使用说明与注意事项

代码详细解析:

  1. normalize() 方法

    • 作用:基础文本清洗,统一格式
    • 处理步骤:小写转换 → 去特殊字符 → 空格归一化 → 去首尾无意义词
    • 关键点 :使用正则表达式预编译(re.compile)提升性能,避免每次调用都重新编译
  2. extract_shop_info() 方法

    • 作用:从店铺名中提取结构化信息

    • 提取内容:店铺类型(官方/授权)、地域、品牌、核心名称

    • 应用场景

      • 相同品牌不同地域的店铺需要区分(小米之家北京店 ≠ 小米之家上海店)
      • 官方与授权店需要区分(Apple官方 ≠ Apple授权)
    • 技术细节

      python 复制代码
      # 使用命名捕获组提取信息
      self.location_pattern = re.compile(r'(北京|上海|...)')
      match = pattern.search(text)
      if match:
          location = match.group(0)  # 获取匹配的文本
  3. 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)
  4. 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}")

代码关键点解析:

  1. 规则优先级系统

    python 复制代码
    # 为什么需要优先级?
    # 场景:两个实体同时满足多条规则,应该用哪条?
    
    # 示例:
    # 实体A: Apple官方旗舰店
    # 实体B: Apple授权专卖店
    
    # 满足规则1:品牌相同(Apple = Apple)
    # 满足规则2:店铺类型不同(官方 ≠ 授权)
    
    # 解决方案:优先级高的规则优先匹配
    # "不同实体规则"的优先级 > "相同实体规则"
    # 因为区分不同实体比合并实体更重要(避免误合并)
  2. 黑白名单机制

    python 复制代码
    # 为什么需要黑白名单?
    
    # 白名单:人工确认的相同实体
    # 例如:"Apple Store" = "苹果商店" (规则难以覆盖的中英文对应)
    
    # 黑名单:人工确认的不同实体
    # 例如:"小" (虽然都叫小米,但是不同业务)
    
    # 优势:
    # 1. 人工审核结果可以持久化保存
    # 2. 新数据可以直接利用历史经验
    # 3. 避免重复审核相同的实体对
  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 + 拼音 + 语义

工程化设计 :黑白名单、增量更新、统计监控

可扩展性:支持自定义规则、动态阈值调整

下一步优化方向

  1. 性能优化:使用Faiss进行向量索引加速
  2. 主动学习:利用人工反馈持续优化模型
  3. 跨语言支持:处理中英混合、多语言实体
  4. 实时服务化:封装为API服务供下游调用

希望这篇文章对你有帮助!🎉

🌟 文末

好啦~以上就是本期 《Python爬虫实战》的全部内容啦!如果你在实践过程中遇到任何疑问,欢迎在评论区留言交流,我看到都会尽量回复~咱们下期见!

小伙伴们在批阅的过程中,如果觉得文章不错,欢迎点赞、收藏、关注哦~
三连就是对我写作道路上最好的鼓励与支持! ❤️🔥

📌 专栏持续更新中|建议收藏 + 订阅

专栏 👉 《Python爬虫实战》,我会按照"入门 → 进阶 → 工程化 → 项目落地"的路线持续更新,争取让每一篇都做到:

✅ 讲得清楚(原理)|✅ 跑得起来(代码)|✅ 用得上(场景)|✅ 扛得住(工程化)

📣 想系统提升的小伙伴:强烈建议先订阅专栏,再按目录顺序学习,效率会高很多~

✅ 互动征集

想让我把【某站点/某反爬/某验证码/某分布式方案】写成专栏实战?

评论区留言告诉我你的需求,我会优先安排更新 ✅


⭐️ 若喜欢我,就请关注我叭~(更新不迷路)

⭐️ 若对你有用,就请点赞支持一下叭~(给我一点点动力)

⭐️ 若有疑问,就请评论留言告诉我叭~(我会补坑 & 更新迭代)


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

相关推荐
weixin_452159552 小时前
C++与Java性能对比
开发语言·c++·算法
80530单词突击赢2 小时前
C++哈希表实现:开散列与闭散列详解
算法·哈希算法·散列表
Timmylyx05182 小时前
类欧几里得学习笔记
笔记·学习·算法
aluluka2 小时前
Emacs折腾日记(三十六)——打造个人笔记系统
笔记·python·emacs
wangluoqi2 小时前
26.2.2练习总结
算法
2301_765703142 小时前
C++中的工厂模式实战
开发语言·c++·算法
黎子越2 小时前
python相关练习
java·前端·python
小白学大数据2 小时前
实测数据:多进程、多线程、异步协程爬虫速度对比
开发语言·爬虫·python·php
小鸡吃米…2 小时前
机器学习 - 精确率与召回率
人工智能·python·机器学习