7.4. difflib —计算增量的助手

2.1 版中的新Function。

此模块提供用于比较序列的类和Function。例如,它可以用于比较文件,并可以产生各种格式的差异信息,包括 HTML 和上下文以及统一的 diff。要比较目录和文件,另请参见filecmp模块。

  • 类别 difflib. SequenceMatcher
    • 这是用于比较任何类型的序列对的灵活类,只要序列元素为hashable即可。基本算法早于 1980 年代末由 Ratcliff 和 Obershelp 在双曲线名称“格式塔模式匹配”下发布的算法,并且比在以前的算法更新颖。这个想法是找到最长的连续匹配子序列,其中不包含“垃圾”元素(Ratcliff 和 Obershelp 算法不解决垃圾问题)。然后将相同的思想递归应用于匹配子序列左侧和右侧的序列片段。这不会产生最小的编辑序列,但是会产生对人们“看起来正确”的匹配。

时间: 基本的 Ratcliff-Obershelp 算法在最坏的情况下是三次时间,在预期的情况下是二次时间。 SequenceMatcher是最坏情况的二次时间,其预期情况的行为以复杂的方式取决于序列共有多少个元素;最好的情况是线性的。

自动垃圾启发式: SequenceMatcher支持一种将某些序列项自动视为垃圾的启发式。试探法计算每个单个项目出现在序列中的次数。如果某项的重复项(在第一个项之后)占序列的 1%以上,并且该序列的长度至少为 200,则该项目被标记为“受欢迎”,并且出于序列匹配的目的被视为垃圾。可以pass在创建SequenceMatcher时将autojunk参数设置为False来关闭此启发式。

2.7.1 版中的新Function:* autojunk *参数。

  • 类别 difflib. Differ
    • 这是一类用于比较文本行序列并产生人类可读的差异或增量的类。 Differ 使用SequenceMatcher来比较行序列,并比较相似(近匹配)行中的字符序列。

Differ delta 的每一行都以两个字母的代码开头:

CodeMeaning
'- '序列 1 特有的行
'+ '序列 2 特有的行
' '这两个序列共有的线
'? '这两个 Importing 序列中都不存在行

以“ ?”开头的行试图将眼睛引导至行内差异,但在任一 Importing 序列中均不存在。如果序列包含制表符,这些行可能会造成混淆。

  • 类别 difflib. HtmlDiff
    • 此类可用于创建 HTML 表(或包含该表的完整 HTML 文件),以显示并排,逐行比较带有行间和行内更改突出显示的文本。该表可以完全或上下文差异模式生成。

此类的构造函数为:

  • __init__(* tabsize = 8 wrapcolumn = None linejunk = None charjunk = IS_CHARACTER_JUNK *)
  • tabsize *是一个可选的关键字参数,用于指定制表位的间距,默认为8

  • wrapcolumn *是一个可选关键字,用于指定断行和换行的列号,默认为None(不换行)。

  • linejunk charjunk *是传递给ndiff()的可选关键字参数(由HtmlDiff使用以生成并排的 HTML 差异)。有关参数默认值和说明,请参见ndiff()文档。

以下是公开的方法:

  • make_file(* fromlines,tolines [,fromdesc] [,todesc] [,context] [,numlines] *)
    • 比较* fromlines tolines *(字符串列表)并返回一个字符串,它是一个完整的 HTML 文件,其中包含一个表格,其中显示了逐行差异,并突出显示了行间和行内更改。
  • fromdesc todesc *是可选的关键字参数,用于指定从/到文件列标题字符串(均默认为空字符串)。

  • context numlines 都是可选的关键字参数。当要显示上下文差异时,将 context *设置为True,否则默认为False以显示完整文件。 * numlines 默认为5。当 context True时, numlines 控制围绕差异高亮显示的上下文行的数量。当 context False时, numlines *控制使用“下一个”超链接时在差异突出显示之前显示的行数(设置为零会导致“下一个”超链接将下一个差异突出显示放置在浏览器,没有任何领先的上下文)。

  • make_table(* fromlines,tolines [,fromdesc] [,todesc] [,context] [,numlines] *)
    • 比较* fromlines tolines *(字符串列表)并返回一个字符串,该字符串是一个完整的 HTML 表,显示逐行差异,并突出显示行间和行内更改。

