Skip to main content

6.3. difflib —计算增量的帮助者

源代码: Lib/difflib.py


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

class difflib.SequenceMatcher

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

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

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

3.2 新版功能: autojunk 参数。

class difflib.Differ

这是一个类,用于比较文本行的序列,并产生人类可读的差异或增量。 Differ使用 SequenceMatcher 来比较线的序列,并且比较类似(近匹配)线内的字符序列。

Differ 增量的每一行以双字母代码开头:

含义

'- '

行对序列1唯一

'+ '

行对序列2唯一

'  '

两个序列共有的

'? '

线不存在于任一输入序列中

以“ ? ”开头的行试图引导眼睛到内向差异,并且不存在于任一输入序列中。如果序列包含选项卡字符,这些行可能会混淆。

class difflib.HtmlDiff

此类可用于创建一个并排显示的HTML表(或包含表的完整HTML文件),逐行比较文本与行间变化和行内变化亮点。该表可以在完全或上下文差分模式下生成。

这个类的构造函数是:

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

初始化 HtmlDiff 的实例。

tabsize 是一个可选的关键字参数,用于指定制表符间距,默认为 8

wrapcolumn 是一个可选的关键字,用于指定行号被折断和换行的列号,缺省为 None,其中行不被换行。

linejunkcharjunk 是传递到 ndiff() 的可选关键字参数(由 HtmlDiff 用于生成并行HTML差异)。有关参数默认值和描述,请参阅 ndiff() 文档。

以下方法是公共的:

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

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

fromdesctodesc 是可选的关键字参数,用于指定从/到文件列标题字符串(默认为空字符串)。

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

在 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 设置,默认为三。

默认情况下,差异控制线(具有 ***--- 的线)使用拖尾换行符创建。这是有帮助的,使得从 io.IOBase.readlines() 创建的输入导致适合与 io.IOBase.writelines() 一起使用的差异,因为输入和输出都具有尾随换行。

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

上下文差异格式通常具有文件名和修改时间的头。可以使用 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

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

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

返回最好的“足够好”匹配的列表。 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('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:接受单个字符串参数的函数,如果字符串是junk则返回true,否则返回false。默认值为 None。还有一个模块级函数 IS_LINE_JUNK(),它过滤掉没有可见字符的行,但最多只有一个字符('#') - 然而,基础 SequenceMatcher 类动态分析哪些行频繁构成噪声,这通常比使用此功能更好。

charjunk:接受字符(长度为1的字符串)的函数,如果字符是垃圾则返回,否则返回false。默认是模块级函数 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() 产生的 sequence,提取源自文件1或2(参数 which)的行,剥离行前缀。

例:

>>> 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 参数设置为 "",以便输出将一致地换行。

上下文差异格式通常具有文件名和修改时间的头。可以使用 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

有关更详细的示例,请参阅 到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, fromfile, tofile, fromfiledate, tofiledate, n, lineterm) 来工作。 dfunc 的输出然后被转换回字节,因此您接收的三角线具有与 ab 相同的未知/不一致的编码。

3.5 新版功能.

difflib.IS_LINE_JUNK(line)

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

difflib.IS_CHARACTER_JUNK(ch)

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

参见

模式匹配:Gestalt方法

讨论John W. Ratcliff和D. E. Metzener的类似算法。这是1988年7月在 Dr. Dobb’s Journal 发表的。

6.3.1. SequenceMatcher对象

SequenceMatcher 类有这个构造函数:

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

可选参数 isjunk 必须是 None (默认值)或单参数函数,它接受一个序列元素,并且当且仅当元素为“junk”且应被忽略时返回true。传递 Noneisjunk 相当于传递 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] 中查找最长匹配块。

如果省略 isjunkNone,则 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,则首先如上确定最长匹配块,但是具有在块中不出现垃圾元素的附加限制。然后,通过在两侧匹配(仅)垃圾元素尽可能地扩展该块。所以结果块永远不匹配垃圾,除非相同的垃圾恰好与一个有趣的匹配相邻。

这里是和以前相同的例子,但考虑空白是垃圾。这防止 ' 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)

此方法返回 named tuple Match(a, b, size)

get_matching_blocks()

描述匹配子序列的三元组的返回列表。每个三元组具有形式 (i, j, n),并且意味着 a[i:i+n] == b[j:j+n]。三元组在 ij 中单调增加。

最后一个三元组是一个虚拟,并且具有值 (len(a), len(b), 0)。它是唯一与 n == 0 的三元组。如果 (i, j, n)(i', j', n') 是列表中的相邻三元组,并且第二个不是列表中的最后三元组,则 i+n != i'j+n != 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()

描述如何将 a 转换为 b 的5元组的返回列表。每个元组的形式是 (tag, i1, i2, j1, j2)。第一元组具有 i1 == j1 == 0,并且剩余的元组具有等于来自先前元组的 i2i1,同样地,j1 等于先前的 j2

tag 值是字符串,具有以下含义:

含义

'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] (子序列相等)。

例如:

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

返回具有高达 n 上下文行的组的 generator

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

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

ratio()

将范围[0,1]中的序列相似性的度量返回为浮点。

其中T是两个序列中元素的总数,M是匹配数,这是2.0 * M / T。注意,如果序列相同,这是 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]中的浮点,测量序列的相似性。作为经验法则,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() 返回的最后一个元组总是一个虚拟的 (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 - 生成的增量不声称是 最小 差异。相反,最小差异通常是反直觉的,因为它们在任何可能的地方同步,有时偶然匹配100页。将同步点限制为连续匹配保留了局部性的一些概念,偶尔产生较长差异的代价。

Differ 类有这个构造函数:

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

可选的关键字参数 linejunkcharjunk 用于过滤器功能(或 None):

linejunk:接受单个字符串参数的函数,如果字符串是junk,则返回true。默认值为 None,这意味着没有行被认为是垃圾。

charjunk:接受单个字符参数(长度为1的字符串)的函数,如果字符是垃圾则返回true。默认值为 None,这意味着没有字符被认为是垃圾。

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

通过单个方法使用 Differ 对象(生成delta):

compare(a, b)

比较两个序列的行,并生成增量(行序列)。

每个序列必须包含以换行符结尾的单个单行字符串。这样的序列可以从文件状对象的 readlines() 方法获得。生成的delta还包括以换行符结尾的字符串,可以通过文件状对象的 writelines() 方法按原样打印。

6.3.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(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. 到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, 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()