levenshtein距离计算字符级相似度,归一化公式为1 – (distance / max(len1, len2));注意性能差、对空白和换行敏感,需预处理;大文件宜用行级diff(如diffplex)或simhash近似去重。

用 LevenshteinDistance 计算两段文本的字符级相似度
直接比较文件内容时,最常用且易实现的是编辑距离(Levenshtein),它统计将一个字符串转为另一个所需的最少单字符编辑操作数(插入、删除、替换)。得分越小,越相似;常归一化为 0~1 的相似度:1 - (distance / math.Max(str1.Length, str2.Length))。
注意:该方法对长文本性能差(O(m×n) 时间+空间),且对换行、空格、缩进敏感——读取文件时建议先统一 NormalizeLineEndings 和 Trim()。
- 若文件超 10KB,别直接传全文进
LevenshteinDistance,先按行切分再聚合 -
String.Equals(str1, str2, StringComparison.OrdinalIgnoreCase)可先快速排除完全相等或大小写差异场景 - 避免用
File.ReadAllText读超大文件,改用StreamReader流式读取并截断前 N 行做粗筛
用 DiffPlex 库做行级差异并推导相似度
当文件是代码、日志或结构化文本时,行粒度比字符粒度更合理。DiffPlex 是 .NET 生态中稳定、轻量的开源 diff 库,能返回添加/删除/修改的行集合。你可以基于“相同行数占比”估算相似度:(totalLines - diff.Lines.count(l => l.Type != DiffPlex.Model.LineType.Equal)) / (double)totalLines。
它不计算语义,但抗格式扰动强——比如把 if (x > 0) 换成 if(x>0),字符距离崩坏,而行 diff 仍视为同一行。
- 安装:
dotnet add package DiffPlex - 务必用
new Differ().CreateLineDiffs,别用CreateCharacterDiffs(后者退化为 Levenshtein) - 预处理很重要:用
Regex.Replace(line, @"s+", " ").Trim()统一空白,否则空格差异会制造大量假“变更行”
用 SimHash 实现海量文件的快速近似去重
如果你要批量比对成百上千个文件(比如日志去重、代码克隆检测),逐对计算编辑距离或 diff 不现实。SimHash 能把任意长度文本映射为一个 64 位整数指纹,汉明距离小即内容相近。.NET 有轻量实现如 SimMetrics.Net 或手撸哈希管道。
它不给出精确相似度百分比,但支持用 BitOperations.PopCount(a ^ b) 快速算出两指纹差异位数——通常 ≤3 视为高度相似。
- 输入需分词:对代码可用
Regex.Split(content, @"(W+)")提取标识符和关键字;对普通文本可用空格+标点切分 - 权重设计影响大:给关键词(如函数名、错误码)更高权重,停用词(
the、is)可忽略 - 注意编码:必须确保两个文件都用同一种
Encoding.UTF8读取,否则哈希值无意义
绕不开的陷阱:文件编码、bom、换行符和空行处理
90% 的“明明内容一样却算出低相似度”问题,根子不在算法,而在读取环节。windows 记事本保存的 UTF-8 文件带 BOM,linux 下的 n 和 Windows 的 rn 被当不同字符,空行位置偏移会让整段 diff 错位。
- 永远用
File.ReadAllLines(path, Encoding.UTF8)显式指定编码,别依赖默认 - 读取后执行:
lines = lines.select(l => Regex.Replace(l, @"s*$", "")).ToArray()清理行尾空白 - 对纯文本比对,可先过滤掉空行:
lines.Where(l => !string.IsNullOrWhiteSpace(l)),避免空行数量差异主导结果 - 若文件含中文,确认未被 GBK 编码损坏——
File.ReadAllBytes+Encoding.GetEncoding("GBK").GetString(bytes)可临时救急
实际项目里,没有“唯一正确”的相似度算法。字符距离适合短配置片段,行 diff 更适合源码或日志,SimHash 是规模门槛跨过 500 个文件后的必选项。真正难的是根据业务定义“什么算相似”:是语法结构一致?关键字段相同?还是语义等价?那得上 ML 模型了——但那是另一回事。