Pythons SequenceMatcher如何工作?

时间:2016-02-20 00:06:27

标签: python string comparison sequencematcher

我对SequenceMatcher返回的两个不同答案感到有点困惑,具体取决于参数的顺序。为什么会这样?

实施例

SequenceMatcher不可交换:

>>> from difflib import SequenceMatcher
>>> SequenceMatcher(None, "Ebojfm Mzpm", "Ebfo ef Mfpo").ratio()
0.6086956521739131

>>> SequenceMatcher(None, "Ebfo ef Mfpo", "Ebojfm Mzpm").ratio()
0.5217391304347826

2 个答案:

答案 0 :(得分:9)

SequenceMatcher.ratio在内部使用SequenceMatcher.get_matching_blocks来计算比率,我将引导您完成相关步骤:

  

SequenceMatcher.ratio

     

返回描述匹配子序列的三元组列表。每个三元组的格式为(i, j, n),表示a[i:i+n] == b[j:j+n]。三元组在ij中单调递增。

     

最后一个三元组是一个虚拟元素,其值为(len(a), len(b), 0)。它是n == 0唯一的三元组。 If (i, j, n)(i', j', n')是列表中的相邻三元组,第二个不是列表中的最后一个三元组,然后是i+n != i'j+n != j';换句话说,相邻的三元组总是描述不相邻的相等的块。

ratio在内部使用SequenceMatcher.get_matching_blocks的结果,并对SequenceMatcher.get_matching_blocks返回的所有匹配序列的大小求和。这是来自difflib.py的确切源代码:

matches = sum(triple[-1] for triple in self.get_matching_blocks())

以上一行很关键,因为上述表达式的结果用于计算比率。我们很快就会看到它如何影响比率的计算。

m1 = SequenceMatcher(None, "Ebojfm Mzpm", "Ebfo ef Mfpo")
m2 = SequenceMatcher(None, "Ebfo ef Mfpo", "Ebojfm Mzpm")

matches1 = sum(triple[-1] for triple in m1.get_matching_blocks()) # matches1=7
matches2 = sum(triple[-1] for triple in m2.get_matching_blocks()) #matches=6

如您所见,我们有7和6.这些只是get_matching_blocks返回的匹配子序列的总和。为什么这很重要?这就是为什么,比率是按照以下方式计算的(这是来自difflib源代码):

def _calculate_ratio(matches, length):
    if length:
        return 2.0 * matches / length
    return 1.0

lengthlen(a) + len(b),其中a是第一个序列,b是第二个序列。

好的,说得够多,我们需要采取行动:

>>> length = len("Ebojfm Mzpm") + len("Ebfo ef Mfpo") 
>>> m1.ratio()
0.6086956521739131
>>> (2.0 * matches1 / length)  == m1.ratio()
True

同样适用于m2

>>> 2.0 * matches2 / length
0.5217391304347826 
>>> (2.0 * matches2 / length) == m2.ratio()
True

注意:并非所有 SequenceMatcher(None a,b).ratio() == SequenceMatcher(None b,a).ratio()都是False,有时可能是True

>>> s1 = SequenceMatcher(None, "abcd", "bcde").ratio()
>>> s2 = SequenceMatcher(None, "bcde", "abcd").ratio()
>>> s1 == s2
True

如果您想知道原因,这是因为

sum(triple[-1] for triple in self.get_matching_blocks())

SequenceMatcher(None, "abcd", "bcde")SequenceMatcher(None, "bcde", "abcd")相同, 3

答案 1 :(得分:3)

我的回答没有提供观察到的问题的确切细节,但是包含了一个一般性解释,说明为什么使用松散定义的差异方法可能会发生这种情况。

基本上一切都归结为这样的事实,在一般情况下,

  1. 可以从给定的一对字符串中提取多个相同长度的公共子序列,

  2. 对于人类专家来说,较长的常见子序列可能看起来不如较短的子序列。

  3. 由于你对这个特殊情况感到困惑,让我们分析下面一对字符串的常见子序列标识:

    • my stackoverflow mysteries
    • mystery

    对我而言,自然匹配为"MYSTER",如下所示:

    my stackoverflow MYSTERies
    .................MYSTERy..
    

    但是,最长匹配完全覆盖两个字符串中较短的一个,如下所示:

    MY STackovERflow mYsteries
    MY.ST.....ER......Y.......
    

    这种匹配的缺点是它引入了多个匹配的子块,而(较短的)自然匹配是连续的。

    因此,调整差异算法,使其输出更令最终用户满意。因此,它们不是100%在数学上优雅,因此不具备您期望从纯粹的学术(而非实际)工具中获得的属性。

    documentation of SequenceMatcher包含相应的注释:

      

    class difflib.SequenceMatcher

         

    这是一个灵活的类,用于比较任何类型的序列对,   只要序列元素是可以清除的。基础的   算法比算法早,并且比算法更有趣   由Ratcliff和Obershelp于1980年代后期出版   双曲线名称“格式塔模式匹配。”的想法是找到   最长的连续匹配子序列,不包含“垃圾”   元素(Ratcliff和Obershelp算法不解决垃圾问题)。   然后将相同的想法递归地应用于片段   序列在匹配子序列的左侧和右侧。   这不会产生最少的编辑序列,但确实会产生对人们“看起来正确”的匹配。