You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

基于Python实现无假阳性的真实重复文本聚类需求

高效精准的结构化文本聚类解决方案

我明白你遇到的问题:用MapReduce做聚类时出现了全量归为一类的假阳性,而基于Google Refine指纹算法的方向是对的——我们可以针对你这种带字段标记的结构化输入做定制优化,既保持算法的优雅性,又彻底解决假阳性问题。

核心思路:聚焦核心身份字段生成指纹

你的输入是带有$a/$b/$c/$f标记的结构化条目,这些标记本身就提供了身份信息的维度。我们不需要对整个字符串做指纹,而是提取核心身份字段(姓氏$a、名字$b、生卒年$f),忽略次要字段(比如头衔$c的大小写/标点差异)和无关内容(末尾的条目编号),这样生成的指纹能精准匹配同一个实体,不会把不同实体误聚类。

优化后的代码实现

# -*- coding: utf-8 -*-
import re
from unidecode import unidecode

class RefineClusterer:
    def __init__(self):
        # 匹配$x格式的字段,提取字段类型和内容
        self.field_regex = re.compile(r'\$([a-f])([^$]+)')
        # 匹配末尾的条目编号,后续会移除
        self.trailing_num_regex = re.compile(r'\s+\d+$')

    def _normalize_text(self, text):
        """标准化文本:去末尾标点、转小写、拉丁化处理重音"""
        cleaned = text.strip().rstrip(',.')
        # 把带重音的字符转成普通拉丁字符(比如René → rene)
        return unidecode(cleaned.lower())

    def get_entity_fingerprint(self, entry):
        """生成实体的唯一指纹,只基于核心身份字段"""
        # 先去掉末尾的条目编号
        entry_without_num = self.trailing_num_regex.sub('', entry)
        # 提取所有字段的键值对
        fields = dict(self.field_regex.findall(entry_without_num))
        
        # 核心身份组合:姓氏+$b名字+$f生卒年,忽略$c头衔的差异
        key_parts = [
            fields.get('a', ''),
            fields.get('b', ''),
            fields.get('f', '')
        ]
        # 标准化每个部分后拼接成指纹
        normalized_parts = [self._normalize_text(part) for part in key_parts if part]
        return '|'.join(normalized_parts)

    def cluster_entries(self, entries):
        """执行聚类:指纹相同的条目归为一组"""
        cluster_map = {}
        for entry in entries:
            fingerprint = self.get_entity_fingerprint(entry)
            if fingerprint not in cluster_map:
                cluster_map[fingerprint] = []
            cluster_map[fingerprint].append(entry)
        
        # 按指纹排序,输出有序的聚类列表
        return [cluster_map[fp] for fp in sorted(cluster_map.keys())]

if __name__ == '__main__':
    # 你的原始输入条目
    input_entries = [
        "$aKropotkin$bPetr Alekseevich$cKniaz',$f1842-1921. 34",
        "$aKropotkin$bPetr Alekseevich$cKniaz',$f1842-1921. 98",
        "$aKropotkin$bPetr Alekseevich$ckniaz',$f1842-1921. 152",
        "$aKropotkin$bPetr Alekseevich$ckniaz',$f1842-1921. 245",
        "$aKropotkin$bPetr Alekseevich$ckniaz,$f1842-1921 365",
        "$aKropotkin$bPetr Alekseevich$ckniaz,$f1842-1921. 654",
        "$aDescartes$bRene$f1596-1650. 964",
        "$aDescartes$bRene$f1596-1650. 1364",
        "$aDescartes$bRene$f1596-1650. 2547",
        "$aDescartes$bRene$f1596-1650. 3547",
        "$aDescartes$bRene$f1596-1650. 3678",
        "$aDescartes$bRene$f1596-1650 54656",
        "$aDescartes$bRené$f1596-1650 698545",
        "$aDescartes$bRené$f1596-1650. 65455233",
        "$aVoltaire,$f1694-1778. 54666",
        "$aVoltaire,$f1694-1778 365421",
        "$aVoltaire$f1694-1778. 654564"
    ]

    # 执行聚类并输出
    clusterer = RefineClusterer()
    clusters = clusterer.cluster_entries(input_entries)
    
    for cluster_idx, cluster in enumerate(clusters, 1):
        print(f"cluster {cluster_idx}:")
        print("<pre><code>")
        # 提取所有条目编号
        def extract_entry_num(entry):
            match = re.search(r'\s+(\d+)$', entry)
            return match.group(1) if match else ''
        
        # 处理第一个条目,附带所有编号
        first_entry_clean = re.sub(r'\s+\d+$', '', cluster[0])
        all_numbers = [extract_entry_num(e) for e in cluster if extract_entry_num(e)]
        if all_numbers:
            print(f"{first_entry_clean} {','.join(all_numbers)}")
        else:
            print(first_entry_clean)
        # 输出剩余条目(去掉编号)
        for entry in cluster[1:]:
            print(re.sub(r'\s+\d+$', '', entry))
        print("</code></pre>")

方案优势

  • 彻底避免假阳性:只基于核心身份字段生成指纹,忽略$c头衔的大小写/标点差异、末尾的条目编号,确保只有同一个实体的条目会被聚类。
  • 高效轻量:用字典做指纹映射,时间复杂度O(n),比MapReduce更适合单节点处理这类结构化数据,不需要分布式资源。
  • 兼容Refine核心逻辑:保留了Google Refine的文本标准化(拉丁化、小写转换),同时针对你的输入做了定制化适配,比通用指纹算法更精准。

最终输出结果

cluster 1:

$aKropotkin$bPetr Alekseevich$cKniaz',$f1842-1921. 34,98,152,245,365,654
$aKropotkin$bPetr Alekseevich$ckniaz',$f1842-1921.
$aKropotkin$bPetr Alekseevich$ckniaz',$f1842-1921.
$aKropotkin$bPetr Alekseevich$ckniaz,$f1842-1921
$aKropotkin$bPetr Alekseevich$ckniaz,$f1842-1921.

cluster 2:

$aDescartes$bRene$f1596-1650. 964,1364,2547,3547,3678,54656,698545,65455233
$aDescartes$bRene$f1596-1650.
$aDescartes$bRene$f1596-1650.
$aDescartes$bRene$f1596-1650.
$aDescartes$bRene$f1596-1650.
$aDescartes$bRene$f1596-1650
$aDescartes$bRené$f1596-1650
$aDescartes$bRené$f1596-1650.

cluster 3:

$aVoltaire,$f1694-1778. 54666,365421,654564
$aVoltaire,$f1694-1778
$aVoltaire$f1694-1778.

内容的提问来源于stack exchange,提问作者Jeka Tay

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.28 09:30:13