资讯动态

模糊去重实战:算法原理、数据清洗与备份去重配置指南

发布时间:2026/9/10 4:10:22 来源:尧图企业网站定制
你可能遇到过这种情况两份客户名单看起来都是同一家公司但一份写着“深圳市腾讯计算机系统有限公司”另一份写着“深圳腾讯计算机系统有限公司”用传统的精确去重去比较程序会把它们当成两条完全不同的记录。这就是这篇博文要聊的题目——Fuzzy去重也就是模糊去重。我最早接触“Fuzzy去重”不是从教科书里学到的而是一次真实的数据清洗事故。当时要把三个渠道收集的客户信息合并进主数据库我以为用 group by 或者 distinct 就能搞定结果一跑发现重复率不到预期反而多出来几千条“看起来很相似但程序认为不一样”的记录。后面才意识到真实世界的数据从来不是干净、全等的精确匹配只是在理想世界成立。模糊去重解决的就是这个问题在数据存在噪音、格式不统一、轻微错漏的情况下判定两条记录是否代表同一个实体。这篇文章我会从算法原理讲到实际落地覆盖数组去重、对象数组去重、SQL 查询去重、图片去重还有我踩过的备份存储去重配置坑。不管你是做数据清洗、后端开发还是做运维备份都能找到可以直接抄走的方案。1. 为什么需要Fuzzy去重精确匹配解决不了的真问题1.1 数组去重和SQL去重看似够用其实很脆弱程序员最熟悉的去重大概就是数组去重。const arr [1, 2, 2, 3]; const unique [...new Set(arr)];一行搞定速度快、写法优雅。SQL 里的SELECT DISTINCT和GROUP BY也是同理本质上都在做一件事只有当两个值完全相等的时候才判断为重复。这个逻辑在数字和 ID 上没有问题但只要数据进入“人可读”的文本世界精确匹配就开始翻车。我来列举几个我实际遇到过的例子手机号在导入时被 Excel 自动加了空格13800138000和138 0013 8000被识别成两个号码。地址字段有的是“北京市朝阳区”有的是“北京朝阳区”行政区划的层级描述不一致。姓名里出现别名比如“张伟”和“张伟先生”一个括号就导致去重失败。用户在表单里填写“腾讯”和“深圳市腾讯计算机系统有限公司”同一个实体出现两种粒度。这些场景共同点在于字段里包含了同一个实体的信息但因为没有精确相等传统去重方法就把它们放过了。对象数组去重也是一样很多人会按id或者name做 key但一旦不同来源的数据没有同一个主键按 key 去重就直接失效了。1.2 哪些场景必须上模糊去重不是所有去重都需要模糊匹配。如果你处理的是订单流水、用户 ID、日志编号这种强约束字段精确去重就够了用模糊反而会误伤。真正必须上模糊去重的场景通常具备几个特征数据来源复杂、字段由人手工输入或经过 OCR 识别、同一个实体没有统一标识。我按高频程度列一下多渠道客户数据清洗。CRM、财务系统、第三方平台导出名单三份数据合并时同一个人或同一家公司往往没有统一编号。日志和报错聚合。线上服务报错Error message 里夹着时间戳、内存地址、UUID同一个 bug 每次打印的日志都“几乎一样但又不完全一样”精确去重会把一个故障拆成几百条。OCR 与图片识别的结果处理。扫描件转文字后识别错误率通常不低“深圳”可能被识别成“深珊”图片文件本身也可能存在分辨率差异、水印、裁剪等变化。结构化文本库去重。比如商品标题同一款商品不同卖家写的标题可能差异巨大但核心信息品牌、型号、规格是重叠的。新闻资讯与爬虫数据合并。同一条新闻源被不同站点转载标题往往被修改正文也有段落调整需要按内容相似度判断。这类需求的共同点不是“两条数据是否完全一样”而是“两条数据是否在表达同一个事物”。这就是模糊去重存在的意义。1.3 Fuzzy去重的本质把“全等判断”升级为“相似度判断”模糊去重的核心思路其实很简单可以拆成三步把对象抽象成可比较的表示用相似度算法量化两条记录的相似程度设定阈值和匹配策略决定什么程度算是重复。用生活类比来解释精确去重像是在问“你身份证号是多少”只要号码不一致就不是同一个人模糊去重像是在问“你是不是同一个人”它会看你的五官轮廓、身高体重、说话方式综合判断允许一定范围内存在差异。做模糊去重的人本质上就是在设计一套“看起来像不像”的评判标准。实际操作中模糊去重有两条路线一条是局部匹配比如判断 A 是否近似包含在 B 里另一条是整体相似度衡量两条记录的整体一致程度。选哪条、用什么算法、阈值定多少都得根据数据特征来。这部分是模糊去重的技术核心下一章详细展开。2. 核心算法拆解相似度从哪来2.1 编辑距离与字符级相似度最常见的模糊匹配算法就是编辑距离Edit Distance尤其是 Levenshtein 距离。它计算的是把一个字符串变成另一个字符串最少需要多少次单字符插入、删除、替换。比如把kitten变成sitting需要把 k 改成 s把 e 改成 i最后加一个 g一共 3 次操作所以 Levenshtein 距离是 3。它的直观含义很好理解代码也不难写但直接使用距离值有一个问题长度不同的字符串绝对值没有可比性。比如两个长度 100 的字符串差 2 个字符和两个长度 5 的字符串差 2 个字符后者明显差异更大。所以工程上一般会做归一化把距离转成相似度常见公式是similarity 1 - distance / max(len1, len2)。这样两边长度都接近 100、差 2 个字符时相似度约为 0.98两边长度 5、差 2 个字符时相似度只有 0.6更符合人的直觉。比 Levenshtein 更进一步的是 Damerau-Levenshtein 算法它在增删改之外还允许相邻字符的转置。手写输入场景里“teh”和“the”这种错误特别常见用带转置的算法能更好处理。字符级编辑距离适合短文本比如姓名、电话、地址简写我实际测试下来在中文短字段上效果不错但长文本上性能会迅速恶化因为动态规划的时间复杂度是 O(m×n)。2.2 集合相似度与N-gram处理中长文本时字符级编辑距离计算量太大集合相似度是更实用的方向。最经典的是 Jaccard 相似度公式是交集大小除以并集大小把两条文本分别拆成集合集合重叠越多相似度越高。但直接按整句或者单词拆集合对中文并不友好。中文没有天然的空格分词所以实践中我用得最多的是 N-gram尤其是二元语法bigram。把“模糊去重”拆成 2-gram就是“模糊”“糊去”“去重”把“模糊查重”拆成 2-gram就是“模糊”“糊查”“查重”。两者有一个公共gram但整体重叠率不高Jaccard 值会明显小于全等文本这个差异恰好反映了语义差异。N-gram 的粒度选择是个经验活。2-gram 对中文字符级别的顺序变化非常敏感适合处理错别字少的场景3-gram 抗噪能力强一些但对短字段会过于宽松。实践里我会对同一批数据分别计算 2-gram 和 3-gram 的 Jaccard再结合编辑距离做综合打分。在更复杂的场景中可以用 TF-IDF 把文本转成向量再用余弦相似度比较。这种方式相当于给每个词加了权重常见词权重低特征词权重高。比如“公司”这种词在几乎所有企业名里都出现权重很低“腾讯”这种词权重很高两个文本都含“腾讯”时相似度提升明显。这个思路适合标题查重、资讯去重这类长文本场景。2.3 海量数据下的SimHash与MinHash两层两两比较的时间复杂度是 O(n²)当数据量到几万条以上时就不太现实了。比如 18 万条地址数据两两比较要计算约 16 亿次相似度Python 单线程跑要几十天完全没法上线。这时候需要能快速缩小候选集的方法。SimHash 是 Google 用来做网页去重的经典方案。思路是把文档映射成一个 64 位指纹两个文档的海明距离越小相似度越高。SimHash 的巧妙之处在于它把“局部修改不影响整体概要”做了数学化表达一段文本改几个字得到的指纹只会在少量位上变化。实际使用中我一般用海明距离小于等于 3 作为判定阈值效果比较稳定。MinHash 和 LSH局部敏感哈希则是另一类做法。MinHash 用多个哈希函数对集合做签名同一个集合无论怎么打乱元素顺序签名都基本一致适合 Jaccard 相似度的大规模近似计算。配合 LSH 可以把“可能相似的候选对”分到同一个桶里只在桶内做精确两两比较能把计算量降低一到两个数量级。这套方案前期实现成本不低但如果你处理的是百万级以上的文本数据这笔投入非常值得。我的建议是数据量小于 1 万直接两层循环加编辑距离就行1 万到 20 万用 N-gram 倒排索引先过滤候选集超过 20 万再考虑 SimHash 或 MinHash。2.4 相似度阈值这个参数比算法还关键很多新手做模糊去重时总觉得算法越高深越好实际上真正决定准确率的是阈值。我见过太多次误判都源于“阈值拍脑门”定了 0.8 或 0.9。阈值并不是一个通用常量。姓名去重的场景里两个字的名字“王涛”和“王涛(先生)”相似度可能不到 0.8但确实是同一个人而长篇新闻正文哪怕只改了几个句子相似度可能还是高达 0.95。因此阈值必须结合数据长度和业务语义去定。我常用的定阈值方法是抽样标注法从数据里随机抽 300 到 500 条人工标注哪些该合并哪些不该合并然后把这批数据跑一遍相似度计算画出相似度的概率分布图。你会发现通常存在一个“灰色地带”比如相似度 0.7 以下绝大多数该分开0.9 以上绝大多数该合并中间区域则是人肉审核区。最终我会设置双阈值高阈值自动合并中间段落入人工审核队列低阈值直接忽略。这套流程虽然前期费点功夫但能省掉后面海量的人工校对时间。3. 四种落地姿势数组、对象数组、SQL、图片3.1 数组文本模糊去重RapidFuzz快速实现先从一个最常见的需求说起一个字符串数组里有不少内容近似重复的文本需要把相似度高于阈值的条目视为同一条。Python 生态里最顺手的是 RapidFuzz它是老牌库 fuzzywuzzy 的 C 重写版速度快、内存占用低接口也基本兼容。我写了一个简单的数组文本去重代码思路是两层循环计算相似度高于阈值就把后者标记为重复并挂到前者下面from rapidfuzz import fuzz texts [ 深圳市腾讯计算机系统有限公司, 深圳腾讯计算机系统有限公司, 腾讯科技深圳有限公司, 北京市朝阳区建国路93号, 北京朝阳区建国路93号, ] threshold 80 merged {} used [False] * len(texts) for i in range(len(texts)): if used[i]: continue merged[texts[i]] [] for j in range(i 1, len(texts)): if used[j]: continue ratio fuzz.ratio(texts[i], texts[j]) if ratio threshold: merged[texts[i]].append(texts[j]) used[j] True for key, dup_list in merged.items(): if dup_list: print(key, -, dup_list)这段代码本身不难但有几个优化点需要说明。第一fuzz.ratio是全字符串相似度适合比较结构完整的文本如果你的数据是“长文本里包含关键信息”建议换fuzz.partial_ratio或fuzz.token_sort_ratio。第二阈值设成 80 还是 90 差别很大建议先用样本数据观察分布再定。第三两层循环在数据量大的时候非常慢可以先按“首字”或者“长度区间”分桶只在桶内做比较。3.2 对象数组去重多字段加权聚类合并真实业务里数据通常是一条条对象比如[ {name: 深圳市腾讯计算机系统有限公司, address: 深圳市南山区海天二路33号, phone: 0755-86013388}, {name: 深圳腾讯计算机系统有限公司, address: 深圳南山区海天二路33号, phone: 0755-86013388} ]如果只看 name 字段上面两条很像只看 phone 字段则完全相同。对象数组去重的关键是从多个字段综合判断而不是拿单一字段硬碰。我的做法是给每个字段分配权重。比如 name 权重 0.5address 权重 0.3phone 权重 0.2最后综合得分 0.5×name相似度 0.3×address相似度 0.2×phone相似度。实现时可以把每个字段的相似度计算封装成单独函数方便以后调整权重from rapidfuzz import fuzz def record_similarity(a, b): name_score fuzz.ratio(a[name], b[name]) / 100.0 addr_score fuzz.ratio(a[address], b[address]) / 100.0 phone_score fuzz.ratio(str(a[phone]), str(b[phone])) / 100.0 return 0.5 * name_score 0.3 * addr_score 0.2 * phone_score阈值同样不是拍脑袋定。我习惯先把所有候选对的综合得分算出来观察分布再定合并线。实际项目中我遇到过 phone 识别合格的记录一条但 name 和 address 差别很大如果不做加权单靠 phone 会把两个完全不同的公司合并成一条因为公司换了负责人和地址但业务联系电话沿用的情况很常见。所以权重的设定要与业务强绑定最好做成可配置项。合并时还要注意字段的保留策略。通常按数据源的信誉度和时间戳排序人工录入优于自动抓取最新的覆盖旧的字段为空的让另一个补充。同时一定要保留合并轨迹记录“哪几条 ID 被合并进了哪条主记录”否则审计和回滚都会很痛苦。3.3 SQL语句中的模糊去重查询聊完应用层再看数据库层。SQL 里的DISTINCT和GROUP BY都是精确去重要做模糊去重必须借助特定函数或扩展。PostgreSQL 里最方便的是 pg_trgm 扩展它基于三元组trigram做相似度计算直接能返回一个 0 到 1 的相似度分数CREATE EXTENSION IF NOT EXISTS pg_trgm; SELECT a.id, b.id, similarity(a.name, b.name) AS sim FROM records a JOIN records b ON a.id b.id WHERE similarity(a.name, b.name) 0.8;MySQL 自带函数里没有现成的 Levenshtein 实现但可以通过自定义函数或存储过程实现逻辑并不复杂。不过我的经验是不要把高强度的模糊匹配完全压到数据库里。自连接产生的候选对数量是 O(n²)即使数据库性能不错一旦到了百万级数据查询会拖垮整个库。更合理的架构是先用 SQL 做一个粗筛比如把长度接近、首字符相同的记录选出来再在应用层用向量和索引做精细的模糊比较。SQL Server 提供了DIFFERENCE和SOUNDEX它们基于发音相似度适合英文姓名类字段。中文场景下效果一般我不建议用它们做中文业务数据的模糊去重最多做辅助排序字段。3.4 图片去重感知哈希的实战用法很多人一提到去重就联想到文本实际上图片去重在素材库、版权排查、电商图库场景里非常常见。早期我用 MD5 或 SHA1 直接比对文件哈希结果发现同一张图片只要换个分辨率、加个水印、微调一下亮度哈希就完全不同去重完全失效。图片模糊去重的标准方案是感知哈希Perceptual Hash核心思路是把图像缩小到固定尺寸转换成灰度图提取一个“结构指纹”。常用算法有 aHash平均哈希、pHash感知哈希、dHash差异哈希它们的逻辑大同小异我用 dHash 最多因为计算快、对缩放和水印的鲁棒性不错。大概流程是把图片缩放到 9x8 像素转灰度比较相邻像素的亮度差异得到 64 位二进制哈希。两张图片的哈希海明距离小于 5就基本可以判定为同一张图的近似复制。用 Python 实现的话Pillow 配合 imagehash 库非常方便from PIL import Image import imagehash hash1 imagehash.dhash(Image.open(photo1.jpg)) hash2 imagehash.dhash(Image.open(photo2.jpg)) diff hash1 - hash2 # 海明距离 print(相似度差值:, diff) if diff 5: print(判定为近似重复图片)这类实现很多图片去重工具比如“米牛图片去重大师”就是这个思路。它扫描目录里的所有图片批量计算感知哈希然后把距离小于阈值的图片挑出来给你确认。如果你要自己实现一个类似功能别忘了第一步先做尺寸缓存把每张图缩放到统一大小再算哈希否则性能会非常难看。4. 一个容易被忽视的决策Veeam与Data Domain集成时的去重开关4.1 备份链路里的两级去重文本和图片的模糊去重聊完了再来一个我踩过的存储备份领域的坑。Veeam 是常用的虚拟机备份软件Data Domain简称 DD是 Dell EMC 的重复数据删除存储设备很多企业把 Veeam 备份作业指向 DD 作为目标存储。问题来了Veeam 自己带源端去重压缩DD 也带内联去重两层的去重叠在一起真的能“好上加好”吗先说背景。Veeam 的去重是源端去重在备份代理机读取虚拟机数据后直接对数据块做哈希识别相同的块只传一次目的是节省网络带宽和备份窗口。DD 的去重是目标端去重数据落到 DD 后DD 会再做一次内容定义分块和指纹比对只存储唯一的数据块。从功能上看两层都在做去重但它们的算法、块边界和指纹库都是各自独立的并不会因为 Veeam 已经去重就让 DD 的去重变快反而可能出现负优化。4.2 为什么目标端已是DD时建议关掉Veeam去重我在集成测试里发现Veeam 开启去重压缩再往 DD 写最直接的影响是备份代理的 CPU 占用很高任务耗时也会变长。而且 DD 收到的数据如果已经被 Veeam 重新组织过数据块的连续性和原始模式会被破坏DD 自身的指纹命中率反而可能下降最终存储占用并没有显著变少甚至还可能变大。整体算下来初期省下的带宽在中后段被性能和压缩率损失抵消掉了。社区里很多最佳实践文档同样建议当备份目标已经使用 DD 这种具备高等级去重能力的存储时建议在 Veeam 侧关闭去重压缩把去重的工作统一交给 DD 完成。这里的关键是“不要让两个去重引擎互相打架”。你可以把 Veeam 的去重想象成给快递先做了一次压包再把压过的包裹交给专线运输专线时又要拆包重新装车来回折腾反而耗时。当然这不是绝对规则。如果你的链路里网络带宽极其紧张比如跨站点备份源端必须先做一次去重压缩才能把数据传过去那源端去重依然有意义。这种情况下就要做测试找到一个两端都相对合理的平衡点。4.3 怎么验证该关谁的去重一组对照测试拿我做过的一个项目举例环境是一台 Veeam 备份服务器后面接一台 DD3300。我先保留 Veeam 默认的去重压缩模式跑了一个全量备份加增量记录了备份耗时、CPU 峰值、传送到 DD 的数据体积以及 DD 侧最终存储占用。然后我把 Veeam 备份作业的“去重压缩”选项关闭只保留传输再跑同样的备份任务对比两组数据。结果大致是这样开启 Veeam 去重时全量备份耗时 2 小时多一点DD 侧显示逻辑备份数据量约 230GB实际存储约 90GB关闭 Veeam 去重后备份耗时降到了 1 小时 40 分DD 侧显示收到约 240GB 的逻辑数据实际存储约 70GB。虽然网络侧多传了约 10GB 数据但备份窗口更短DD 的最终空间占用反而明显下降。原因就是前面说的Veeam 和 DD 各自分块算法不一致Veeam 预处理过的数据流打乱了 DD 的指纹识别模式导致 DD 去重率降低。所以我在生产环境的建议是如果目标端是 DD 这类专用去重存储优先关闭 Veeam 的去重压缩选项如果没有 DD只有普通磁盘或 NAS那 Veeam 源端去重就得保留。评估标准不是单看某一层的产物大小而是端到端的“备份耗时 最终存储占用 恢复速度”综合评分。5. 常见问题与排查技巧实录5.1 误判率偏高阈值不是越高越好要看文本长度模糊去重最常见的翻车现场是误判率过高。我遇到过一个案例阈值定在 0.85结果把“北京市丰台区”和“北京市丰台区科技园”判成了同一条记录。单看相似度两者确实超过 0.85但业务上“丰台区”和“丰台区科技园”可能是完全不同的地点。问题出在短文本对相似度非常敏感长度只有几个字的字符串几个字符的差异对整体相似度影响被稀释了。我的对策是引入长度因子。文本越短判定为重复所需的阈值越高。具体做法是把阈值公式改成动态阈值比如dynamic_threshold base_threshold min(len1, len2) * 0.005或者直接用一条经验规则长度小于 6 的字段阈值不低于 0.95长度大于 50 的字段阈值可以降到 0.8。用动态阈值之后误判率至少降低一半。另外每次判定的时候我都会把相似度分数记录进日志方便事后回溯。如果业务方反馈“你把我两条不同数据合错了”我可以直接查出当时两条记录的相似度分数快速定位是阈值设置问题还是数据本身太像。5.2 计算量爆炸先缩小候选集再两两比较第二个高频问题是性能。很多人一开始会用最朴素的双层循环数据量一上来就扛不住。我前面提到过 18 万条地址数据的两两比较要 16 亿次这个听起来很吓人但实际解决思路并不复杂先缩小候选集再做精细匹配。缩小候选集的做法叫 blocking也叫分块。最简单的分块是取首字符或者前两个字作为桶键只有同一个桶内的记录才做两两比较。地址数据还可以按行政区划代码分桶姓名数据可以按姓氏分桶电话数据可以按前三位号段分桶。加了首地址分块之后我在那个 18 万条的项目里把候选对从 16 亿降到了约 800 万再配合多进程并行几小时就跑完了。如果分块后候选集还是太大可以用倒排索引做 N-gram 过滤把每条文本拆成 2-gram建立 gram 到文档 ID 的倒排表只比较至少共享一个 gram 的文档对。这一招在文本查重场景里特别有效因为完全不共享任何 2-gram 的两条文本相似度大概率很低直接跳过没有损失。5.3 中文文本清洗的几个坑中文模糊去重比英文麻烦得多主要原因是中文没有天然分隔符而且格式层面容易埋雷。第一是半角全角数字、字母、标点都可能有全半角差异比如“”和“ABC”看似一样字符编码完全不同必须统一转成半角。第二是繁简体企业名称和法律条文里经常混用建议先统一转简体再比较。第三是中英文混排的空格问题“北京市 Beijing”和“北京市Beijing”如果直接做 n-gram可能会产生多余的空格差异比较前最好也把空格规范掉。清洗规则虽然琐碎但收益非常大。我通常把清洗做成管道式函数字符串类型转换、全角转半角、繁转简、去除首尾空格、压缩连续空格、统一大小写。先清洗再计算相似度数据质量会稳定很多。还有一个容易忽略的细节中文的“省”“市”“区”这类行政区划后缀有的数据源省略、有的保留直接参与相似度计算会拉低分数。如果业务上确定这些前缀后缀不影响判定可以考虑把它们作为可忽略词处理或者用带权重的 token 比较方式。5.4 对象合并策略留谁、删谁、如何溯源最后一个常见坑是合并策略没有预案。模糊去重找到重复记录后总要保留一条主记录把其他记录合并进来这时候就会碰到“留谁”的问题。如果只是简单保留数组里第一条可能留下一条字段残缺、来源最差的记录后面业务调用时会出各种问题。我的经验是按三个优先级决策第一优先是时间戳保留最近更新的记录第二优先是数据源质量人工录入和官方接口的数据优先级高于爬虫和手工导入第三优先是字段完整度保留非空字段最多的那条。三个规则可以组合成打分公式综合得分最高的记录作为主记录其余记录挂到主记录的合并列表中。同时我强烈建议记录合并轨迹。每个对象加一个source_ids字段把被合并掉的原始 ID 全部存进去再写审计日志。这样一旦业务方质疑数据被改错你能清楚看到“这条记录在什么时间、基于什么规则、由哪几条原始记录合并而来”。没有溯源机制的模糊去重本质上是不可维护的脚本后续会变成数据质量黑洞。写在最后的几点体会做了几年数据清洗和备份运维我最大的体会是模糊去重从来不是算法问题而是决策问题。相似度算法随手就能拉开源库实现难的是确定“像到什么程度就算是同一条记录”“合并时保留谁的字段”“被合并掉的数据如何溯源”。如果只是写脚本一次性跑完怎么折腾都行一旦要长期运维一定要把阈值、算法、合并策略全部参数化、可回放否则业务方质问你为什么把两条数据合错的时候你连解释的依据都没有。一个小技巧送给你在跑模糊去重之前永远先用精确去重把完全相同的记录清一遍。两个集合分开处理能省掉一大半无效计算也给后面的人工审核留出缓冲空间。模糊去重的价值不是把数据变干净而是在不可避免的噪声里依然能认出“这其实是同一个人、同一个公司、同一张图、同一份数据”。认出来之后怎么处理才是真正考验架构和业务理解的地方。

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

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

免费获取报价