difflib – シーケンスを比較する

目的:特にテキストファイルの行単位での、シーケンスを比較する
利用できるバージョン:2.1 以上

difflib モジュールはシーケンス間の差異を算出するためのツールを提供します。特にテキストの比較に便利で、複数の共通 diff フォーマットを用いたレポートを生成する機能があります。

本稿で紹介する全てのサンプルは 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 convallis. 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 convallis. 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 が生成するデフォルト出力は diff コマンドツールによく似ています。共通の値や何が変更されているかを表すマークアップデータを含みつつ、両方のリストからのオリジナルの入力値も含みます。

  • 1番目のシーケンスにはその入力値があったことを表すために行の先頭へ - が付けられるかもしれませんが、2番目にはありません。
  • 2番目のシーケンスには + が行の先頭へ付けられますが、1番目にはありません。
  • ある行で文字の追加のみ行われたら、その行のどの位置かを表すために追加の行の先頭へ ? が付けられます。
  • もし何も変更がなければ、他の行が何かのマークアップを持つ可能性があるので行の先頭には余分なスペースが表示されます。

テキストを比較するには、行単位でシーケンスを分割して compare() にそのシーケンスを渡します。

import difflib
from difflib_data import *

d = difflib.Differ()
diff = d.compare(text1_lines, text2_lines)
print '\n'.join(diff)

サンプルデータの両方のテキストの1行目は同じなので、最初の行は先頭に何もマークアップされずに表示されます。

1:   Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Integer

2行目はテキストにカンマを入れるように変更しました。両方のバージョンのテキスト行が表示されます。さらに4行目には、そのテキストで実際に , の文字が追加されたカラム位置を表す追加情報を表示します。

2: - eu lacus accumsan arcu fermentum euismod. Donec pulvinar porttitor
3: + eu lacus accumsan arcu fermentum euismod. Donec pulvinar, porttitor
4: ?                                                         +
5:

その出力の6-9行目は余分なスペースが削除された位置を表示します。

6: - tellus. Aliquam venenatis. Donec facilisis pharetra tortor.  In nec
7: ?                                                             -
8:
9: + tellus. Aliquam venenatis. Donec facilisis pharetra tortor. In nec

次はあるフレーズ内の単語を置き換えるといったもっと複雑な変更が行われました。

10: - mauris eget magna consequat convallis. Nam sed sem vitae odio
11: ?                                              - --
12:
13: + mauris eget magna consequat convallis. Nam cras vitae mi vitae odio
14: ?                                            +++ +++++   +
15:

そして、そのパラグラフの最後の文章はかなり変更されました。そのため、その差分は単純に旧バージョンを削除して新バージョンを追加することで表現されます(20-23行目)。

16:   pellentesque interdum. Sed consequat viverra nisl. Suspendisse arcu
17:   metus, blandit quis, rhoncus ac, pharetra eget, velit. Mauris
18:   urna. Morbi nonummy molestie orci. Praesent nisi elit, fringilla ac,
19:   suscipit non, tristique vel, mauris. Curabitur vel lorem id nisl porta
20: - adipiscing. Suspendisse eu lectus. In nunc. Duis vulputate tristique
21: - enim. Donec quis lectus a justo imperdiet tempus.
22: + adipiscing. Duis vulputate tristique enim. Donec quis lectus a justo
23: + imperdiet tempus. Suspendisse eu lectus. In nunc.

ndiff() 関数は基本的に同じ出力を生成します。

import difflib
from difflib_data import *

diff = difflib.ndiff(text1_lines, text2_lines)
print '\n'.join(list(diff))

その処理は特に入力の “ノイズ” を取り除いてテキストデータを扱うことに適しています。

$ python difflib_ndiff.py
  Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Integer
- eu lacus accumsan arcu fermentum euismod. Donec pulvinar porttitor
+ eu lacus accumsan arcu fermentum euismod. Donec pulvinar, porttitor
?                                                         +

- tellus. Aliquam venenatis. Donec facilisis pharetra tortor.  In nec
?                                                             -

+ tellus. Aliquam venenatis. Donec facilisis pharetra tortor. In nec
- mauris eget magna consequat convallis. Nam sed sem vitae odio
?                                             ------

+ mauris eget magna consequat convallis. 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.

他の出力フォーマット

Differ クラスは全ての入力行を表示しますが、 unified 形式の diff は変更された行と前後の数行のみを含みます。Python 2.3 では、unified 形式の出力を生成するために unified_diff() 関数が追加されました。

import difflib
from difflib_data import *

diff = difflib.unified_diff(text1_lines, text2_lines, lineterm='')
print '\n'.join(list(diff))

subversion 又は他のバージョン管理ツールのユーザはその出力に見覚えがあるでしょう。

$ python difflib_unified.py
---
+++
@@ -1,10 +1,10 @@
 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 convallis. Nam sed sem vitae odio
+eu lacus accumsan arcu fermentum euismod. Donec pulvinar, porttitor
+tellus. Aliquam venenatis. Donec facilisis pharetra tortor. In nec
+mauris eget magna consequat convallis. 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() を使用するとよく似た読み易い出力を生成します。

