difflib — 用于计算差异的辅助工具

源代码: Lib/difflib.py


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

class difflib.SequenceMatcher

这是一个灵活的类,用于比较任何类型的序列对,只要序列元素是可哈希的。 基本算法早于 1980 年代后期 Ratcliff 和 Obershelp 以夸张的名称“格式塔模式匹配”发表的算法,并且比它稍复杂一些。其思想是找到不包含“垃圾”元素的最长连续匹配子序列;这些“垃圾”元素在某种意义上是不感兴趣的,例如空白行或空格。(处理垃圾是对 Ratcliff 和 Obershelp 算法的扩展。)然后将相同的想法递归地应用于匹配子序列的左侧和右侧的序列片段。这不会产生最小编辑序列,但往往会产生“看起来对”人的匹配。

时间:基本的 Ratcliff-Obershelp 算法在最坏的情况下是立方时间,在预期情况下是平方时间。SequenceMatcher 在最坏的情况下是平方时间,并且其预期情况的行为以复杂的方式依赖于序列有多少个共同的元素;最佳情况时间是线性的。

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

在 3.2 版本中更改: 添加了 *autojunk* 参数。

class difflib.Differ

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

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

代码

含义

'- '

'+ '

'+ '

'- '

' ' ' '

两个序列共有的行

'? '

两个输入序列中都不存在的行

以“?”开头的行尝试引导眼睛注意行内差异,并且不存在于任何一个输入序列中。如果序列包含空格字符(例如空格、制表符或换行符),这些行可能会令人困惑。

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 控制在差异突出显示之前显示的行数(设置为零会导致“下一个”超链接将下一个差异突出显示放置在浏览器顶部,而没有任何前导上下文)。

注意

fromdesctodesc 被解释为未转义的 HTML,并且在接收来自不受信任的来源的输入时应正确转义。

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

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

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

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

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

比较 ab (字符串列表);以上下文差异格式返回增量(一个 生成器,生成增量行)。

上下文差异是一种紧凑的方式,只显示已更改的行以及一些上下文行。更改以前后样式显示。上下文行的数量由 n 设置,默认为 3。

默认情况下,差异控制行(带有 ***--- 的行)会创建一个尾随换行符。这很有用,这样从 io.IOBase.readlines() 创建的输入将产生适合与 io.IOBase.writelines() 一起使用的差异,因为输入和输出都有尾随换行符。

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

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

>>> import sys
>>> from difflib import *
>>> 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 样式的增量(一个 生成器,生成增量行)。

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

linejunk:一个接受单个字符串参数的函数,如果字符串是垃圾则返回 true,否则返回 false。默认值为 None。还有一个模块级函数 IS_LINE_JUNK(),它会过滤掉没有可见字符的行,但最多会保留一个井号字符 ('#') - 但是,底层的 SequenceMatcher 类会对哪些行过于频繁而构成噪声进行动态分析,这通常比使用此函数效果更好。

charjunk:一个接受字符(长度为 1 的字符串)的函数,如果该字符是垃圾则返回 true,否则返回 false。默认值是模块级函数 IS_CHARACTER_JUNK(),它会过滤掉空格字符(空格或制表符;在此处包含换行符不是一个好主意!)。

>>> 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 (字符串列表);以统一差异格式返回增量(一个 生成器,生成增量行)。

统一差异是一种紧凑的方式,只显示已更改的行以及一些上下文行。更改以内联样式显示(而不是单独的前后块)。上下文行的数量由 n 设置,默认为 3。

默认情况下,差异控制行(带有 ---+++@@ 的行)会创建一个尾随换行符。这很有用,这样从 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 返回的格式生成一系列增量行(也是字节)。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 的默认值。

另请参阅

模式匹配:格式塔方法

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

SequenceMatcher 对象

SequenceMatcher 类具有以下构造函数

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

可选参数 isjunk 必须为 None (默认值)或一个接受序列元素并返回 true 的单参数函数,当且仅当该元素是“垃圾”并且应该被忽略时。为 isjunk 传递 None 等同于传递 lambda x: False;换句话说,不忽略任何元素。例如,传递

lambda x: x in " \t"

如果您将行作为字符序列进行比较,并且不希望在空格或硬制表符上同步。

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

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

在 3.2 版本中更改: 添加了 *autojunk* 参数。

SequenceMatcher 对象具有三个数据属性:bjunkbisjunkTrue 的元素集合;bpopular 是启发式方法(如果未禁用)认为流行的非垃圾元素集合;b2j 是将 b 的其余元素映射到它们出现的位置列表的字典。每当使用 set_seqs()set_seq2() 重置 b 时,所有三个属性都会被重置。

3.2 版本新增: bjunkbpopular 属性。

SequenceMatcher 对象具有以下方法

set_seqs(a, b)

设置要比较的两个序列。

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

set_seq1(a)

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

set_seq2(b)

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

find_longest_match(alo=0, ahi=None, blo=0, bhi=None)

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

如果省略了 isjunk 或为 Nonefind_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)

此方法返回一个 命名元组 Match(a, b, size)

在 3.9 版本中更改: 添加了默认参数。

get_matching_blocks()

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

