6.3. difflib - 计算增量的助手

源代码: Lib/difflib.py

此模块提供了用于比较序列的类和函数。它可以用于比较文件,例如,使用并可以产生各种格式,包括 HTML 和上下文和统一的差异的差异信息。为比较文件或目录, 参见filecmp 模块.

class difflib.SequenceMatcher

这是一个灵活的类,用于比较的序列的任何类型,对,只要序列元素是hashable基本算法比20世纪80年代末由Ratcliff和Obershelp在双曲线名称“gestalt模式匹配”下发布的算法早,并且是一个有趣的人。这个想法是找到不包含“垃圾”元素的最长的连续匹配子序列;这些“垃圾”元素在某种意义上是不感兴趣的元素,例如空白行或空格。(处理垃圾是Ratcliff和Obershelp算法的扩展。)同样的想法然后应用递归序列的左侧和右侧的匹配序列片段。这不会产生最小的编辑序列,但却倾向于产生"眺望权"的比赛的人。

计时:基本Ratcliff-Obershelp算法是最坏情况下的三次时间和预期情况下的二次时间。SequenceMatcher是最坏情况的二次时间,并且预期情况行为以复杂的方式依赖于序列具有共同数量的元素;最佳情况时间是线性的。

自动垃圾启发式: SequenceMatcher支持自动将某些序列项视为垃圾的启发式算法。启发式计数每个单独项目序列中出现了多少次。如果超过 1%的序列和序列项目 (后的第一个) 的重复帐户是至少 200 项这么长,这一项目被标记为"流行",被视为为序列匹配的垃圾。这启发式算法可以通过将autojunk参数设置为False ,创建SequenceMatcher时关闭。

版本3.2中的新功能: autojunk参数。

class difflib.Differ

这是文本的一类比较序列、 行和生产人类可读的差异或增量。Differ使用SequenceMatcher ,比较序列的线条,和比较的类似 (接近匹配) 行中的字符序列。

每一行开头以两个Differ的字母符号开头:

含义
'- '序列1独有
'+ '序列2独有
' '两个序列共有的
'? '行不存在于任一输入序列中

以 '?' 开头的行试图高亮每行内的分歧,引导眼睛和任一输入序列中不在场。这些行可以是混乱的如果序列包含制表符字符。

class difflib.HtmlDiff

此类可用于创建一个 HTML table (或包含这个 table 的一个完整 HTML 文件) 并排逐行比较并突出显示行间和行内的不同。可以在完整模式或上下文差异模式下生成这个table。

此类的构造函数是:

__init__(tabsize=8, wrapcolumn=None, linejunk=None, charjunk=IS_CHARACTER_JUNK)

初始化HtmlDiff的实例。

tabsize是一个可选的关键字参数,指定 tab 停止间距,默认值为8

wrapcolumn是一个可选的关键字,以指定在哪里行的破裂和包裹,默认值为None不换行的列数。

linejunkcharjunk是传递到ndiff()中的可选关键字参数(由HtmlDiff HTML差异)。请参阅ndiff()参数默认值和说明文档。

下列方法是公共的:

make_file(fromlines, tolines, fromdesc='', todesc='', context=False, numlines=5, *, charset='utf-8')

比较fromlinestolines(字符串列表),返回一个字符串,它是一个完整的 HTML 文件,包含一个表格,显示每行的差异,行之间和行内更改突出显示。

fromdesctodesc 是可选的关键字参数来指定 from/to 文件列标题字符串(两个都默认为空字符串)。

contextnumlines 两个都是可选的关键字参数。当要显示上下文差异时,设置 contextTrue,否则设置默认值为 False 以便显示完整的文件。numlines 默认值为 5contextTrue 时,numlines 控制突出显示差异行的上下文行数。context False 时,numlines 控制在使用“next”超链​​接时显示时突出显示的差异之前的行数(设置为 0 将导致“next”超链​​接将下一个突出显示的差异放置在浏览器的顶部而没有任何前导上下文)。

在版本3.5中已更改: charset仅添加了关键字参数。HTML文档的默认字符集从'ISO-8859-1'更改为'utf-8'

make_table(fromlines, tolines, fromdesc='', todesc='', context=False, numlines=5)

Fromlinestolines (字符串列表) 进行了比较,并返回一个字符串,它是完整 HTML 表显示一行一行的差异与跨线和内线条进行更改突出显示。

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

Tools/scripts/diff.py是命令行前端向此类和包含它的使用很好的例子。

difflib.context_diff(a, b, fromfile='', tofile='', fromfiledate='', tofiledate='', n=3, lineterm='\n')

比较ab(字符串列表);在上下文差异格式中返回Δ(生成Δ行的generator)。

上下文差别是上下文的以紧凑的方式显示已更改的行,再加上的几行。更改显示在前/后风格。n向三个默认设置的上下文行数。

