模块目的:比较序列,尤其是多行文本。
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()
方法只会标记修改的行和一些上下文环境。
# difflib_unified.py
import difflib
from difflib_data import *
diff = difflib.unified_diff(text1_lines, text2_lines, lineterm='')
print('\n'.join(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. 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.
使用context_diff()
也可以产生相似的输出。
无用的数据
所有的比较差异化的方法都可以接收参数来控制哪些文本行需要被忽略,文本行中的哪些字符需要被忽略。这些参数可以用来跳过标记符或者空格所引起的不同。比如下例:
# difflib_junk.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 sapces 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()
方法会返回调整第一个序列,使之匹配第二个序列的命令列表。这些命令是具有5个元素的元组,包括一个指令字符串(opcode,见下表)和两对代表序列起始位置的下表,i1
,i2
,j1
和j2
。
指令(opcode) | 定义 |
‘replace’ | 将 |
‘delete’ | 去除 |
‘insert’ | 在 |
‘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))
print(' before =', s1)
del s1[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
类可以处理自定义的类,也可以处理内置类,只要它们是可以哈希的。
参考:
2.“Pattern Matching: The Gestalt Approach” - 对一个相似算法的讨论 by John W. Ratcliff 和 D. E. Metzener