difflib
difflib - 计算deltas的助手
2.1版本中的新功能。
该模块提供了用于比较序列的类和函数。它可以用于比较文件,并可以产生各种格式的差异信息,包括HTML和上下文以及统一差异。为了比较目录和文件,请参阅filecmp
模块。
class difflib.SequenceMatcher
这是一个灵活的类,用于比较任何类型的序列对,只要序列元素是可哈希的。基本算法早于20世纪80年代后期由Ratcliff和Obershelp在双曲线名称“gestalt pattern matching”下发布的算法,并且比它更有趣。该想法是找到不包含“junk”元素的最长连续匹配子序列(Ratcliff和Obershelp算法不能解决垃圾问题)。然后递归地将相同的想法应用于匹配子序列的左侧和右侧的序列片段。这不会产生最小的编辑序列,但往往会产生对人们“看起来正确”的匹配。
时机:
基本Ratcliff-Obershelp算法在最坏情况下为立方时间,在预期情况下为二次时间。SequenceMatcher
是最坏情况下的平方时间,并且预期情况行为以复杂的方式依赖于序列有多少共同元素; 最佳案例时间是线性的。
自动垃圾启发式
:SequenceMatcher支持将某些序列项自动视为垃圾的启发式方法。 启发式计算每个单独项目出现在序列中的次数。 如果一个项目的重复(在第一个之后)占序列的1%以上并且序列长度至少为200个项目,则该项目被标记为“流行”并且为了序列匹配而被视为垃圾。 创建SequenceMatcher时,可以通过将autojunk参数设置为False来关闭启发式。
2.7.1版中的新增功能: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
不包装行的位置。
linejunk和charjunk是传入到ndiff()中的可选关键字参数(由HtmlDiff用于生成并排HTML差异)。 有关参数默认值和说明,请参阅ndiff()文档。
以下方法是公开的:
make_file(fromlines, tolines [, fromdesc][, todesc][, context][, numlines])
比较fromlines
和tolines
(字符串列表)并返回一个字符串,该字符串是一个完整的HTML文件,其中包含一个表格,显示逐行显示的行间差异和行内变化。
fromdesc
和todesc
是可选的关键字参数,用于指定文件列标题字符串(均默认为空字符串)。
上下文和numlines都是可选的关键字参数。 在显示上下文差异时将上下文设置为True,否则默认为False以显示完整文件。 numlines默认为5.当context为True时,numlines控制环绕差异高亮的上下文行数。 上下文为False时,numlines控制在使用“下一个”超链接时显示差异高亮之前显示的行数(设置为零将导致“下一个”超链接将下一个差异高亮显示在浏览器顶部,而不带任何前导上下文)。
make_table(fromlines, tolines [, fromdesc][, todesc][, context][, numlines])
比较fromlines
和tolines
(字符串列表)并返回一个字符串,它是一个完整的HTML表格,显示逐行显示的行间差异和行内变化。
该方法的参数与该方法的参数相同make_file()
。
Tools/scripts/diff.py
是这个类的命令行前端,包含了它的一个很好的例子。
2.4版本中的新功能。
difflib.context_diff(a, b[, fromfile][, tofile][, fromfiledate][, tofiledate][, n][, lineterm])
比较a
和b
(字符串列表); 以上下文差异格式返回一个delta(一个产生delta行的发生器)。
上下文差异是一种紧凑的方式,只显示已经改变的行以及几行上下文。所做的更改以前/后样式显示。上下文行的数量由n
设置,缺省值为3。
默认情况下,差异控制行(带***或---的那些行)是使用尾随换行符创建的。 这很有帮助,因此从file.readlines()创建的输入会导致适用于file.writelines()的差异,因为输入和输出都具有尾随换行符。
对于没有尾随换行符的输入,请将lineterm
参数设置为""
使输出将统一换行。
上下文差异格式通常具有文件名和修改时间的标题。任何或所有这些可以使用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版本的新功能。
difflib.get_close_matches(word, possibilities[, n][, cutoff])
返回最佳“足够好”匹配的列表。单词
是需要紧密匹配的序列(通常是一个字符串),可能性
是匹配单词
的序列列表(通常是字符串列表)。
可选参数n
(默认值3
)是要返回的最近匹配的最大数量; n
必须大于0
。
可选参数cutoff
(默认值0.6
)是范围为0,1中的浮点数。忽略至少得分与单词
相似的可能性。
列表中返回可能性中最好的(不超过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
三角形(生成三角形线的发生器)。
可选的关键字参数linejunk
和charjunk
用于过滤器函数(或None
):
linejunk:接受单个字符串参数的函数,如果字符串是垃圾,则返回true;否则返回false。 默认值是(无),从Python 2.3开始。 在此之前,默认值是模块级别的函数IS_LINE_JUNK(),该函数会过滤掉不含可见字符的行,除了最多一个字符('#')。 从Python 2.3开始,底层的SequenceMatcher类会动态分析哪些线路频繁构成噪声,而且这通常比2.3之前的默认值更好。
charjunk
:接受一个字符(一个长度为1的字符串)的函数,如果该字符是垃圾,则返回;否则返回false。默认是模块级函数IS_CHARACTER_JUNK()
,它可以过滤掉空格字符(空白或制表符;注意:在这里包括换行符的错误思路!)。
Tools/scripts/ndiff.py
是这个函数的命令行前端。
>>> 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(sequence, which)
返回生成增量的两个序列之一。
给定由Differ.compare()或ndiff()生成的序列,从文件1或2(参数which)中提取行,剥离行前缀。
例:
>>> 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
(字符串列表); 以统一差异格式返回一个delta(一个产生delta行的发生器)。
统一差异是一种紧凑的方式,只显示已经改变的行和几行上下文。这些更改以内联样式显示(而不是在块之前/之后单独显示)。上下文行的数量由n
设置,缺省值为3。
默认情况下,diff控制行(带有---,+++或@@的行)是使用尾随换行符创建的。 这很有帮助,因此从file.readlines()创建的输入会导致适用于file.writelines()的差异,因为输入和输出都具有尾随换行符。
对于没有尾随换行符的输入,请将lineterm
参数设置为""
使输出将统一换行。
上下文差异格式通常具有文件名和修改时间的标题。任何或所有这些可以使用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版本的新功能。
difflib.IS_LINE_JUNK(line)
对于可忽略的线条返回true。 如果行为空白或包含单个“#”,则该行可以忽略,否则它不可忽略。 在Python 2.3之前,在ndiff()中用作参数linejunk的默认值。
difflib.IS_CHARACTER_JUNK(ch)
对于可忽略的字符返回true。 如果ch是一个空格或制表符,那么字符ch是可以忽略的,否则它是不可忽略的。 用作ndiff()中的参数charjunk的默认值。
扩展内容
模式匹配:Gestalt方法讨论John W. Ratcliff和DE Metzener的类似算法。这是1988年7月在Dobb博士的杂志上发表的。
1. SequenceMatcher对象
这个SequenceMatcher
类有这个构造函数:
class difflib.SequenceMatcher(isjunk=None, a='', b='', autojunk=True)
可选参数isjunk必须是None(缺省值)或一个带有序列元素的单参数函数,并且当且仅当元素是“junk”且应该被忽略时返回true。 传递None给isjunk相当于传递lambda x:0; 换句话说,没有元素被忽略。 例如,通过:
lambda x: x in " \t"
如果您将线条作为字符序列进行比较,并且不想在空白或硬标签上同步。
可选参数a
和b
是要比较的序列; 都默认为空字符串。两个序列的元素必须是可散列的。
可选参数autojunk
可用于禁用自动垃圾启发式。
2.7.1版中的新增功能:autojunk
参数。
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]
。
如果省略了isjunk或None,则find_longest_match()返回(i,j,k)使得[i:i + k]等于b [j:j + k],其中al <= i <= i + k <= ahi和blo <= 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)
。
在版本2.6中更改:此方法返回一个指定的元组 Match(a, b, size)
。
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'; 换句话说,相邻三元组总是描述不相邻的相等块。
在版本2.5中进行了更改:确保相邻三元组始终描述不相邻的块。
>>> 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()
返回列表描述如何变成b的5元组。 每个元组的形式是(标签,i1,i2,j1,j2)。 第一个元组具有i1 == j1 == 0,其余元组的i1等于来自前一个元组的i2,同样,j1等于先前的j2。
该标签
值是字符串,这些含义:
值 | 含义 |
---|---|
'更换' | ai1:i2应该由bj1:j2代替。 |
'删除' | ai1:i2应该被删除。请注意,在这种情况下j1 == j2。 |
'插' | bj1:j2应该插入ai1:i1。请注意,在这种情况下i1 == i2。 |
'等于' | ai1:i2 == bj1:j2。 |
例如:
>>> 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])
返回最多包含n
行上下文的组的生成器。
从返回的组开始get_opcodes()
,此方法分割出更小的变更集群,并消除没有变化的干预范围。
这些组以与格式相同的格式返回get_opcodes()
。
2.3版本的新功能。
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
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]
扩展内容
- 该
get_close_matches()
模块中的功能显示了如何使用简单的代码构建SequenceMatcher
来完成有用的工作。
- 用于构建小型应用程序的简单版本控制配方
SequenceMatcher
。
3.不同对象
请注意,Differ
生成的增量不会声称是最小的
差异。相反,最小差异常常是违反直觉的,因为它们可以在任何可能的地方同步,有时意外地相隔100页。限制连续匹配的同步点保留了一些局部性的概念,偶尔会产生更长的差异。
这个Differ
类有这个构造函数:
class difflib.Differ([linejunk[, charjunk]])
可选的关键字参数linejunk
和charjunk
用于过滤器函数(或None
):
linejunk
:接受单个字符串参数的函数,如果字符串是垃圾,则返回true。默认值是None
,这意味着没有行被认为是垃圾。
charjunk
:接受单个字符参数(长度为1的字符串)的函数,如果字符是垃圾,则返回true。默认值是None
,这意味着没有人物被认为是垃圾。
Differ
通过一种方法使用对象(生成增量):
compare(a, b)
比较两个序列的行,并生成delta(一系列行)。
每个序列必须包含以换行符结尾的单个单行字符串。 这些序列可以从文件类对象的readlines()方法获得。 生成的增量也由换行符终止的字符串组成,可以通过类似文件对象的writelines()方法按原样打印。
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.
5. difflib的命令行界面
这个例子展示了如何使用difflib创建一个diff-like工具。 它也包含在Python源代码发行版中,作为Tools / scripts / diff.py。
""" 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)
fromlines = open(fromfile, 'U').readlines()
tolines = open(tofile, 'U').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()