模糊去重(Fuzzy Dedup)从算法原理到工程落地全解析
2026/9/16 5:02:29 网站建设 项目流程

我是在一次报表合并时被“Fuzzy 去重”这个词打醒的。销售系统导出的客户名单要跟 CRM 主数据做关联,三张表里“张三”“张 三”“ZHANG SAN”看着完全是三条记录,业务上却是同一个人。当时我最先想到的去重方式很快被证明没用——标准的精确去重要求字段完全一致才保留一条,结果一条重复都没标出来。后来才明白,这类场景要的不是“相等判断”,而是“相似度判断”,这就是模糊匹配去重,也是很多数据清洗、内容查重、备份存储场景背后真正在用的东西。这篇文章我把 Fuzzy 去重从算法原理到工程落地完整拆一遍,覆盖数组/SQL 清洗里的精确去重、备份系统的字节级去重、视频查重的感知哈希,以及一套能直接跑的 Python 模糊去重脚本,适合正在做数据治理、后端清洗或者内容系统的朋友参考。

1. 先分清你是哪种去重:五个场景可能说的不是同一件事

“去重”这个词在开发里到处都是,但不同语境下它解决的问题差得很远。早年我在公司内部做过一次技术分享,标题就叫“去重简史”,现场让大家举手说自己做的去重是哪种,结果前端、后端、算法、运维各说各话。所以聊 Fuzzy 去重之前,必须先分清你所在的场景属于哪一类。

1.1 数组去重和 C 语言题里的“去重”是精确匹配

最基础的去重是编程题里那种:给一个数组,去掉重复元素。前端会写[...new Set(array)],C 语言就手写双重循环或者先排序再相邻比较,对象数组可能用Map以某个 id 字段做键。这套思路的核心是“完全相等”,比较的是值本身,要么===,要么strcmp返回 0,判定逻辑非常简单。

问题是,这种去重到了真实业务数据面前几乎不堪一击。因为现实数据里的“重复”很少长得一模一样:两个字段可能只差一个空格,可能全角和半角不一样,可能是繁体简体混用,甚至可能是同一个人的姓名在不同系统里写法不同。数组去重解决不了这些,它只适合代码内部对集合元素做规范化处理这类限定场景。

1.2 SQL 清洗里的去重:精确匹配是常规动作

比数组去重更常用的是 SQL 层的去重。面试里最常见的三板斧:DISTINCTGROUP BY、还有窗口函数ROW_NUMBER() OVER(PARTITION BY ... ORDER BY ...)。做数仓清洗时,聚合去重基本都是这个套路。这类操作处理的是“键完全一致的重复行”,性能挺好,语义也很明确。

但 SQL 精确去重有个天然短板:它不认识“脏数据”。比如名字列里有一行是“张三”,另一行是“张 三”,中间多了个空格;或者手机号一列是138 0013 8000,一列是13800138000。用DISTINCT去重,这些统统算两条。这时候 SQL 工程师就会很痛苦——加正则表达式清洗可以处理简单的空格和全半角问题,但遇到“王晓明”和“王小明”这种同音不同字,或者两家系统里同一个地址表述完全不同,SQL 的基本函数就无能为力了。

这种场景里,你需要把数据拉到应用层,用模糊匹配算法做相似度判定,再决定哪些记录应该合并成一组。这就是 Fuzzy 去重最典型的用武之地。

1.3 备份系统的块级去重:存储层在做“字节级模糊”

还有一个容易被忽略的去重场景在备份存储里。大家用的备份软件、重删设备,比如 Veeam 配 Data Domain 做备份归档,底层都会做“重复数据删除”,英文叫 deduplication。这里的去重对象不是表记录,而是文件里的数据块。

