6.3. difflib --- 差分の計算を助ける

ソースコード: Lib/difflib.py


このモジュールは、シーケンスを比較するためのクラスや関数を提供しています。例えば、ファイルの差分を計算して、それを HTML や context diff, unified diff などいろいろなフォーマットで出力するために、このモジュールを利用することができます。ディレクトリやファイル群を比較するためには、 filecmp モジュールも参照してください。

class difflib.SequenceMatcher

柔軟性のあるクラスで、二つのシーケンスの要素がハッシュ化できる(hashable)型であれば、どの型の要素を含むシーケンスも比較可能です。基本的なアルゴリズムは、1980年代の後半に発表された Ratcliff と Obershelp による"ゲシュタルトパターンマッチング"と大げさに名づけられたアルゴリズム以前から知られている、やや凝ったアルゴリズムです。その考え方は、"junk" 要素を含まない最も長い互いに隣接したマッチ列を探すことです。ここで、 "junk" 要素とは、空行や空白などの、意味を持たない要素のことです。 (junk を処理するのは、Ratcliff と Obershelp のアルゴリズムに追加された拡張です。)この考え方は、マッチ列の左右に隣接するシーケンスの断片に対して再帰的にあてはめられます。この方法では編集を最小にするシーケンスは生まれませんが、人間の目からみて「正しい感じ」にマッチする傾向があります。

実行時間: 基本的な Ratcliff-Obershelp アルゴリズムは、最悪の場合3乗、期待値で2乗となります。 SequenceMatcher オブジェクトでは、最悪のケースで2乗、期待値は比較されるシーケンス中に共通に現れる要素数に非常にややこしく依存しています。最良の場合は線形時間になります。

自動 junk ヒューリスティック: SequenceMatcher は、シーケンスの特定の要素を自動的に junk として扱うヒューリスティックをサポートしています。このヒューリスティックは、各個要素がシーケンス内に何回現れるかを数えます。ある要素の重複数が (最初のものは除いて) 合計でシーケンスの 1% 以上になり、そのシーケンスが 200 要素以上なら、その要素は "popular" であるものとしてマークされ、シーケンスのマッチングの目的からは junk として扱われます。このヒューリスティックは、 SequenceMatcher の作成時に autojunk パラメタを False に設定することで無効化できます。

バージョン 3.2 で追加: autojunk パラメータ。

class difflib.Differ

テキスト行からなるシーケンスを比較するクラスです。人が読むことのできる差分を作成します。 Differ クラスは SequenceMatcher クラスを利用して、行からなるシーケンスを比較したり、(ほぼ)同一の行内の文字を比較したりします。

Differ クラスによる差分の各行は、2文字のコードで始まります:

コード 意味
'- ' 行はシーケンス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() のドキュメントを参照してください。

以下のメソッドが public になっています:

make_file(fromlines, tolines, fromdesc='', todesc='', context=False, numlines=5, *, charset='utf-8')

fromlinestolines (いずれも文字列のリスト) を比較し、行間または行内の変更点が強調表示された行差分の入った表を持つ完全な HTML ファイルを文字列で返します。

fromdesc および todesc はオプションのキーワード引数で、差分表示テーブルにおけるそれぞれ差分元、差分先ファイルのカラムのヘッダになる文字列を指定します (いずれもデフォルト値は空文字列です)。

context および numlines はともにオプションのキーワード引数です。contextTrue にするとコンテキスト差分を表示し、デフォルトの False にすると完全なファイル差分を表示します。numlines のデフォルト値は 5 で、contextTrue の場合、numlines は強調部分の前後にあるコンテキスト行の数を制御します。contextFalse の場合、numlines は "next" と書かれたハイパーリンクをたどった時に到達する場所が次の変更部分より何行前にあるかを制御します (値をゼロにした場合、"next" ハイパーリンクを辿ると変更部分の強調表示がブラウザの最上部に表示されるようになります)。

バージョン 3.5 で変更: charset キーワード専用引数が追加されました。HTML 文書のデフォルトの文字集合が 'ISO-8859-1' から 'utf-8' に変更されました。

make_table(fromlines, tolines, fromdesc='', todesc='', context=False, numlines=5)

fromlinestolines (いずれも文字列のリスト) を比較し、行間または行内の変更点が強調表示された行差分の入った完全な HTML テーブルを文字列で返します。

このメソッドの引数は、 make_file() メソッドの引数と同じです。

Tools/scripts/diff.py はこのクラスへのコマンドラインフロントエンドで、使い方を学ぶ上で格好の例題が入っています。