此方法的参数与make_file()方法的参数相同。

Tools/scripts/diff.py是此类的命令行前端,并且包含一个很好的用法示例。

2.4 版的新Function。

  • difflib. context_diff(* a,b [,fromfile] [,tofile] [,fromfiledate] [,tofiledate] [,n] [,lineterm] *)
    • 比较* a b *(字符串列表);以上下文差异格式返回增量(生成增量行的generator)。

上下文差异是一种仅显示已更改的行以及上下文的几行的紧凑方式。更改以之前/之后的样式显示。上下文行的数量由* n *设置,默认为三。

默认情况下,差异控制线(带有***---的控制线)使用尾随换行符创建。这很有用,因为从file.readlines()创建的 Importing 将导致差异适合与file.writelines()一起使用,因为 Importing 和输出均具有尾随换行符。

对于没有尾随换行符的 Importing,请将* lineterm *参数设置为"",以使输出一致地没有换行符。

上下文差异格式通常具有文件名和修改时间的 Headers。可以使用* fromfile tofile fromfiledate tofiledate *的字符串指定其中的任何一个或全部。修改时间通常以 ISO 8601 格式表示。如果未指定,则字符串默认为空白。

>>> s1 = ['bacon\n', 'eggs\n', 'ham\n', 'guido\n']
>>> s2 = ['python\n', 'eggy\n', 'hamster\n', 'guido\n']
>>> for line in context_diff(s1, s2, fromfile='before.py', tofile='after.py'):
...     sys.stdout.write(line)  
*** before.py
--- after.py
***************
*** 1,4 ****
! bacon
! eggs
! ham
  guido
--- 1,4 ----
! python
! eggy
! hamster
  guido

有关更详细的示例,请参见difflib 的命令行界面

2.3 版的新Function。

  • difflib. get_close_matches(* word,可能性[,n] [,截断] *)
    • 返回最佳“足够好”的匹配项列表。 * word 是需要紧密匹配的序列(通常是字符串), possibilities 是要与之匹配 word *的序列列表(通常是字符串列表)。

可选参数* n *(默认值3)是要返回的最大匹配数; * n *必须大于0

可选参数* cutoff (默认为0.6)是[0,1]范围内的浮点数。得分不低于 word *的可能性将被忽略。

可能性中的最佳匹配(不超过* n *个)以列表形式返回,并按相似性得分排序,最相似的为第一。

>>> get_close_matches('appel', ['ape', 'apple', 'peach', 'puppy'])
['apple', 'ape']
>>> import keyword
>>> get_close_matches('wheel', keyword.kwlist)
['while']
>>> get_close_matches('apple', keyword.kwlist)
[]
>>> get_close_matches('accept', keyword.kwlist)
['except']
  • difflib. ndiff(* a,b [,linejunk] [,charjunk] *)
    • 比较* a b *(字符串列表);返回Differ样式的增量(generator生成增量线)。

可选关键字参数* linejunk charjunk *用于过滤器Function(或None):

  • linejunk *:一个函数,它接受单个字符串参数,如果字符串为垃圾,则返回 true,否则返回 false。从 Python 2.3 开始,默认值为(None)。在此之前,默认值是模块级FunctionIS_LINE_JUNK(),该Function过滤掉不包含可见字符的行,但最多不包含 1 磅字符('#')。从 Python 2.3 开始,底层的SequenceMatcher类对哪些行如此频繁以至于构成噪声进行了动态分析,这通常比 2.3 之前的默认版本更好。

  • charjunk *:一个函数,该函数接受一个字符(长度为 1 的字符串),并在字符为垃圾字符时返回,否则返回 false。默认值为模块级别的函数IS_CHARACTER_JUNK(),该函数会过滤出空格字符(空格或制表符;注意:在其中包括换行符是个坏主意!)。

Tools/scripts/ndiff.py是此Function的命令行前端。

