Python Module之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()方法只會標記修改的行和一些上下文環境。

# 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,見下表)和兩對代表序列起始位置的下表,i1i2j1j2

指令(opcode) 定義
‘replace’ a[i1:i2]替換成b[j1:j2]
‘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))
        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類可以處理自定義的類,也可以處理內置類,只要它們是可以哈希的。

原文點這裏

參考:

1.difflib模塊的官方文檔

2.“Pattern Matching: The Gestalt Approach” - 對一個相似算法的討論 by John W. Ratcliff 和 D. E. Metzener

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章