存储层去重和业务层去重有个关键区别:它不是做“逻辑相等”,而是在字节流上做“指纹匹配”。Data Domain 把数据切成不定长的块,对所有块计算哈希指纹,如果发现新来的块指纹和已有的块指纹一致,就不再落盘,只记录一个引用。所以同样的数据哪怕换个文件名、换个目录、时间戳变了,只要块内容没变,照样能识别出来重复。这其实是非常底层、非常严格的一致性判断,比字符串相似度严苛得多。

这里就牵扯到网上那个很热的说法:“Veeam 和 Data Domain 集成时建议不开 Veeam 的去重”。原因后面第 5 章我会单独讲,本质上就是:当底层重删设备已经做了块级去重之后,如果上层备份软件再做一层自己的去重,两者的切块方式不统一,反而会把底层的去重率拉低。

1.4 内容平台的“近似查重”:你不逐字节比,靠感知

近两年短视频行业的去重热度很高,很多人做视频剪辑、发多平台内容,会找“视频去重软件”。这跟前面几种又不一样。平台方做内容查重,面对的是成百上千万条视频,MD5 这种精确哈希基本没用——因为同一个视频换个编码、加个滤镜、裁剪掉一截边缘,MD5 就完全不同了。

所以平台用的是感知哈希(Perceptual Hash)。它把视频画面缩放到固定尺寸,计算每个像素块的亮度关系,生成一个可以互相比较的指纹;只要两条视频指纹的汉明距离小于阈值,就判定它们“感知上重复”。这里的“模糊”是视觉意义上的,不需要一个字节相同,但人眼看着像,算法就认为重复。

从数组去重到 SQL 去重,再到备份存储和视频查重,你会发现“去重”其实是一个谱系:最左端是精确相等,最右端是感知相似。而 Fuzzy 去重通常指中间这一段——用相似度算法来处理那些“逻辑上是同一件事、字面上不完全相同”的数据,这也是本文要聊的重点。

2. Fuzzy 的三种核心算法:编辑距离、n-gram 相似度与感知哈希

模糊去重不是某个单一算法,而是一族相似度度量方法。实际选型时候的常见误区是“拿一个算法打天下”,但其实文本长度不同、脏数据形态不同,适用的算法完全不同。我按适用场景由小到大,把最常用的三类拆开讲。

2.1 Levenshtein 编辑距离:处理错别字和格式碎片

Levenshtein 距离的定义很朴素:把一个字符串变成另一个字符串,最少需要多少次插入、删除、替换操作。比如“小猫”变成“小猫咪”,需要插入一个“咪”,距离就是 1;“西安”变成“西-安”,距离也是 1,只是多了一个字符的差异。

计算方式常用动态规划。假设两个字符串长度分别为 m 和 n,开一个 (m+1)×(n+1) 的矩阵,状态转移就是三个操作里取最小:

def levenshtein(a: str, b: str) -> int: m, n = len(a), len(b) dp = [[0] * (n + 1) for _ in range(m + 1)] for i in range(m + 1): dp[i][0] = i for j in range(n + 1): dp[0][j] = j for i in range(1, m + 1): for j in range(1, n + 1): cost = 0 if a[i - 1] == b[j - 1] else 1 dp[i][j] = min( dp[i - 1][j] + 1, # 删除 dp[i][j - 1] + 1, # 插入 dp[i - 1][j - 1] + cost # 替换 ) return dp[m][n] def similarity(a: str, b: str) -> float: d = levenshtein(a, b) return 1 - d / max(len(a), len(b))

编辑距离对短字符串特别友好。姓名、手机号、公司名这种字段,通常就几个到十几个字符,DP 开销完全可以忽略。实际用的时候有个关键细节:绝对距离不好用,要归一化。比如“A”“B”距离是 1,“ABCDEFG”“ABXDEFG”距离也是 1,但后者的相似度显然更高。所以我习惯用1 - d / max(len(a), len(b))这种相似度形式,再配合阈值判断。

注意 Levenshtein 处理的是字符层面的差异,它对“词语顺序颠倒”这种问题很无力。“北京朝阳路”和“朝阳路北京”编辑距离很大,但人类一眼就知道说的是同一类地址信息。

2.2 集合相似度与 shingling:长文本更抗局部变动

文本越长,编辑距离的计算成本越不可控,而且它对“局部插入、删词”极其敏感。比如地址字段“北京市朝阳区建国路 88 号”和“北京市朝阳区建国路 88 号院”,编辑距离是 1,还行;但如果是“北京市朝阳区建国路 88 号 3 号楼 502 室”和“北京市朝阳区建国路 88 号 3 号楼 502”,长度差了 3 个字符,归一化后的相似度就明显下降。

更长文本更适合用 n-gram + Jaccard 相似度。思路是把句子切成连续的 n 个字符片段(shingle),然后计算两个集合的交集和并集比例:

def char_ngrams(s: str, n: int = 2): return {s[i:i + n] for i in range(len(s) - n + 1)} def jaccard(a: str, b: str) -> float: A, B = char_ngrams(a), char_ngrams(b) return len(A & B) / len(A | B)

同样拿上面两个地址来比,bigram 集合里“北京市”“京市朝”“市朝阳”“朝阳区”“阳区建”“区建国”“建国路”“国路”这些片段大部分都在,Jaccard 值会比编辑距离稳健很多。

这条路径对中文特别友好,因为中文不像英文有天然空格分词,按字符切 n-gram 是最简单且不需要额外词典的做法。当然如果场景是英文,通常先用空格分词再切词 n-gram 会更好。n 的取值也有讲究:n 太小,集合里全是常见字符组合,任何两句中文都很像;n 太大,又过度敏感。经过多次实测,中文短文本用 bigram 最稳,长文本用 trigram 更好。

2.3 SimHash 和感知哈希:给海量文本与视频做“指纹”

如果数据量到了百万、千万级别,不管是编辑距离还是 Jaccard 都撑不住两两比较。这时候要用哈希指纹 + 汉明距离的思路,SimHash 是文本领域最经典的做法。

SimHash 的核心过程是降维:先把文本分词,每个词算一个哈希值;然后对每一位,根据当前词是否在该位为 1 做加权累加;最后把所有累加结果正负转成 0/1,得到一个 64 位指纹。两个文本越相似,它们的 64 位指纹的汉明距离就越小。实际工程里,汉明距离小于等于 3 通常就可以认为近似重复。

感知哈希本质上也是这个思路,只是输入从文本换成了画面。视频查重工具会把视频帧缩放到固定尺寸(比如 8×8 或 32×32),对像素亮度做哈希,生成指纹;同一段视频哪怕缩放、裁剪、加个水印、调个色调,指纹仍然接近,汉明距离照样在阈值以内。

这三类算法并不互斥。我在实际项目里的组合方式是:短字段用编辑距离,长文本用 n-gram Jaccard,大规模数据做第一道粗筛用 SimHash,最后用精确方法复核。模糊去重的核心不是复现某一个算法,而是组合出一套合适你的“比对链路”。

3. 动手跑一个真实场景:客户信息表的模糊去重脚本

理论讲完,现在上一段能直接落地的代码。我用一个非常典型的场景:两张表合并后,发现同一个人可能有不同写法,比如姓名多了空格、手机号格式不一致、地址表述不同。目标是识别出哪些是同一个客户,并给出一个可以回写业务库的分组结果。

3.1 数据准备和归一化:去空格、全半角、繁简转换

不管用什么相似度算法,第一步永远是归一化。这一步做得好,后面算法压力小一半。我见过很多新人上来直接跑 fuzz,结果因为一个全角空格,相似度被拉低一大截。

归一化我至少会做四件事:去首尾和中间空白、全角转半角、统一大小写、能转繁体就转简体。Python 里全半角可以结合unicodedata.normalize来做,里面还顺带处理了带声调的字符分解问题。示例数据如下:

records = [ {"id": 101, "name": "张三", "phone": "138 0013 8000", "city": "北京市"}, {"id": 102, "name": "张 三", "phone": "13800138000", "city": "北京"}, {"id": 103, "name": "王小明", "phone": "13912345678", "city": "上海市"}, {"id": 104, "name": "王晓明", "phone": "139 1234 5678", "city": "上海"}, {"id": 105, "name": "李四", "phone": "13700001111", "city": "广州市"}, ]

归一化函数可以这样写:

import re import unicodedata def normalize_text(s: str) -> str: if not isinstance(s, str): s = str(s) # 全角转半角 s = unicodedata.normalize('NFKC', s) # 去掉所有空白字符 s = re.sub(r'\s+', '', s) # 统一小写(英文场景有用,中文不影响) s = s.lower() return s

如果在真实业务里遇到繁体数据,建议加一个opencc或者zhconv库做繁简转换,效果立竿见影。注意 NFKC 会把全角数字“138”转成半角“138”,还能处理一些特殊 Unicode 字符,这个对手机号清格特别有用。

每一条记录处理完,我会额外新增一个norm_phone字段,统一后用于后面的分桶键。

3.2 用联合字段避免 O(n²) 爆炸

“两两全部比较”是新手最容易踩的坑。1 万条数据两两比较就是 4999.5 万次调用,哪怕单次耗时 0.1 毫秒,也要超过 8 分钟。要避免这个坑,不能只靠一对一比对,必须先分桶。

常见做法是选一个“重复对内大概率相同、不同记录间又能区分开”的字段做阻塞键。对客户表来说,手机号后 4 位是个不错的候选:同一人即使手机号前缀变了,后 4 位往往不变;不同人撞后 4 位的概率大约千分之一左右。

def build_groups(records): buckets = {} for r in records: norm_phone = normalize_text(r["phone"]) if len(norm_phone) >= 4: key = norm_phone[-4:] else: key = norm_phone buckets.setdefault(key, []).append(r) return buckets

然后只在同一个桶内部做相似度比对。这样 1 万条如果平均 20 条一个桶,总共 500 个桶,比对次数大概只有 9 万次左右,比全量少 500 倍。分桶这一步是模糊去重里性价比最高的优化,没有之一。

3.3 RapidFuzz 计算姓名、地址相似度,再用并查集合并

分桶之后,桶内数据量通常已经很小了,这时可以放心用 RapidFuzz 库(内部有高度优化的 C 实现,比单纯手写 Levenshtein 快得多)。

from rapidfuzz import fuzz def compute_similarity(a: str, b: str) -> int: # 返回值的取值范围是 0-100 return fuzz.ratio(normalize_text(a), normalize_text(b))

fuzz.ratio对地址、长句这种场景如果不够稳,可以换fuzz.token_sort_ratio,它会先把字符串分词排序再比较,能解决字段里词序颠倒的问题。但要注意,中文姓名场景下分词排序意义不大,主要还是靠ratio或者partial_ratio

相似度算完,接下来最重要的一步是分组合并。如果只做“两两判定”,会出现一个问题:A 和 B 相似、B 和 C 相似,但 A 和 C 不像;如果把 A、B 归为一组,C 又跟 B 是一组,最终 C 应该并入 A 这组吗?工程上最直接的解法是用并查集(Union-Find),把相似度超过阈值的两条记录不断合并进同一个集合,最终每个集合就是一个去重组。

class UnionFind: def __init__(self, n): self.parent = list(range(n)) self.size = [1] * n def find(self, x): while self.parent[x] != x: self.parent[x] = self.parent[self.parent[x]] x = self.parent[x] return x def union(self, a, b): ra, rb = self.find(a), self.find(b) if ra == rb: return if self.size[ra] < self.size[rb]: ra, rb = rb, ra self.parent[rb] = ra self.size[ra] += self.size[rb]

判断逻辑完整代码如下:

def fuzzy_dedup(records, threshold=85): uf = UnionFind(len(records)) buckets = build_groups(records) for bucket in buckets.values(): n = len(bucket) for i in range(n): for j in range(i + 1, n): ni = normalize_text(bucket[i]["name"]) nj = normalize_text(bucket[j]["name"]) score = fuzz.ratio(ni, nj) if score >= threshold: uf.union(bucket[i]["id"], bucket[j]["id"]) # 整理分组结果 groups = {} for r in records: root = uf.find(r["id"]) groups.setdefault(root, []).append(r["id"]) return groups

上面代码里union(bucket[i]["id"], bucket[j]["id"])传的是记录的 id,UnionFind 内部初始化为 n 个节点,所以这里要求 id 和数组下标对齐;如果 id 是字符串或者不连续,建议先把记录映射成连续下标,或者在 UnionFind 里用字典维护 parent。用并查集并不是因为模糊相似度在数学上满足传递性,而是工程上它能把连锁关系合并成一个组,避免同一条记录同时归属多个组。

3.4 调阈值和复核机制:宁可漏,不要错

阈值怎么定,这是模糊去重里争议最大的地方。经验值方面,短字段(姓名、品牌名)相似度 85 以上基本可以判重,90 以上很稳;长文本地址建议放到 75 到 80;而 SimHash 汉明距离小于等于 3 才比较可信。

不同阈值的效果差异,我整理成一张参考表:

阈值召回表现误判表现适用场景
60-70几乎全召回,但大量不同记录被误合并误判严重,业务上难接受只做候选生成,不做最终判断
75-80能查出“同一个地址但写法不同”少量误判,需要人工复核地址、长文本场景
85-90只合并“非常明显重复”的记录误判很少姓名、短文本、电话核对场景
90+漏掉很多真实重复几乎没有误判高可靠要求的最终确认

我的原则是“宁可漏,不要错”。去重的结果通常会影响主数据、标签、报表,误合并两个真实客户带来的业务影响,远比漏合并严重得多。所以线上阈值我会定得保守一些,宁可让一部分重复数据待查,也不让系统自动把两个不同的人捏成一个。

另外要留一个人工复核队列。凡是相似度落在 70 到阈值之间、但没到自动合并门槛的,丢到待确认列表里,由运营或者审核人员人工处理。模糊去重在真实系统里永远不是纯自动化的仪式,它本质上是把“机器处理不了的那一小部分”筛出来交给人类。

4. 上了生产之后:性能、误杀和增量更新

跑通一个脚本只是开始。一旦数据量从几千条涨到几百万条,或者每天新增几万条,模糊去重的工程挑战就会从“算法选型”变成“系统设计”。这里我把生产环境里最常遇到的四个问题逐个讲透。

4.1 两两比较为什么不行:一组数字说明问题

先说个更具体的测算。假设你有 10 万条记录,如果不用分桶直接两两比较,需要计算的相似度次数是 C(100000,2),约等于 50 亿次。RapidFuzz 再快,一次调用也要几十微秒,50 亿次就是几个小时起步。这在离线批处理里也许还能忍,但没有哪个业务系统愿意等这么久。

分桶之后情况完全不同。假如按手机号后 4 位分桶,理论上能分出约 10000 个桶,平均每个桶 10 条数据。每个桶内部比较 45 次,总次数是 45×10000,等于 45 万次,比全量少了近 1.1 万倍。我经手过的项目里,一次 5 万条的客户表去重,从全量比较的 2 小时降到分桶后的 11 秒,就是这一步的收益。

4.2 阻塞字段的挑选:三原则和失败案例

分桶键选不好,整个方案都会废掉。什么样的字段适合做阻塞键,我总结了三原则:分布足够散、重复对内同值率足够高、计算成本足够低。

“分布散”是说要尽量均匀,避免出现超大桶。一个反面案例是拿性别做阻塞键,男女两个桶,所有重复对确实都在同一桶里,但每个桶里有好几万条,等于没分。“重复对内同值率高”的意思是,凡是真正的重复,阻塞字段要大概率相同。比如拿“用户姓名全字段”做阻塞键,那模糊去重就白做了,因为姓名字段本身可能就被空格和错别字污染。计算成本低这个不用多说,阻塞键会被反复拿来算哈希和拼串,千万别放超长文本。

我踩过的坑是拿完整手机号做阻塞键。当时想着手机号是强标识,结果发现同一人的手机号在前后两个系统里居然不一样,一个是 13 位老号码,一个是 11 位新号码,完整手机号分桶直接把它们分到了两个桶,模糊去重完全没机会相遇。后来改成“手机号后 4 位 + 所在省份首字”组合键,才把这类情况召回进来。

4.3 增量更新:不能每次全量重跑

生产系统最怕听到“每次跑全量”。数据量大的时候,全量重跑不仅耗时,还会影响线上资源。增量更新的思路是:把每个分桶的桶内数据缓存成倒排索引,新数据进来时只需要计算它落入的桶,和桶内已有记录做比对。

具体做法可以这样设计:

  • 全量跑完后,保存一张映射表:(block_key, record_id, normalized_text, group_id)
  • 新记录到达时,先计算它的block_key
  • 只加载这个block_key下的存量记录,和新记录做相似度比对。
  • 如果超过阈值,新记录直接加入已有group_id;否则给新记录新建group_id
  • 更新倒排索引,把新记录放进对应桶。

这样的增量链路里,单条新数据只会触发一次桶内比对,几乎瞬时完成。比全量每天重跑要省太多资源了。不过有个隐患:如果系统上线后数据质量规则变了(比如清洗逻辑变了,或者新增了繁体字段),增量索引会和全量结果产生偏差。这时候还是需要定期做一次全量重跑来校准。

4.4 回写:让去重结果真正进入业务库

模糊去重跑完之后最关键的一步是回写。很多团队在这里翻车:算法跑出一份重复组名单,却不知道怎么落库,或者落库方式不对导致业务系统主数据被破坏。

比较稳妥的回写步骤是先建映射表,再更新老表。做法是在业务表里增加一列dedup_group_id,把所有记录映射到某个组的根记录 id 上;查询时按组聚合,展示层只显示主记录。这样不会物理删除任何一条数据,最大程度保留了可回溯能力。如果业务方确实要做物理合并,也应该先做组映射,再在二次确认后把冗余记录做归档处理。

映射表建好之后,日常巡检可以直接用 SQL 看每个分组的大小:

SELECT group_id, COUNT(*) AS cnt FROM customer_dedup_map GROUP BY group_id HAVING COUNT(*) > 1 ORDER BY cnt DESC;

这条查询能帮你快速发现哪些组被合并得过大。一个组如果有几十上百条记录,大概率是阈值放太宽或者阻塞键选错了,需要人工重点排查。

5. 视频去重和备份去重里的 Fuzzy 逻辑

聊到最后,回头再看热搜词里的另外两个高频场景:短视频去重和 Veeam/Data Domain 集成。这两个场景看起来跟“客户表去重”八竿子打不着,但底层的模糊匹配思想是相通的。

5.1 内容平台的视频查重:感知哈希与汉明距离

短视频领域说的“视频去重”,从内容平台的角度看,其实是在做“近似查重”。平台每天上传的视频数量巨大,如果只按 MD5 去重,用户把同一个视频翻转一下、加个边框、换一段 BGM,MD5 就全变了,系统完全没有识别能力。所以平台普遍采用感知哈希方案,一般流程是:

  • 对视频抽帧,比如每秒抽 1 帧或均匀抽 10 帧。
  • 每帧缩放到小尺寸,比如 8×8 灰度图。
  • 计算相邻像素亮度大小关系,生成感知哈希值。
  • 把多帧的哈希值组合成视频指纹。
  • 用汉明距离跟指纹库比对,距离小于阈值判定为重复内容。

从算法上讲,这和前面 SimHash 判断文本重复是一模一样的逻辑:先降维,再算距离,最后用阈值做判定。区别只是输入从“词”变成了“像素块”,从“分词权重”变成了“灰度关系”。所以当你看到有人讨论“视频查重”时,不用把它想成什么新东西,它就是 Fuzzy 去重思想在感知层的一次应用。

不过要提醒一句,这类查重系统识别的是“内容雷同”,不是“文件一样”。平台判断两条视频相似,不代表它们就是搬运,很多剪辑作品、二次创作也会命中相似的画面指纹,所以头部平台通常在感知哈希之外还会叠加更复杂的语义特征。

5.2 备份存储里的“Fuzzy”:内容定义分块和指纹去重

再看备份存储。Veeam 配合 Data Domain 做备份是后端运维的高频组合,社区里一直有个主流建议:使用 Data Domain 作为目标存储时,建议关闭 Veeam 自带的源端去重。很多刚入门的朋友不理解,为什么“去重”这么好用的功能要关掉?

关键在于 Data Domain 本身已经做了非常强的目标端全局去重。它用的是内容定义分块(Content-Defined Chunking,CDC):不是按固定大小切块,而是根据数据内容的滑动窗口哈希来决定块边界。这样即使数据流中间插入或删除了几个字节,后续大部分块的边界还能对齐,重复数据照样能识别出来。

但 Veeam 如果开启了源端去重,会在本地先把备份数据切成自己的块、算好指纹,再传输到 Data Domain。问题就出在“切块方式不统一”上:Veeam 切的块和 Data Domain 的 CDC 块边界对不上。Data Domain 拿到 Veeam 处理过的数据后,无法在里面找到足够多匹配自己块边界的数据块,全局去重率反而下降。相当于上下两层各做各的去重,互相干扰。

这里的“去重”本质上也是模糊匹配:Data Domain 的 CDC 要识别的不是“路径相同”“文件名相同”,而是“字节流在内容层面是否出现过”。固定分块好比精确匹配,只要有 1 字节的偏移,整块就全不匹配了;可变长分块则类似模糊对齐,容忍偏移,只匹配内容本身。这和模糊去重里用 n-gram 容忍局部差错的逻辑,本质上是同一个思路。

5.3 一个通用判断:在正确的层做去重

把客户表去重、视频查重、备份去重放在一起看,会发现一个共同规律:去重要做在正确的层级上,模糊度要和数据的失真方式匹配。

  • 客户表数据的失真来自人类的输入习惯(空格、错别字、同音字),所以用编辑距离、n-gram 这些字符级模糊匹配;
  • 视频数据的失真来自剪辑、压缩、滤镜,所以用感知哈希这种图像级模糊匹配;
  • 备份数据的失真来自数据流的偏移和文件封装变化,所以用内容定义分块这种字节级模糊对齐。

一个常见错误是“所有层都做去重”。Veeam 和 Data Domain 叠加去重是个例子,业务系统里也经常出现“应用层去重 + 数据库去重 + 数仓再清一次”的三重重复劳动。每增加一层去重,就多一层误差和性能损耗。正确的做法是先确定主去重层,其他层只做必要的预清洗,不该动的地方尽量不动。

我现在的习惯是,拿到一批数据先不急着写去重函数,而是先抽 50 条肉眼扫一遍,看脏数据长什么样。是空格问题?全半角?还是同音错别字?算法选型完全取决于你最常遇到的那种脏法。场景不同,Fuzzy 的“模糊度”也不同——备份场景要模糊的是字节偏移,视频场景要模糊的是画面感知,而业务数据要模糊的是语言习惯。先认清你的数据是怎么脏的,再决定怎么去重,这比任何算法都重要。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询