>>> diff = ndiff('one\ntwo\nthree\n'.splitlines(1),
...              'ore\ntree\nemu\n'.splitlines(1))
>>> print ''.join(diff),
- one
?  ^
+ ore
?  ^
- two
- three
?  -
+ tree
+ emu
  • difflib. restore(序列其中)
    • 返回生成增量的两个序列之一。

给定Differ.compare()ndiff()产生的* sequence ,提取源自文件 1 或 2 的行(参数 which *),去除行前缀。

Example:

>>> diff = ndiff('one\ntwo\nthree\n'.splitlines(1),
...              'ore\ntree\nemu\n'.splitlines(1))
>>> diff = list(diff) # materialize the generated delta into a list
>>> print ''.join(restore(diff, 1)),
one
two
three
>>> print ''.join(restore(diff, 2)),
ore
tree
emu
  • difflib. unified_diff(* a,b [,fromfile] [,tofile] [,fromfiledate] [,tofiledate] [,n] [,lineterm] *)
    • 比较* a b *(字符串列表);以统一的 diff 格式返回增量(生成增量行的generator)。

统一差异是仅显示已更改的行以及上下文的几行的紧凑方式。更改以内联样式显示(而不是单独的 before/after 块)。上下文行的数量由* n *设置,默认为三。

默认情况下,差异控制线(带有---+++@@的控制线)以尾随换行符创建。这很有用,因为从file.readlines()创建的 Importing 将导致差异适合与file.writelines()一起使用,因为 Importing 和输出均具有尾随换行符。

对于没有尾随换行符的 Importing,请将* lineterm *参数设置为"",以使输出一致地没有换行符。

上下文差异格式通常具有文件名和修改时间的 Headers。可以使用* fromfile tofile fromfiledate tofiledate *的字符串指定其中的任何一个或全部。修改时间通常以 ISO 8601 格式表示。如果未指定,则字符串默认为空白。

>>> s1 = ['bacon\n', 'eggs\n', 'ham\n', 'guido\n']
>>> s2 = ['python\n', 'eggy\n', 'hamster\n', 'guido\n']
>>> for line in unified_diff(s1, s2, fromfile='before.py', tofile='after.py'):
...     sys.stdout.write(line)   
--- before.py
+++ after.py
@@ -1,4 +1,4 @@
-bacon
-eggs
-ham
+python
+eggy
+hamster
 guido

有关更详细的示例,请参见difflib 的命令行界面

2.3 版的新Function。

  • difflib. IS_LINE_JUNK(* line *)

    • 对于可忽略的行,返回 true。如果* line 为空白或包含单个'#',则 line 行可忽略。否则,不可忽略。在 Python 2.3 之前用作ndiff()中参数 linejunk *的默认值。
  • difflib. IS_CHARACTER_JUNK(* ch *)

    • 对于可忽略字符返回 true。如果* ch 是空格或制表符,则字符 ch 是可忽略的,否则不可忽略。用作ndiff()中参数 charjunk *的默认值。

See also

7.4.1. SequenceMatcher 对象

SequenceMatcher类具有以下构造函数:

    • class * difflib. SequenceMatcher(* isjunk = None a ='' b ='' autojunk = True *)
    • 可选参数* isjunk 必须为None(默认值),或者是一个带参数的元素的单参数函数,当且仅当该元素为“垃圾”且应将其忽略时才返回 true。为 isjunk *传递None等效于lambda x: 0传递;换句话说,不会忽略任何元素。例如,传递:
lambda x: x in " \t"

如果要将行作为字符序列进行比较,并且不想在空格或硬标签上同步。

可选参数* a b *是要比较的序列;两者都默认为空字符串。两个序列的元素必须为hashable

可选参数* autojunk *可用于禁用自动垃圾启发式。

2.7.1 版中的新Function:* autojunk *参数。

SequenceMatcher对象具有以下方法:

  • set_seqs(* a b *)
    • 设置两个要比较的序列。