difflib.context_diff(a, b, fromfile='', tofile='', fromfiledate='', tofiledate='', n=3, lineterm='\n')

ab (文字列のリスト) を比較し、差分 (差分形式の行を生成するジェネレータ(generator)) を、 context diff のフォーマット(以下「コンテクスト形式」)で返します。

コンテクスト形式は、変更があった行に前後数行を加えてある、コンパクトな表現方法です。変更箇所は、変更前/変更後に分けて表します。コンテクスト (変更箇所前後の行) の行数は n で指定し、デフォルト値は 3 です。

デフォルトで、 diff 制御行 (***--- を含む行) は改行付きで生成されます。 io.IOBase.readlines() で作られた入力が io.IOBase.writelines() で扱うのに適した diff になるので (なぜなら入力と出力の両方が改行付きのため) 、これは有用です。

行末に改行文字を持たない入力に対しては、出力でも改行文字を付加しないように 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']
>>> sys.stdout.writelines(context_diff(s1, s2, fromfile='before.py', tofile='after.py'))
*** before.py
--- after.py
***************
*** 1,4 ****
! bacon
! eggs
! ham
  guido
--- 1,4 ----
! python
! eggy
! hamster
  guido

より詳細な例は、 difflib のコマンドラインインタフェース を参照してください。

difflib.get_close_matches(word, possibilities, n=3, cutoff=0.6)

「十分」なマッチの上位のリストを返します。word はマッチさせたいシーケンス (大概は文字列) です。possibilitiesword にマッチさせるシーケンスのリスト (大概は文字列のリスト) です。

オプションの引数 n (デフォルトでは 3)はメソッドの返すマッチの最大数です。n0 より大きくなければなりません。

オプションの引数 cutoff (デフォルトでは 0.6)は、区間 [0, 1] に入る小数の値です。word との一致率がそれ未満の possibilities の要素は無視されます。

possibilities の要素でマッチした上位(多くても n 個)は、類似度のスコアに応じて(一番似たものを先頭に)ソートされたリストとして返されます。

>>> get_close_matches('appel', ['ape', 'apple', 'peach', 'puppy'])
['apple', 'ape']
>>> import keyword
>>> get_close_matches('wheel', keyword.kwlist)
['while']
>>> get_close_matches('pineapple', keyword.kwlist)
[]
>>> get_close_matches('accept', keyword.kwlist)
['except']
difflib.ndiff(a, b, linejunk=None, charjunk=IS_CHARACTER_JUNK)

ab (文字列のリスト) を比較し、差分 (差分形式の行を生成するジェネレータ(generator)) を、 Differ のスタイルで返します。

オプションのキーワード引数 linejunkcharjunk には、フィルタ関数 (または None) を渡します。

linejunk: 文字列型の引数 1 つを受け取る関数です。文字列が junk の場合は真を、そうでない場合は偽を返します。デフォルトでは None です。モジュールレべルの関数 IS_LINE_JUNK() は、高々 1 つのシャープ記号('#')を除いて可視の文字を含まない行をフィルタリングするものです。しかし、下層にある SequenceMatcher クラスが、どの行が雑音となるほど頻繁に登場するかを動的に分析します。このクラスによる分析は、この関数を使用するよりも通常うまく動作します。

charjunk: 文字 (長さ1の文字列) を受け取る関数です。文字列が junk の場合は真を、そうでない場合は偽を返します。デフォルトでは、モジュールレべルの関数 IS_CHARACTER_JUNK() であり、これは空白文字類 (空白またはタブ、改行文字をこれに含めてはいけません) をフィルタして排除します。

Tools/scripts/ndiff.py は、この関数のコマンドラインのフロントエンド(インターフェイス)です。

>>> diff = ndiff('one\ntwo\nthree\n'.splitlines(keepends=True),
...              'ore\ntree\nemu\n'.splitlines(keepends=True))
>>> print(''.join(diff), end="")
- one
?  ^
+ ore
?  ^
- two
- three
?  -
+ tree
+ emu
difflib.restore(sequence, which)

差分を生成した元の二つのシーケンスのうち一つを返します。

Differ.compare() または ndiff() によって生成された sequence を与えられると、行頭のプレフィクスを取りのぞいてファイル 1 または 2 (引数 which で指定される) に由来する行を復元します。

例:

>>> diff = ndiff('one\ntwo\nthree\n'.splitlines(keepends=True),
...              'ore\ntree\nemu\n'.splitlines(keepends=True))
>>> diff = list(diff) # materialize the generated delta into a list
>>> print(''.join(restore(diff, 1)), end="")
one
two
three
>>> print(''.join(restore(diff, 2)), end="")
ore
tree
emu
difflib.unified_diff(a, b, fromfile='', tofile='', fromfiledate='', tofiledate='', n=3, lineterm='\n')

ab (文字列のリスト) を比較し、差分 (差分形式の行を生成するジェネレータ(generator)) を、 unified diff フォーマット(以下「ユニファイド形式」)で返します。

ユニファイド形式は変更があった行にコンテキストとなる前後数行を加えた、コンパクトな表現方法です。変更箇所は (変更前/変更後を分離したブロックではなく) インラインスタイルで表されます。コンテクストの行数は、n で指定し、デフォルト値は 3 です。

デフォルトで、 diff 制御行 (---, +++, @@ を含む行) は改行付きで生成されます。 io.IOBase.readlines() で作られた入力が io.IOBase.writelines() で扱うのに適した diff になるので (なぜなら入力と出力の両方が改行付きのため) 、これは有用です。

行末に改行文字を持たない入力に対しては、出力でも改行文字を付加しないように 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']
>>> sys.stdout.writelines(unified_diff(s1, s2, fromfile='before.py', tofile='after.py'))
--- before.py
+++ after.py
@@ -1,4 +1,4 @@
-bacon
-eggs
-ham
+python
+eggy
+hamster
 guido

より詳細な例は、 difflib のコマンドラインインタフェース を参照してください。

difflib.diff_bytes(dfunc, a, b, fromfile=b'', tofile=b'', fromfiledate=b'', tofiledate=b'', n=3, lineterm=b'\n')

dfunc を使用して ab (bytes オブジェクトのリスト) を比較して、差分形式の行 (これも bytes オブジェクトです) を*dfunc* の戻り値の形式で返します。dfunc は、呼び出し可能である必要があります。一般に、これは unified_diff() または context_diff() です。

未知のエンコーディングまたは一貫性のないエンコーディングのデータ同士を比較できます。n 以外のすべての入力は、bytes オブジェクトである必要があります。n 以外のすべての入力を損失なく str に変換して、dfunc(a, b, fromfile, tofile, fromfiledate, tofiledate, n, lineterm) を呼び出すことにより動作します。dfunc の出力は、bytes 型に変換されます。これにより、受け取る差分形式の行のエンコーディングは、ab の未知または一貫性のないエンコーディングと同一になります。

バージョン 3.5 で追加.

difflib.IS_LINE_JUNK(line)

無視できる行のとき真を返します。行 line は空白、または '#' ひとつのときに無視できます。それ以外のときには無視できません。古いバージョンでは ndiff() の引数 linejunk にデフォルトで使用されました。

difflib.IS_CHARACTER_JUNK(ch)

無視できる文字のとき真を返します。文字 ch が空白、またはタブ文字のときには無視できます。それ以外の時には無視できません。 ndiff() の引数 charjunk としてデフォルトで使用されます。

参考

Pattern Matching: The Gestalt Approach
John W. Ratcliff と D. E. Metzener による類似のアルゴリズムに関する議論。Dr. Dobb's Journal 1988年7月号掲載。

6.3.1. SequenceMatcherオブジェクト

SequenceMatcher クラスには、以下のようなコンストラクタがあります:

class difflib.SequenceMatcher(isjunk=None, a='', b='', autojunk=True)

オプションの引数 isjunk は、None (デフォルトの値です) にするか、単一の引数をとる関数でなければなりません。後者の場合、関数はシーケンスの要素を受け取り、要素が junk であり、無視すべきである場合に限り真を返すようにしなければなりません。isjunkNone を渡すと、lambda x: 0 を渡したのと同じになります; すなわち、いかなる要素も無視しなくなります。例えば以下のような引数を渡すと:

lambda x: x in " \t"

空白とタブ文字を無視して文字のシーケンスを比較します。

オプションの引数 ab は、比較される文字列で、デフォルトでは空の文字列です。両方のシーケンスの要素は、ハッシュ化可能(hashable)である必要があります。

オプションの引数 autojunk は、自動 junk ヒューリスティックを無効にするために使えます。

バージョン 3.2 で追加: autojunk パラメータ。