$ python difflib_context.py
***
---
***************
*** 1,10 ****
  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 convallis. 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.
--- 1,10 ----
  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 convallis. 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.

HTML 出力

HtmlDiffDiff と同じような情報を HTML で生成します。

import difflib
from difflib_data import *

d = difflib.HtmlDiff()
print d.make_table(text1_lines, text2_lines)

このサンプルは make_table() を使用して、差異情報を含んだ table タグのみを返します。 make_file() メソッドを使用すると完全な HTML フォーマットのファイルを出力します。

Note

その出力はあまりに冗長なのでここでは紹介しません。

不要なデータ

diff シーケンスを生成する全ての関数は、無視すべき行と行内の文字を表す引数を受け取ります。そういった引数は、ファイルの2つのバージョン間でマークアップかスペースの変更を無視するために使用されます。

# This example is taken from the source for difflib.py.

from difflib import SequenceMatcher

A = " abcd"
B = "abcd abcd"

print 'A = %r' % A
print 'B = %r' % B

print '\nWithout junk detection:'

s = SequenceMatcher(None, A, B)
i, j, k = s.find_longest_match(0, 5, 0, 9)
print '  i = %d' % i
print '  j = %d' % j
print '  k = %d' % k
print '  A[i:i+k] = %r' % A[i:i+k]
print '  B[j:j+k] = %r' % B[j:j+k]

print '\nTreat spaces as junk:'

s = SequenceMatcher(lambda x: x==" ", A, B)
i, j, k = s.find_longest_match(0, 5, 0, 9)
print '  i = %d' % i
print '  j = %d' % j
print '  k = %d' % k
print '  A[i:i+k] = %r' % A[i:i+k]
print '  B[j:j+k] = %r' % B[j:j+k]

Differ のデフォルトはどのような行や印字可能文字も無視しませんが、ノイズを検出するために SequenceMatcher の機能に依存します。 ndiff() のデフォルトはスペースやタブ文字を無視します。

$ python difflib_junk.py
A = ' abcd'
B = 'abcd abcd'

Without junk detection:
  i = 0
  j = 4
  k = 5
  A[i:i+k] = ' abcd'
  B[j:j+k] = ' abcd'

Treat spaces as junk:
  i = 1
  j = 0
  k = 4
  A[i:i+k] = 'abcd'
  B[j:j+k] = 'abcd'

任意の型を比較する

SequenceMatcher は、オブジェクトがハッシュ化できる限り、どのようなオブジェクト型のシーケンスでも比較します。それは実際のデータに影響しない “不要な” 値を取り除いて、そのシーケンスからもっとも長いマッチングブロックを識別するアルゴリズムを使用します。

import difflib
from difflib_data import *

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 %s from positions [%d:%d]' % (s1[i1:i2], i1, i2)
        del s1[i1:i2]

    elif tag == 'equal':
        print 'The sections [%d:%d] of s1 and [%d:%d] of s2 are the same' % \
            (i1, i2, j1, j2)

    elif tag == 'insert':
        print 'Insert %s from [%d:%d] of s2 into s1 at %d' % \
            (s2[j1:j2], j1, j2, i1)
        s1[i1:i2] = s2[j1:j2]

    elif tag == 'replace':
        print 'Replace %s from [%d:%d] of s1 with %s from [%d:%d] of s2' % (
            s1[i1:i2], i1, i2, s2[j1:j2], j1, j2)
        s1[i1:i2] = s2[j1:j2]

    print 's1 =', s1
    print 's2 =', s2
    print

print 's1 == s2:', s1==s2

このサンプルは2つの整数値リストを比較します。そして get_opcodes() を使用して、 オリジナルのリストを新たなリストへ変換する命令セットを表示します。その変更は、リストの要素が追加・削除された後でインデックスが正確に維持されるように逆の順番で適用されます。

$ python 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 [5:6] of s1 with [1] from [5:6] of s2
s1 = [1, 2, 3, 5, 6, 1]
s2 = [2, 3, 5, 4, 6, 1]

The sections [4:5] of s1 and [4:5] of s2 are the same
s1 = [1, 2, 3, 5, 6, 1]
s2 = [2, 3, 5, 4, 6, 1]

Insert [4] from [3:4] of s2 into s1 at 4
s1 = [1, 2, 3, 5, 4, 6, 1]
s2 = [2, 3, 5, 4, 6, 1]

The sections [1:4] of s1 and [0:3] of s2 are the same
s1 = [1, 2, 3, 5, 4, 6, 1]
s2 = [2, 3, 5, 4, 6, 1]

Remove [1] from positions [0:1]
s1 = [2, 3, 5, 4, 6, 1]
s2 = [2, 3, 5, 4, 6, 1]

s1 == s2: True

SequenceMatcher は、それらハッシュ化できる限り、ビルトイン型と同様に独自クラスで動作します。

See also

difflib
本モジュールの標準ライブラリドキュメント
Pattern Matching: The Gestalt Approach
1998年7月 Dr. Dobb のジャーナルに John W. Ratcliff と D. E. Metzener による似たようなアルゴリズムの議論が発表されました。

テキスト処理ツール

Bookmark and Share