SequenceMatcher计算并缓存有关第二个序列的详细信息,因此,如果要将一个序列与多个序列进行比较,请使用set_seq2()一次设置常用序列,然后针对每个其他序列重复调用set_seq1()

  • set_seq1(* a *)

    • 设置要比较的第一个序列。要比较的第二个 Sequences 不变。
  • set_seq2(* b *)

    • 设置要比较的第二个序列。要比较的第一个序列不变。
  • find_longest_match((* alo ahi blo bhi *)

    • a[alo:ahi]b[blo:bhi]中找到最长的匹配块。

如果Ellipsis* isjunk None,则find_longest_match()返回(i, j, k),使得a[i:i+k]等于b[j:j+k],其中alo <= i <= i+k <= ahiblo <= j <= j+k <= bhi。对于所有满足这些条件的(i', j', k'),附加条件k >= k'i <= i',以及i == i'j <= j'也要满足。换句话说,在所有最大匹配块中,返回最早在 a 中开始的块,在所有最大匹配块中最早在 a 中开始的块,返回最早在 b *中开始的块。

>>> s = SequenceMatcher(None, " abcd", "abcd abcd")
>>> s.find_longest_match(0, 5, 0, 9)
Match(a=0, b=4, size=5)

如果提供了* isjunk *,则首先如上所述确定最长的匹配块,但附加的限制是该块中不会出现垃圾元素。然后,pass在两侧匹配(仅)垃圾元素来尽可能扩展该块。因此,结果块永远不会在垃圾上匹配,除非相同的垃圾恰好与某个有趣的匹配相邻。

这是与之前相同的示例,但考虑到空白是垃圾。这样可以防止' abcd'直接匹配第二个序列尾部的' abcd'。相反,只有'abcd'可以匹配,并且与第二个序列中最左边的'abcd'匹配:

>>> s = SequenceMatcher(lambda x: x==" ", " abcd", "abcd abcd")
>>> s.find_longest_match(0, 5, 0, 9)
Match(a=1, b=0, size=4)

如果没有块匹配,则返回(alo, blo, 0)

在 2.6 版中更改:此方法返回named tuple Match(a, b, size)

  • get_matching_blocks ( )
    • 返回描述非重叠匹配子序列的三 Tuples 列表。每个三 Tuples 的形式为(i, j, n),表示a[i:i+n] == b[j:j+n]。三 Tuples 在* i j *中单调增加。

最后一个三 Tuples 是一个虚拟对象,其值为(len(a), len(b), 0)。它是n == 0的唯一三 Tuples。如果(i, j, n)(i', j', n')是列表中的相邻三 Tuples,而第二个不是列表中的最后一个三 Tuples,则i+n < i'j+n < j';换句话说,相邻的三 Tuples 总是描述不相邻的相等块。

在版本 2.5 中进行了更改:保证了相邻的三 Tuples 始终描述不相邻的块。

>>> s = SequenceMatcher(None, "abxcd", "abcd")
>>> s.get_matching_blocks()
[Match(a=0, b=0, size=2), Match(a=3, b=2, size=2), Match(a=5, b=4, size=0)]
  • get_opcodes ( )
    • 返回 5Tuples 的列表,描述如何将* a 转换为 b 。每个 Tuples 的形式为(tag, i1, i2, j1, j2)。第一个 Tuples 具有i1 == j1 == 0,其余的 Tuples 的 i1 等于前一个 Tuples 的 i2 ,同样, j1 等于前一个 j2 *。
  • tag *值是字符串,具有以下含义:
ValueMeaning
'replace'a[i1:i2]应替换为b[j1:j2]
'delete'a[i1:i2]应该被删除。请注意,在这种情况下,j1 == j2
'insert'b[j1:j2]应该插入a[i1:i1]。请注意,在这种情况下为i1 == i2
'equal'a[i1:i2] == b[j1:j2](子序列相等)。

For example:

>>> a = "qabxcd"
>>> b = "abycdf"
>>> s = SequenceMatcher(None, a, b)
>>> for tag, i1, i2, j1, j2 in s.get_opcodes():
...    print ("%7s a[%d:%d] (%s) b[%d:%d] (%s)" %
...           (tag, i1, i2, a[i1:i2], j1, j2, b[j1:j2]))
 delete a[0:1] (q) b[0:0] ()
  equal a[1:3] (ab) b[0:2] (ab)
replace a[3:4] (x) b[2:3] (y)
  equal a[4:6] (cd) b[3:5] (cd)
 insert a[6:6] () b[5:6] (f)
  • get_grouped_opcodes([* n *])
    • 返回最多+1 个上下文行的generator个组。

get_opcodes()返回的组开始,此方法会拆分较小的更改集群,并消除没有更改的中间范围。

这些组以与get_opcodes()相同的格式返回。

2.3 版的新Function。

  • ratio ( )
    • 以[0,1]范围内的浮点数形式返回序列相似性的度量。

其中 T 是两个序列中元素的总数,M 是匹配数,这是 2.0 * M /T。请注意,如果序列相同,则为1.0;如果没有相同之处,则为0.0

如果尚未调用get_matching_blocks()get_opcodes(),则计算成本很高,在这种情况下,您可能需要先tryquick_ratio()real_quick_ratio()以获得上限。

  • quick_ratio ( )

    • 相对较快地返回ratio()的上限。
  • real_quick_ratio ( )

尽管quick_ratio()real_quick_ratio()总是至少与ratio()一样大,但由于逼近的不同程度,返回匹配字符与总字符的比率的三种方法可能会得出不同的结果:

>>> s = SequenceMatcher(None, "abcd", "bcde")
>>> s.ratio()
0.75
>>> s.quick_ratio()
0.75
>>> s.real_quick_ratio()
1.0

7.4.2. SequenceMatcher 示例

本示例比较两个字符串,并将空格视为“垃圾”:

>>> s = SequenceMatcher(lambda x: x == " ",
...                     "private Thread currentThread;",
...                     "private volatile Thread currentThread;")

ratio()返回[0,1]中的浮点数,以测量序列的相似性。根据经验,当ratio()的值超过 0.6 时,表示序列是紧密匹配的:

>>> print round(s.ratio(), 3)
0.866

如果您只对序列匹配的位置感兴趣,则get_matching_blocks()很方便:

>>> for block in s.get_matching_blocks():
...     print "a[%d] and b[%d] match for %d elements" % block
a[0] and b[0] match for 8 elements
a[8] and b[17] match for 21 elements
a[29] and b[38] match for 0 elements

请注意,get_matching_blocks()返回的最后一个 Tuples 总是(len(a), len(b), 0),这是唯一的情况,最后一个 Tuples 元素(匹配的元素数)是0

如果您想知道如何将第一个序列更改为第二个序列,请使用get_opcodes()

>>> for opcode in s.get_opcodes():
...     print "%6s a[%d:%d] b[%d:%d]" % opcode
 equal a[0:8] b[0:8]
insert a[8:8] b[8:17]
 equal a[8:29] b[17:38]

See also

7.4.3. 不同的对象

请注意,Differ生成的增量不会声称是“最小”差异。相反,最小差异通常是违反直觉的,因为它们尽可能地同步,有时偶然相距 100 页。将同步点限制为连续匹配可以保留局部性的概念,但偶尔会产生较长的差异。

Differ类具有以下构造函数:

    • class * difflib. Differ([* linejunk * [,* charjunk *]])
    • 可选关键字参数* linejunk charjunk *用于过滤器Function(或None):
  • linejunk *:一个函数,它接受单个字符串参数,如果字符串为垃圾,则返回 true。默认值为None,这意味着没有行被视为垃圾。

  • charjunk *:一个函数,它接受单个字符参数(长度为 1 的字符串),如果字符为垃圾,则返回 true。默认值为None,表示没有字符被视为垃圾字符。

Differ对象pass一种方法使用(生成增量):

  • compare(* a b *)
    • 比较两条线序列,并生成增量(一条线序列)。

每个序列必须包含以换行符结尾的单个单行字符串。此类序列可以从文件类对象的readlines()方法获得。生成的增量还包含换行符终止的字符串,这些字符串可以pass文件状对象的writelines()方法按原样打印。

7.4.4. 不同的例子

本示例比较两个文本。首先,我们设置文本,以换行符结尾的单个单行字符串的序列(此类序列也可以从类似文件的对象的readlines()方法获得):

>>> text1 = '''  1. Beautiful is better than ugly.
...   2. Explicit is better than implicit.
...   3. Simple is better than complex.
...   4. Complex is better than complicated.
... '''.splitlines(1)
>>> len(text1)
4
>>> text1[0][-1]
'\n'
>>> text2 = '''  1. Beautiful is better than ugly.
...   3.   Simple is better than complex.
...   4. Complicated is better than complex.
...   5. Flat is better than nested.
... '''.splitlines(1)

接下来,我们实例化一个 Differ 对象:

>>> d = Differ()

请注意,在实例化Differ对象时,我们可以传递函数以过滤掉行和字符“垃圾”。有关详细信息,请参见Differ()构造函数。

最后,我们比较两个:

>>> result = list(d.compare(text1, text2))

result是字符串列表,所以让它漂亮地打印出来:

>>> from pprint import pprint
>>> pprint(result)
['    1. Beautiful is better than ugly.\n',
 '-   2. Explicit is better than implicit.\n',
 '-   3. Simple is better than complex.\n',
 '+   3.   Simple is better than complex.\n',
 '?     ++\n',
 '-   4. Complex is better than complicated.\n',
 '?            ^                     ---- ^\n',
 '+   4. Complicated is better than complex.\n',
 '?           ++++ ^                      ^\n',
 '+   5. Flat is better than nested.\n']

作为单个多行字符串,它看起来像这样:

>>> import sys
>>> sys.stdout.writelines(result)
    1. Beautiful is better than ugly.
-   2. Explicit is better than implicit.
-   3. Simple is better than complex.
+   3.   Simple is better than complex.
?     ++
-   4. Complex is better than complicated.
?            ^                     ---- ^
+   4. Complicated is better than complex.
?           ++++ ^                      ^
+   5. Flat is better than nested.

7.4.5. difflib 的命令行界面

本示例说明如何使用 difflib 创建类似diff的 Util。它也以Tools/scripts/diff.py的形式包含在 Python 源代码发行版中。

""" Command line interface to difflib.py providing diffs in four formats:

* ndiff:    lists every line and highlights interline changes.
* context:  highlights clusters of changes in a before/after format.
* unified:  highlights clusters of changes in an inline format.
* html:     generates side by side comparison with change highlights.

"""

import sys, os, time, difflib, optparse

def main():
     # Configure the option parser
    usage = "usage: %prog [options] fromfile tofile"
    parser = optparse.OptionParser(usage)
    parser.add_option("-c", action="store_true", default=False,
                      help='Produce a context format diff (default)')
    parser.add_option("-u", action="store_true", default=False,
                      help='Produce a unified format diff')
    hlp = 'Produce HTML side by side diff (can use -c and -l in conjunction)'
    parser.add_option("-m", action="store_true", default=False, help=hlp)
    parser.add_option("-n", action="store_true", default=False,
                      help='Produce a ndiff format diff')
    parser.add_option("-l", "--lines", type="int", default=3,
                      help='Set number of context lines (default 3)')
    (options, args) = parser.parse_args()

    if len(args) == 0:
        parser.print_help()
        sys.exit(1)
    if len(args) != 2:
        parser.error("need to specify both a fromfile and tofile")

    n = options.lines
    fromfile, tofile = args # as specified in the usage string

    # we're passing these as arguments to the diff function
    fromdate = time.ctime(os.stat(fromfile).st_mtime)
    todate = time.ctime(os.stat(tofile).st_mtime)
    with open(fromfile, 'U') as f:
        fromlines = f.readlines()
    with open(tofile, 'U') as f:
        tolines = f.readlines()

    if options.u:
        diff = difflib.unified_diff(fromlines, tolines, fromfile, tofile,
                                    fromdate, todate, n=n)
    elif options.n:
        diff = difflib.ndiff(fromlines, tolines)
    elif options.m:
        diff = difflib.HtmlDiff().make_file(fromlines, tolines, fromfile,
                                            tofile, context=options.c,
                                            numlines=n)
    else:
        diff = difflib.context_diff(fromlines, tolines, fromfile, tofile,
                                    fromdate, todate, n=n)

    # we're using writelines because diff is a generator
    sys.stdout.writelines(diff)

if __name__ == '__main__':
    main()