SequenceMatcher オブジェクトは3つのデータ属性を持っています: bjunk は、 isjunkTrue であるような b の要素の集合です; bpopular は、 (無効でなければ) ヒューリスティックによって popular であると考えられる非ジャンク要素の集合です; b2j は、 b の残りの要素をそれらが生じる位置のリストに写像する dict です。この 3 つは set_seqs() または set_seq2()b がリセットされる場合は常にリセットされます。

バージョン 3.2 で追加: bjunk および bpopular 属性。

SequenceMatcher オブジェクトは以下のメソッドを持ちます:

set_seqs(a, b)

比較される2つの文字列を設定します。

SequenceMatcher オブジェクトは、2つ目のシーケンスについての詳細な情報を計算し、キャッシュします。 1つのシーケンスをいくつものシーケンスと比較する場合、まず set_seq2() を使って文字列を設定しておき、別の文字列を1つずつ比較するために、繰り返し set_seq1() を呼び出します。

set_seq1(a)

比較を行う1つ目のシーケンスを設定します。比較される2つ目のシーケンスは変更されません。

set_seq2(b)

比較を行う2つ目のシーケンスを設定します。比較される1つ目のシーケンスは変更されません。

find_longest_match(alo, ahi, blo, bhi)

a[alo:ahi]b[blo:bhi] の中から、最長のマッチ列を探します。

isjunk が省略されたか None の時、 find_longest_match()a[i:i+k]b[j:j+k] と等しいような (i, j, k) を返します。その値は alo <= 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 が与えられている場合、上記の通り、はじめに最長のマッチ列を判定します。ブロック内に junk 要素が見当たらないような追加条件の際はこれに該当しません。次にそのマッチ列を、その両側の junk 要素にマッチするよう、できる限り広げていきます。そのため結果となる列は、探している列のたまたま直前にあった同一の junk 以外の junk にはマッチしません。

以下は前と同じサンプルですが、空白を junk とみなしています。これは ' abcd' が2つ目の列の末尾にある ' abcd' にマッチしないようにしています。代わりに 'abcd' にはマッチします。そして 2つ目の文字列中、一番左の '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) を返します。

このメソッドは named tuple Match(a, b, size) を返します。

get_matching_blocks()

Return list of triples describing matching subsequences. Each triple is of the form (i, j, n), and means that a[i:i+n] == b[j:j+n]. The triples are monotonically increasing in i and j.

The last triple is a dummy, and has the value (len(a), len(b), 0). It is the only triple with n == 0. If (i, j, n) and (i', j', n') are adjacent triples in the list, and the second is not the last triple in the list, then i+n != i' or j+n != j'; in other words, adjacent triples always describe non-adjacent equal blocks.

>>> 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()

ab にするための方法を記述する5つのタプルを返します。それぞれのタプルは (tag, i1, i2, j1, j2) という形式であらわされます。最初のタプルは i1 == j1 == 0 であり、i1 はその前にあるタプルの i2 と同じ値です。同様に j1 は前の j2 と同じ値になります。

tag の値は文字列であり、次のような意味です:

意味
'replace' a[i1:i2]b[j1:j2] に置き換えられる。
'delete' a[i1:i2] は削除される。この時、j1 == j2 である。
'insert' b[j1:j2]a[i1:i1] に挿入される。この時 i1 == i2 である。
'equal' a[i1:i2] == b[j1:j2] (サブシーケンスは等しい).

例えば:

>>> a = "qabxcd"
>>> b = "abycdf"
>>> s = SequenceMatcher(None, a, b)
>>> for tag, i1, i2, j1, j2 in s.get_opcodes():
...     print('{:7}   a[{}:{}] --> b[{}:{}] {!r:>8} --> {!r}'.format(
...         tag, i1, i2, j1, j2, a[i1:i2], b[j1:j2]))
delete    a[0:1] --> b[0:0]      'q' --> ''
equal     a[1:3] --> b[0:2]     'ab' --> 'ab'
replace   a[3:4] --> b[2:3]      'x' --> 'y'
equal     a[4:6] --> b[3:5]     'cd' --> 'cd'
insert    a[6:6] --> b[5:6]       '' --> 'f'
get_grouped_opcodes(n=3)

最大 n 行までのコンテクストを含むグループを生成するような、ジェネレータ(generator)を返します。

このメソッドは、 get_opcodes() で返されるグループの中から、似たような差異のかたまりに分け、間に挟まっている変更の無い部分を省きます。

グループは get_opcodes() と同じ書式で返されます。

ratio()

[0, 1] の範囲の浮動小数点数で、シーケンスの類似度を測る値を返します。

T が2つのシーケンスの要素数の総計だと仮定し、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() の上界を、非常に高速に計算します。

この文字列全体のマッチ率を返す3つのメソッドは、精度の異なる近似値を返します。 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

6.3.2. SequenceMatcher の例

この例は2つの文字列を比較します。空白を "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 となる唯一のケースです。

はじめのシーケンスがどのようにして2番目のものになるのかを知るには、 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]