最后一个三元组是一个虚拟的三元组,值为 (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,其余元组的 *i1* 等于前一个元组的 *i2*,同样地,*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* 行上下文的组的生成器

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() 来获得上限。

注意

注意:ratio() 调用的结果可能取决于参数的顺序。例如

>>> SequenceMatcher(None, 'tide', 'diet').ratio()
0.25
>>> SequenceMatcher(None, 'diet', 'tide').ratio()
0.5
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

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() 返回的最后一个元组始终是一个虚拟元组,(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]

另请参阅

Differ 对象

请注意,Differ 生成的增量并不能声称是最小差异。相反,最小差异通常是反直观的,因为它们会在任何可能的地方同步,有时会在 100 页之外偶然匹配。将同步点限制为连续匹配保留了局部性的概念,偶尔会以产生更长的差异为代价。

Differ 类具有以下构造函数

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

可选关键字参数 *linejunk* 和 *charjunk* 用于筛选函数(或 None

linejunk: 一个接受单个字符串参数的函数,如果该字符串是垃圾则返回 true。默认值为 None,表示不认为任何行是垃圾。

charjunk: 一个接受单个字符参数(长度为 1 的字符串)的函数,如果该字符是垃圾则返回 true。默认值为 None,表示不认为任何字符是垃圾。

这些垃圾过滤函数可以加快匹配以查找差异,并且不会导致任何不同的行或字符被忽略。有关解释,请阅读 find_longest_match() 方法的 isjunk 参数的说明。

Differ 对象通过一个方法使用(生成差异)

compare(a, b)

比较两个行序列,并生成差异(行序列)。

每个序列必须包含以换行符结尾的单独单行字符串。这些序列可以从类似文件的对象的 readlines() 方法获得。生成的差异也由以换行符结尾的字符串组成,可以通过类似文件的对象的 writelines() 方法按原样打印。

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)

接下来,我们实例化一个 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.

difflib 的命令行接口

此示例演示如何使用 difflib 创建类似 diff 的实用程序。

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

ndiff 示例

此示例演示如何使用 difflib.ndiff()

"""ndiff [-q] file1 file2
    or
ndiff (-r1 | -r2) < ndiff_output > file1_or_file2

Print a human-friendly file difference report to stdout.  Both inter-
and intra-line differences are noted.  In the second form, recreate file1
(-r1) or file2 (-r2) on stdout, from an ndiff report on stdin.

In the first form, if -q ("quiet") is not specified, the first two lines
of output are

-: file1
+: file2

Each remaining line begins with a two-letter code:

    "- "    line unique to file1
    "+ "    line unique to file2
    "  "    line common to both files
    "? "    line not present in either input file

Lines beginning with "? " attempt to guide the eye to intraline
differences, and were not present in either input file.  These lines can be
confusing if the source files contain tab characters.

The first file can be recovered by retaining only lines that begin with
"  " or "- ", and deleting those 2-character prefixes; use ndiff with -r1.

The second file can be recovered similarly, but by retaining only "  " and
"+ " lines; use ndiff with -r2; or, on Unix, the second file can be
recovered by piping the output through

    sed -n '/^[+ ] /s/^..//p'
"""

__version__ = 1, 7, 0

import difflib, sys

def fail(msg):
    out = sys.stderr.write
    out(msg + "\n\n")
    out(__doc__)
    return 0

# open a file & return the file object; gripe and return 0 if it
# couldn't be opened
def fopen(fname):
    try:
        return open(fname)
    except IOError as detail:
        return fail("couldn't open " + fname + ": " + str(detail))

# open two files & spray the diff to stdout; return false iff a problem
def fcompare(f1name, f2name):
    f1 = fopen(f1name)
    f2 = fopen(f2name)
    if not f1 or not f2:
        return 0

    a = f1.readlines(); f1.close()
    b = f2.readlines(); f2.close()
    for line in difflib.ndiff(a, b):
        print(line, end=' ')

    return 1

# crack args (sys.argv[1:] is normal) & compare;
# return false iff a problem

def main(args):
    import getopt
    try:
        opts, args = getopt.getopt(args, "qr:")
    except getopt.error as detail:
        return fail(str(detail))
    noisy = 1
    qseen = rseen = 0
    for opt, val in opts:
        if opt == "-q":
            qseen = 1
            noisy = 0
        elif opt == "-r":
            rseen = 1
            whichfile = val
    if qseen and rseen:
        return fail("can't specify both -q and -r")
    if rseen:
        if args:
            return fail("no args allowed with -r option")
        if whichfile in ("1", "2"):
            restore(whichfile)
            return 1
        return fail("-r value must be 1 or 2")
    if len(args) != 2:
        return fail("need 2 filename args")
    f1name, f2name = args
    if noisy:
        print('-:', f1name)
        print('+:', f2name)
    return fcompare(f1name, f2name)

# read ndiff output from stdin, and print file1 (which=='1') or
# file2 (which=='2') to stdout

def restore(which):
    restored = difflib.restore(sys.stdin.readlines(), which)
    sys.stdout.writelines(restored)

if __name__ == '__main__':
    main(sys.argv[1:])