difflib -- 比较序列
概览:对序列进行比较,尤其是比较多行的文本。
difflib
模块包含了用于计算和处理序列之间差异的工具。它对于比较文本非常有用,并且它还包括了能够按照一些常见格式生成差异报告的函数。
本节的实例将使用difflib_data.py
模块中的通用测试数据:
#difflib_data.py
text1 = """Lorem ipsum dolor sit amet, consectetuer adipiscing
elit. Integer eu lacus accumsan arcu fermentum euismod. Donec
pulvinar porttitor tellus. Aliquam venenatis. Donec facilisis
pharetra tortor. In nec mauris eget magna consequat
convalis. Nam sed sem vitae odio pellentesque interdum. Sed
consequat viverra nisl. Suspendisse arcu metus, blandit quis,
rhoncus ac, pharetra eget, velit. Mauris urna. Morbi nonummy
molestie orci. Praesent nisi elit, fringilla ac, suscipit non,
tristique vel, mauris. Curabitur vel lorem id nisl porta
adipiscing. Suspendisse eu lectus. In nunc. Duis vulputate
tristique enim. Donec quis lectus a justo imperdiet tempus."""
text1_lines = text1.splitlines()
text2 = """Lorem ipsum dolor sit amet, consectetuer adipiscing
elit. Integer eu lacus accumsan arcu fermentum euismod. Donec
pulvinar, porttitor tellus. Aliquam venenatis. Donec facilisis
pharetra tortor. In nec mauris eget magna consequat
convalis. Nam cras vitae mi vitae odio pellentesque interdum. Sed
consequat viverra nisl. Suspendisse arcu metus, blandit quis,
rhoncus ac, pharetra eget, velit. Mauris urna. Morbi nonummy
molestie orci. Praesent nisi elit, fringilla ac, suscipit non,
tristique vel, mauris. Curabitur vel lorem id nisl porta
adipiscing. Duis vulputate tristique enim. Donec quis lectus a
justo imperdiet tempus. Suspendisse eu lectus. In nunc."""
text2_lines = text2.splitlines()
比较文本体
Differ
类用于处理文本行的序列,能够产生适于人类阅读的文本差异或修改说明,包括每一行的差异。Differ
的默认输出,类似于Unix下的diff
命令行工具,它包括两个列表中的原始输入值,以及包括用于指示修改的常用值和标记数据。
- 以
-
为前缀的行,表示该行出现在第一个序列,不存在于第二个序列 - 以
+
为前缀的行,表示该行出现在第二个序列,不存在于第一个序列 - 如果某一行在版本间有增量差异,那么还会有以
?
开头的一行,用于突出显示新版本中的修改 - 如果某一行没有改变,它将在最左侧额外打印一个空格,从而和其他有差异的行输出对齐。
在把大段字符串传给compare()
之前,可以把它分解为由文本行组成的序列,从而能产生更可读的结果。
# difflib_differ.py
import difflib
from difflib_data import *
d = difflib.Differ()
diff = d.compare(text1_lines, text2_lines)
print('\n'.join(diff))
在案例数据中的两段文本开始是相同的,所以第一行输入没有任何额外的标记。
Lorem ipsum dolor sit amet, consectetuer adipiscing
elit. Integer eu lacus accumsan arcu fermentum euismod. Donec
在修改后的数据的第三行,包含了一个逗号。这一行的两个版本都被打印了出来,并且输出中新增的第5行额外信息显示了文本中被修改的列,我们也可以从中看出,修改后的文本新增了一个字符。
- pulvinar porttitor tellus. Aliquam venenatis. Donec facilisis
+ pulvinar, porttitor tellus. Aliquam venenatis. Donec facilisis
? +
接下来的几行输出中,可以看出一些多余的空格被删除了。
- pharetra tortor. In nec mauris eget magna consequat
? -
+ pharetra tortor. In nec mauris eget magna consequat
接下来,发生了更复杂的变化,一段文本中的记歌词被替换了。
- convalis. Nam sed sem vitae odio pellentesque interdum. Sed
? - --
+ convalis. Nam cras vitae mi vitae odio pellentesque interdum. Sed
? +++ +++++ +
段落中的最后一句话发生了非常大的变化,所以这一差别,被表达为从就版本中删除了一行,并在新版本中新增了一行。
consequat viverra nisl. Suspendisse arcu metus, blandit quis,
rhoncus ac, pharetra eget, velit. Mauris urna. Morbi nonummy
molestie orci. Praesent nisi elit, fringilla ac, suscipit non,
tristique vel, mauris. Curabitur vel lorem id nisl porta
- adipiscing. Suspendisse eu lectus. In nunc. Duis vulputate
- tristique enim. Donec quis lectus a justo imperdiet tempus.
+ adipiscing. Duis vulputate tristique enim. Donec quis lectus a
+ justo imperdiet tempus. Suspendisse eu lectus. In nunc.
ndiff()
函数能够产生完全相同的输出。它的处理过程被特意定制,使得它能够在处理文本数据的同时能够忽略一些来自输入中的“噪声”。
其他输出格式
Differ
类的输出包括输入的每一行,而统一差异(unified diff)仅包括被修改的行,和一些必要的上下文。接下来介绍的unified_diff()
函数能够产生这种类型的输出。
# difflib_unified.py
import difflib
from difflib_data import *
diff = difflib.unified_diff(
text1_lines,
text2_lines,
lineterm='',
)
print('\n'.join(list(diff)))
lineterm
参数用于告诉unified_diff()
不需要在输出中的每个控制行后追加新的一行,因为输入行不包括它们。在打印的时候,每一行都会增加换行。对于许多版本控制工具的用户来说,这些输出看起来应该会很熟悉。
$ python3 difflib_unified.py
---
+++
@@ -1,11 +1,11 @@
Lorem ipsum dolor sit amet, consectetuer adipiscing
elit. Integer eu lacus accumsan arcu fermentum euismod. Donec
-pulvinar porttitor tellus. Aliquam venenatis. Donec facilisis
-pharetra tortor. In nec mauris eget magna consequat
-convalis. Nam sed sem vitae odio pellentesque interdum. Sed
+pulvinar, porttitor tellus. Aliquam venenatis. Donec facilisis
+pharetra tortor. In nec mauris eget magna consequat
+convalis. Nam cras vitae mi vitae odio pellentesque interdum. S
ed
consequat viverra nisl. Suspendisse arcu metus, blandit quis,
rhoncus ac, pharetra eget, velit. Mauris urna. Morbi nonummy
molestie orci. Praesent nisi elit, fringilla ac, suscipit non,
tristique vel, mauris. Curabitur vel lorem id nisl porta
-adipiscing. Suspendisse eu lectus. In nunc. Duis vulputate
-tristique enim. Donec quis lectus a justo imperdiet tempus.
+adipiscing. Duis vulputate tristique enim. Donec quis lectus a
+justo imperdiet tempus. Suspendisse eu lectus. In nunc.
使用context_diff()
产生相似的可读输出。
垃圾数据
对于所有产生对比序列的对比函数来说,它们都可以接受用于指定那些行可以被忽略、一行中的那些字符可以被忽略的参数。例如,使用这些参数可以使对比函数忽略文件的两个版本中标记或空格的更改。
# difflib_junk.py
# This example is adapted from the source for difflib.py
from difflib import SequenceMatcher
def show_results(match):
print(' a = {}'.format(match.a))
print(' b = {}'.format(match.b))
print(' size = {}'.format(match.size))
i, j, k = match
print(' A[a:a+size] = {!r}'.format(A[i:i + k]))
print(' B[b:b+size] = {!r}'.format(B[j:j + k]))
A = " abcd"
B = "abcd abcd"
print('A = {!r}'.format(A))
print('B = {!r}'.format(B))
print('\nWithout junk detection:')
s1 = SequenceMatcher(None, A, B)
match1 = s1.find_longest_match(0, len(A), 0, len(B))
show_results(match1)
print('\nTreat spaces as junk:')
s2 = SequenceMatcher(lambda x: x == " ", A, B)
match2 = s2.find_longest_match(0, len(A), 0, len(B))
show_results(match2)
Differ
的默认行为,是不去显式忽略任何行或字符,而是依赖SequenceMatcher
的检测噪声的能力。ndiff()
的默认行为是忽略空格和制表符。
$ python3 difflib_junk.py
A = ' abcd'
B = 'abcd abcd'
Without junk detection:
a = 0
b = 4
size = 5
A[a:a+size] = ' abcd'
B[b:b+size] = ' abcd'
Treat spaces as junk:
a = 1
b = 0
size = 4
A[a:a+size] = 'abcd'
B[b:b+size] = 'abcd'
比较任意类型数据
如果数据是可哈希的,那么SequenceMatcher
类可以比较任意类型的两个序列。它使用一种算法来识别序列中最长的连续匹配块,并消除实际数据中对实际数据没有意义的“垃圾”数据。
get_opcodes()
返回一个指令列表,用于描述第一个序列通过某种修改后能和第二个序列匹配的操作。指令被编码为一个五元组,包括一个字符串指令(“操作码”,见下表),和两组表达序列中开始和结束位置的索引(表示为i1
、i2
、j1
和j2
)。
difflib.get_opcodes() 指令
OpCode | Definition |
---|---|
'replace' | 使用b[j1:j2]替换a[i1:i2] |
'delete' | 整个删除a[i1:i2] |
'insert' | 在a[i1:i1]位置插入b[j1:j2] |
'equal' | 子序列已经相同 |
# difflib_seq.py
import difflib
s1 = [1, 2, 3, 5, 6, 4]
s2 = [2, 3, 5, 4 ,6, 1]
print('Initial data:')
print('s1 = ', s1)
print('s2 = ', s2)
print('s1 == s2:', s1 == s2)
print()
matcher = difflib.SequenceMatcher(None, s1, s2)
for tag, i1, i2, j1, j2 in reversed(matcher.get_opcodes()):
if tag == 'delete':
print('Remove {} from positions [{}:{}]'.format(
s1[i1:i2], i1, i2))
elif tag == 'equal':
print('s1[{}:{}] and s2[{}:{}] are the same'.format(
i1, i2, j1, j2))
elif tag == 'insert':
print('Insert {} from s2[{}:{}] into s1 at {}'.format(
s2[j1:j2], j1, j2, i1))
print(' before =', s1)
s1[i1:i2] = s2[j1:j2]
elif tag == 'replace':
print(('Replace {} from s1[{}:{}] '
'with {} from s2[{}:{}]').format(
s1[i1:i2], i1, i2, s2[j1:j2], j1, j2))
print(' before =', s1)
s1[i1:i2] = s2[j1:j2]
print(' after =', s1, '\n')
print('s1 == s2:', s1 == s2)
这个例子比较两个整数列表,并使用get_opcodes()
导出将原始列表转换为新版本的指令。这些修改被反序执行,以保证元素在添加和删除后,列表索引保持准确。
$ python3 difflib_seq.py
Initial data:
s1 = [1, 2, 3, 5, 6, 4]
s2 = [2, 3, 5, 4, 6, 1]
s1 == s2: False
Replace [4] from s1[5:6] with [1] from s2[5:6]
before = [1, 2, 3, 5, 6, 4]
after = [1, 2, 3, 5, 6, 1]
s1[4:5] and s2[4:5] are the same
after = [1, 2, 3, 5, 6, 1]
Insert [4] from s2[3:4] into s1 at 4
before = [1, 2, 3, 5, 6, 1]
after = [1, 2, 3, 5, 4, 6, 1]
s1[1:4] and s2[0:3] are the same
after = [1, 2, 3, 5, 4, 6, 1]
Remove [1] from positions [0:1]
before = [1, 2, 3, 5, 4, 6, 1]
after = [2, 3, 5, 4, 6, 1]
s1 == s2: True
SequenceMatcher
类适用于自定义类和内置类型,前提是它们是可哈希的。
See Also
- difflib的标准库文档(Python 3.5)
- “Pattern Matching: The Gestalt Approach” -- Discussion of a similar algorithm by John W. Ratcliff and D. E. Metzener published in Dr. Dobb’s Journal in July, 1988.