difflib — 用于计算差异的辅助函数

源代码: Lib/difflib.py


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

class difflib.SequenceMatcher

这是一个灵活的类,用于比较任意类型的序列对,只要序列元素是 可哈希的。 基本算法早于 Ratcliff 和 Obershelp 在 20 世纪 80 年代后期以“格式塔模式匹配”这一夸张的名字发表的算法,并且比该算法稍微复杂一些。 其思想是找到最长的不包含“垃圾”元素的连续匹配子序列;这些“垃圾”元素在某种意义上是不感兴趣的元素,例如空行或空格。(处理垃圾是对 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 是可选的关键字参数,用于指定 from/to 文件列标题字符串(两者均默认为空字符串)。

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() 生成的序列,提取源自文件 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 设置,默认为三。

默认情况下,差异控制行(带有 ---+++@@ 的行)会在末尾添加换行符。这很有用,因为从 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 之外的所有输入都必须是字节对象,而不是字符串。通过将所有输入(除 n 之外)无损转换为字符串,并调用 dfunc(a, b, fromfile, tofile, fromfiledate, tofiledate, n, lineterm) 来实现。然后将 dfunc 的输出转换回字节,因此您收到的增量行与 ab 具有相同的未知/不一致编码。

3.5 版新增。

difflib.IS_LINE_JUNK(line)

如果可以忽略行 line,则返回 True。如果 line 为空或仅包含一个 '#',则该行可忽略,否则不可忽略。在旧版本中用作 ndiff() 中参数 linejunk 的默认值。

difflib.IS_CHARACTER_JUNK(ch)

如果可以忽略字符 ch,则返回 True。如果 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。传递 None 作为 isjunk 等效于传递 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 或其值为 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,则首先如上所述确定最长的匹配块,但要附加限制,即块中不应出现垃圾元素。然后,通过匹配两侧的(仅)垃圾元素,尽可能地扩展该块。因此,除非相同的垃圾恰好与有趣的匹配相邻,否则结果块永远不会匹配垃圾。

以下是与之前相同的示例,但将空格视为垃圾。这可以防止 ' 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]。三元组在 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,并且剩余的元组具有等于前一个元组的 *i2* 的 *i1*,以及类似地,等于前一个 *j2* 的 *j1*。

*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:])