Python SequenceMatcher:两段重复文字很像,相似度为什么却接近零
比较两段长文本,只有开头一个字符不同,其余都是同一字符,直觉上应该非常相似。SequenceMatcher 默认给出的分数却可能为零。遇到这种结果,先查看匹配器的高频元素启发式,再怀疑文件内容或字符编码;输入长度与重复程度也参与了匹配过程。
下面用人工构造的短实验隔离原因,在 CPython 3.12.14 执行。保存为 demo.py,运行 python demo.py。我们故意让两段文本的首字符不同,后面各有二百一十个相同字符;这些重复字符对本任务非常有意义,不能因为频繁就忽略其作用。
同时检查分数与高频集合
第二个序列足够长时,默认启发式会把过于常见的元素放入 bpopular,减少寻找匹配起点时的候选。判定依据是重复次数,而不是这个字符在业务中的重要性。字符文本、按行列表和词元列表的元素单位不同,同一份原文换一种拆分方式,判断也可能改变。
AI概念示意图:两条重复图形序列正在对齐,放大窗口显示被高频筛选忽略的长段对应关系。图片只说明概念。
from difflib import SequenceMatcher
left = "x" + "a" * 210
right = "y" + "a" * 210
default = SequenceMatcher(None, left, right)
explicit = SequenceMatcher(None, left, right, autojunk=False)
assert default.bpopular == {"a"}
assert default.ratio() == 0.0
assert explicit.bpopular == set()
assert explicit.ratio() == 210 / 211
assert explicit.get_matching_blocks()[0].size == 210
print("default:", default.ratio())
print("explicit:", round(explicit.ratio(), 6))
same = SequenceMatcher(None, right, right)
assert same.bpopular == {"a"}
assert same.ratio() == 1.0
print("identical:", same.ratio())
short = SequenceMatcher(None, "x" + "a" * 10, "y" + "a" * 10)
assert short.bpopular == set()
assert short.ratio() == 10 / 11
print("short popular:", sorted(short.bpopular))自动规则在第二序列至少有二百个元素时启动;某元素除首次之外的重复次数超过该序列长度的百分之一,便可能被列为高频。示例的长度是二百一十一,已达到启动门槛,重复次数也明显超过频率条件,便于稳定复现。
高频不等于从所有结果里删除
示例打印 default: 0.0 与 explicit: 0.995261,后者匹配了二百一十个字符。关闭 autojunk 以后,重复元素可以参与寻找起点,分数明显改变。这里没有声称新分数更接近某种通用真值,只是证明启发式与当前数据的含义不一致,需要由任务选择规则。
同一份长文本与自身比较仍然得到一,尽管重复字符仍属于高频集合。它说明“进入 bpopular”不等于预先从两边彻底删除该元素。算法找到的匹配还可以扩展,所以不能把默认分数解释为只统计少见字符,也不能只看集合大小就推算最终结果。
短样本没有达到启动自动高频处理的长度条件,bpopular 为空。真实排查时可以保留边界附近的样本,记录原始长度、拆分单位、开关和匹配块。只输出一个四舍五入后的分数,会把这些决定结果的条件都丢掉,难以解释两次运行的差异。
关闭开关之后仍有边界
SequenceMatcher 寻找适合人阅读的连续匹配块,并不承诺最短编辑脚本;ratio 也不是语义相似度或概率。两句话词序很接近却含义相反,仍可能得高分。把分数用于自动合并或去重前,应准备真实的正反样本,明确误合并与漏检分别怎样处理。
自动启发式主要根据第二个输入建立索引,整体算法的并列选择也可能受输入顺序影响。关闭 autojunk 不能被当作“所有相似度从此对称”的保证。需要对称量的场景,应先选定相应定义并测试,而不是把两个方向的分数随意平均后继续沿用旧阈值。
关闭启发式可能增加大量重复序列的匹配成本。本文只验证确定的匹配结果,没有以一次本机耗时推断性能。上线前应限制文本规模,在代表性数据上同时观察用时、内存与误判样本;若业务关心词或行,就先把元素单位确定下来,再讨论开关与阈值。