默认情况下,创建尾随换行符 diff 控制线 (那些***---)。这有助于使从io.IOBase.readlines()创建的输入产生适合与io.IOBase.writelines()一起使用的差异,因为输入和输出具有尾随换行符。

对于没有尾随换行符的输入,将lineterm参数设置为"",以便输出将一律换行。

上下文 diff 格式通常有一个标题为文件名和修改时间。使用字符串为fromfiletofilefromfiledatetofiledate可能指定任何或所有这些。修改时间通常是使用 ISO 8601 格式表示的。如果未指定,字符串默认为空格。

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

更详细的示例,请参阅A command-line interface to difflib

difflib.get_close_matches(word, possibilities, n=3, cutoff=0.6)

返回一个最佳的"足够好"匹配列表。word是需要密切匹配的序列(通常是字符串),possibilities是用来与word匹配的序列列表(通常是字符串列表)。

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

可选参数cutoff(默认0.6) 是一个在 [0,1] 范围内的浮点数。Possibilities 中与word相似得分不足的将被忽略

在列表中,首先按相似性得分,最类似排序返回最佳 (不超过n) 匹配之中的可能性。

>>> get_close_matches('appel', ['ape', 'apple', 'peach', 'puppy'])
['apple', 'ape']
>>> import keyword
>>> get_close_matches('wheel', keyword.kwlist)
['while']
>>> get_close_matches('pineapple', keyword.kwlist)
[]
>>> get_close_matches('accept', keyword.kwlist)
['except']
difflib.ndiff(a, b, linejunk=None, charjunk=IS_CHARACTER_JUNK)

比较(字符串列表)ab;返回 Differ 风格的差异(一个生成差异行的 generator)。

可选的关键字参数linejunkcharjunk是过滤函数(或None):

linejunk: 一个函数,接受单个字符串参数,并返回 true,如果字符串是垃圾或假如果不。默认值为None还有一个模块级函数IS_LINE_JUNK(),它过滤掉没有可见字符的行,除了最多一个字符('#') - SequenceMatcher类对哪些行频繁构成噪声进行动态分析,这通常比使用此函数工作更好。

charjunk: 不接受一个字符 (长度为 1 的字符串),并返回如果字符是垃圾或假如果一个函数。默认是模块级函数IS_CHARACTER_JUNK(),它过滤掉空格字符(空格或制表符;在这里包含换行符是个不错的主意!)。

Tools/scripts/ndiff.py是命令行对此函数的前端。

>>> diff = ndiff('one\ntwo\nthree\n'.splitlines(keepends=True),
...              'ore\ntree\nemu\n'.splitlines(keepends=True))
>>> print(''.join(diff), end="")
- one
?  ^
+ ore
?  ^
- two
- three
?  -
+ tree
+ emu
difflib.restore(sequence, which)

返回两个序列生成一个三角洲之一。

给定一个显序列产生的Differ.compare()ndiff(),提取线来自文件,1 或 2 (参数),脱线的前缀。

示例:

>>> diff = ndiff('one\ntwo\nthree\n'.splitlines(keepends=True),
...              'ore\ntree\nemu\n'.splitlines(keepends=True))
>>> diff = list(diff) # materialize the generated delta into a list
>>> print(''.join(restore(diff, 1)), end="")
one
two
three
>>> print(''.join(restore(diff, 2)), end="")
ore
tree
emu
difflib.unified_diff(a, b, fromfile='', tofile='', fromfiledate='', tofiledate='', n=3, lineterm='\n')

比较ab(字符串列表);以统一差分格式返回增量(generator生成增量线)。

统一的差别是上下文的以紧凑的方式显示已更改的行,再加上的几行。更改以内联样式显示(而不是单独的前/后块)。n向三个默认设置的上下文行数。

默认情况下,比较控制线 (那些---+++@@) 创建尾随换行符。这有助于使从io.IOBase.readlines()创建的输入产生适合与io.IOBase.writelines()一起使用的差异,因为输入和输出具有尾随换行符。

对于没有尾随换行符的输入,将lineterm参数设置为"",以便输出将一律换行。

上下文 diff 格式通常有一个标题为文件名和修改时间。使用字符串为fromfiletofilefromfiledatetofiledate可能指定任何或所有这些。修改时间通常是使用 ISO 8601 格式表示的。如果未指定,字符串默认为空格。

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

更详细的示例,请参阅A command-line interface to difflib

difflib.diff_bytes(dfunc, a, b, fromfile=b'', tofile=b'', fromfiledate=b'', tofiledate=b'', n=3, lineterm=b'\n')

使用dfunc比较ab(字节对象列表);以dfunc返回的格式生成一个delta行(也是字节)序列。dfunc必须是可调用的,通常为unified_diff()context_diff()