参考

6.3.3. Differ オブジェクト

Differ オブジェクトによって生成された差分が 最小 であるなどとは言いません。むしろ、最小の差分はしばしば直観に反しています。その理由は、どこでもできるとなれば一致を見いだしてしまうからで、ときには思いがけなく100ページも離れたマッチになってしまうのです。一致点を互いに隣接したマッチに制限することで、場合によって長めの差分を出力するというコストを掛けることにはなっても、ある種の局所性を保つことができるのです。

Differ は、以下のようなコンストラクタを持ちます:

class difflib.Differ(linejunk=None, charjunk=None)

オプションのキーワードパラメータ linejunkcharjunk は、フィルタ関数を渡します (使わないときは None):

linejunk: ひとつの文字列引数を受け取る関数です。文字列が junk のときに真を返します。デフォルトでは、None であり、どんな行であっても junk とは見なされません。

charjunk: この関数は文字(長さ1の文字列)を引数として受け取り、文字が junk であるときに真を返します。デフォルトは None であり、どんな文字も junk とは見なされません。

これらの junk フィルター関数により、差分を発見するマッチングが高速化し、差分の行や文字が無視されることがなくなります。説明については、 find_longest_match() メソッドの isjunk 引数の説明をご覧ください。

Differ オブジェクトは、以下の1つのメソッドを通して利用されます。(差分を生成します):

compare(a, b)

文字列からなる2つのシーケンスを比較し、差分(を表す文字列からなるシーケンス)を生成します。

各シーケンスの要素は、改行で終わる独立した単一行からなる文字列でなければなりません。そのようなシーケンスは、ファイル風オブジェクトの readlines() メソッドによって得ることができます。(得られる)差分は改行文字で終了する文字列のシーケンスとして得られ、ファイル風オブジェクトの writelines() メソッドによって出力できる形になっています。

6.3.4. Differ の例

以下の例は2つのテキストを比較しています。最初に、テキストを行毎に改行で終わる文字列のシーケンスにセットアップします (そのようなシーケンスは、ファイル風オブジェクトの 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(keepends=True)
>>> 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(keepends=True)

次に Differ オブジェクトをインスタンス化します:

>>> d = Differ()

注意: Differ オブジェクトをインスタンス化するとき、行 junk と文字 junk をフィルタリングする関数を渡すことができます。詳細は Differ() コンストラクタを参照してください。

最後に、2つを比較します:

>>> result = list(d.compare(text1, text2))

result は文字列のリストなので、pretty-printしてみましょう:

>>> 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.

6.3.5. difflib のコマンドラインインタフェース

この例は、 difflib を使って diff に似たユーティリティーを作成する方法を示します。これは、 Python のソース配布物にも、 Tools/scripts/diff.py として含まれています。

#!/usr/bin/env python3
""" 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, argparse
from datetime import datetime, timezone

def file_mtime(path):
    t = datetime.fromtimestamp(os.stat(path).st_mtime,
                               timezone.utc)
    return t.astimezone().isoformat()

def main():

    parser = argparse.ArgumentParser()
    parser.add_argument('-c', action='store_true', default=False,
                        help='Produce a context format diff (default)')
    parser.add_argument('-u', action='store_true', default=False,
                        help='Produce a unified format diff')
    parser.add_argument('-m', action='store_true', default=False,
                        help='Produce HTML side by side diff '
                             '(can use -c and -l in conjunction)')
    parser.add_argument('-n', action='store_true', default=False,
                        help='Produce a ndiff format diff')
    parser.add_argument('-l', '--lines', type=int, default=3,
                        help='Set number of context lines (default 3)')
    parser.add_argument('fromfile')
    parser.add_argument('tofile')
    options = parser.parse_args()

    n = options.lines
    fromfile = options.fromfile
    tofile = options.tofile

    fromdate = file_mtime(fromfile)
    todate = file_mtime(tofile)
    with open(fromfile) as ff:
        fromlines = ff.readlines()
    with open(tofile) as tf:
        tolines = tf.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)

    sys.stdout.writelines(diff)

if __name__ == '__main__':
    main()