允许您比较未知或不一致编码的数据。n之外的所有输入必须是字节对象,而不是str。通过将所有输入(除n)无损地转换为str并调用dfunc(a, b, tofile, fromfiledate, tofiledate, n, lineterm)dfunc的输出然后转换回字节,因此您接收的delta线具有与ab相同的未知/不一致编码, 。

版本3.5中的新功能。

difflib.IS_LINE_JUNK(line)

返回 true 可忽略行。线线是可忽略的如果线为空或包含一个单一的'#',否则就不是可忽略。用作旧版本中ndiff()中参数linejunk的默认值。

difflib.IS_CHARACTER_JUNK(ch)

返回 true 可忽略的字符。字符ch是可忽略的如果ch是空格或制表符),否则就不是可忽略。用作参数charjunkndiff()的默认值。

请参见

模式匹配:Gestalt方法
讨论John W. Ratcliff和D. E. Metzener的类似算法。这发表在Dr。 Dobb's Journal

6.3.1.SequenceMatcher对象

SequenceMatcher类具有此构造函数:

class difflib.SequenceMatcher(isjunk=None, a='', b='', autojunk=True)

可选参数isjunk必须是None(默认值) 或一个单参数的函数,采用序列的元素,并返回 true,当且仅当该元素是"垃圾",并且应该忽略。None传递给isjunk等效于传递lambda x: 0 ;换句话说,没有元素被忽略。例如,将传递:

lambda x: x in " \t"

如果您比较行作为序列的字符,并且不希望以同步上空白或硬制表符。

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

可选参数autojunk可以用来禁用自动垃圾启发式算法。

版本3.2中的新功能: autojunk参数。

SequenceMatcher对象获得三个数据属性:bjunkb的元素集合,isjunkTruebpopular是启发式算法流行的非垃圾元素集合(如果未禁用); b2j是将b的其余元素映射到它们出现的位置的列表的dict。bset_seqs()set_seq2()复位时,

版本3.2中的新功能: bjunkbpopular属性。

SequenceMatcher对象具有以下方法:

set_seqs(a, b)

设置两个序列进行比较。

SequenceMatcher计算和缓存有关第二个序列的详细的信息,所以如果你想要比较反对多个序列的一个序列,使用set_seq2()来一次设置常用的序列和set_seq1()反复,一次为每个调用的其他两个序列。

set_seq1(a)

设置要比较的第一个序列。要比较的第二个序列不会更改。

set_seq2(b)

设置要比较的第二个序列。要比较的第一个序列不会更改。

find_longest_match(alo, ahi, blo, bhi)

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

If isjunk was omitted or None, find_longest_match() returns (i, j, k) such that a[i:i+k] is equal to b[j:j+k], where alo <= i <= i+k <= ahi and blo <= j <= j+k <= bhi. 所有(我 ', j', k')满足这些条件,附加条件k > = k' < = 我 ',如果 = =我' j < = j'还会见了。换句话说,所有的最大匹配块,返回一个启动最早的,和所有那些大匹配块的最早在开始,返回在b中最早启动的一个。

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

如果提供了isjunk ,第一次作为上述情况,但没有垃圾的元素出现在块中的附加限制确定最长匹配的块。然后,那块是由匹配 (仅限) 垃圾元素两边都尽量延伸。所以,产生的块永远不会匹配上除了垃圾由于相同的垃圾刚好是毗邻场有趣的比赛。

这里是和以前一样,同样的示例,但考虑空白是垃圾。这可以防止' 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, 血压, 0)

此方法返回named tuple匹配 (a、 b、 大小)

get_matching_blocks()

返回描述匹配子序列的三元组列表。每个三是窗体的(我, j, n),意味着那[i:i + n] = = b [j:j + n]三元组在第一和j递增。

最后三人是假的和具有价值(len(a), len(b), 0)它是唯一的三人房含n = = 0如果(i, j, n)> j', n')是列表中的相邻三元组,第二个不是列表中的最后一个三元组,则 != i'j + n / t14> j';换句话说,相邻三元组总是描述不相邻的相等块。

>>> 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()

5 元组返回列表描述如何将变成b每个元组是窗体的(标记, i1、 i2、 j1、 j2)第一个元组有i1 = = j1 = = 0,还有i1等于从前面的元组,和,同样,等于以前的j2 j1 i2剩余的元组。

标记值是字符串,带有这些含义:

含义
'replace'a[i1:i2]应替换为b[j1:j2]
'delete'a[i1:i2]应被删除。注意,在这种情况下,j1 == j2
'insert'应在a[i1:i1]插入b[j1:j2]请注意,在这种情况下,i1 == i2
'equal'a [i1:i2] == b [j1:j2]

举个例子:

>>> a = "qabxcd"
>>> b = "abycdf"
>>> s = SequenceMatcher(None, a, b)
>>> for tag, i1, i2, j1, j2 in s.get_opcodes():
...     print('{:7}   a[{}:{}] --> b[{}:{}] {!r:>8} --> {!r}'.format(
...         tag, i1, i2, j1, j2, a[i1:i2], b[j1:j2]))
delete    a[0:1] --> b[0:0]      'q' --> ''
equal     a[1:3] --> b[0:2]     'ab' --> 'ab'
replace   a[3:4] --> b[2:3]      'x' --> 'y'
equal     a[4:6] --> b[3:5]     'cd' --> 'cd'
insert    a[6:6] --> b[5:6]       '' --> 'f'
get_grouped_opcodes(n=3)

返回一种generator的上下文的n行与团体。

开始与团体经由get_opcodes(),这种方法拆分出变化的较小群集和消除没有更改的干预范围。

组在get_opcodes()相同的格式返回。

ratio()

在 [0,1] 范围内返回一个浮点数作为序列的相似性度量。

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

这是昂贵的计算如果已经调用没有get_matching_blocks()get_opcodes() ,在这种情况下你可能想要尝试quick_ratio()real_quick_ratio()第一次去一个上限。

quick_ratio()

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

real_quick_ratio()

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

6.3.2.SequenceMatcher示例

此示例比较两个字符串,将空格视为“junk”:

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

ratio()返回一个浮点数在 [0,1],测量序列的相似性。作为一个经验法则,在 0.6 手段的序列是一个ratio()值接近的匹配项:

>>> 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()返回的最后一个元组一直都是形同虚设, (len(a), len(b), 0),这是最后一个元组元素 (元素匹配数目) 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]

请参见

6.3.3.Differ对象

注意Differ的-生成的增量使不自称是极小的差异。与此相反,极小的差异往往违反直觉的因为他们可能,有时意外的比赛 100 页分开角落同步。连续匹配限制同步点保留一些概念的地方,偶尔的成本产生更长的时间差异。

Differ类具有此构造函数:

class difflib.Differ(linejunk=None, charjunk=None)

可选的关键字参数linejunkcharjunk是过滤函数 (或None):

linejunk: 一个函数,接受单个字符串参数,并返回 true,如果字符串是垃圾。默认值是None意义没有线被认为是垃圾。

charjunk: 一个函数,接受单个字符参数 (长度为 1 的字符串),并返回 true,则该字符是垃圾。默认值是None意义的照片中没有字符被认为是垃圾。

这些垃圾过滤功能加速匹配以发现差异,并且不会导致任何不同的行或字符被忽略。请阅读find_longest_match()方法的isjunk参数的说明,以获取说明。

Differ对象通过一个单独的方法是使用 (增量生成):

compare(a, b)

比较两个序列的行,并生成三角洲 (行序列)。

每个序列必须包含以换行符结尾的个别单行字符串。这些序列可索取文件类似物体的readlines()方法。生成的三角洲还包括换行符终止的字符串,准备作为打印-是通过文件类似对象的writelines()方法。

6.3.4.Differ示例

本示例将两个文本进行比较。首先,我们建立的案文,以换行符结尾的各个单行字符串序列 (这种序列可以也得到从文件类似物体的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(keepends=True)
>>> 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(keepends=True)

下一步我们实例化一个不同的对象:

>>> 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.

6.3.5.A command-line interface to difflib

此示例演示如何使用 difflib 来创建一个diff-喜欢实用程序。它也包含在 Python 源代码发行版,作为Tools/scripts/diff.py

#!/usr/bin/env python3
""" 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, argparse
from datetime import datetime, timezone

def file_mtime(path):
    t = datetime.fromtimestamp(os.stat(path).st_mtime,
                               timezone.utc)
    return t.astimezone().isoformat()

def main():

    parser = argparse.ArgumentParser()
    parser.add_argument('-c', action='store_true', default=False,
                        help='Produce a context format diff (default)')
    parser.add_argument('-u', action='store_true', default=False,
                        help='Produce a unified format diff')
    parser.add_argument('-m', action='store_true', default=False,
                        help='Produce HTML side by side diff '
                             '(can use -c and -l in conjunction)')
    parser.add_argument('-n', action='store_true', default=False,
                        help='Produce a ndiff format diff')
    parser.add_argument('-l', '--lines', type=int, default=3,
                        help='Set number of context lines (default 3)')
    parser.add_argument('fromfile')
    parser.add_argument('tofile')
    options = parser.parse_args()

    n = options.lines
    fromfile = options.fromfile
    tofile = options.tofile

    fromdate = file_mtime(fromfile)
    todate = file_mtime(tofile)
    with open(fromfile) as ff:
        fromlines = ff.readlines()
    with open(tofile) as tf:
        tolines = tf.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)

    sys.stdout.writelines(diff)

if __name__ == '__main__':
    main()