difflibで文字列の差分比較をする【Python】

Python

概要

difflibは文字列比較のために使うPython標準モジュールです。モジュール内の各クラスやメソッドの使用法は公式ドキュメントが詳しいですが、モジュール内の構成がやや分かりにくい印象だったので、自分なりに整理してみたのが本記事です。

Pythonでの文字列差分比較をdifflibで行うための分かりやすいまとめ、が本記事の目的なので、SequenceMatcherクラス(文字列の類似度を求める)とかの使い方には言及していません。要は、WinMergeでできるような、2つのテキストファイルを比較して差異を表示する、ということをするイメージです。

difflibモジュールの全体像

クラスと関数が含まれています。

クラスは、

  • SequenceMatcher
  • Differ
  • HtmlDiff

関数は、

  • context_diff
  • get_close_matches
  • ndiff
  • restore
  • unified_diff
  • diff_bytes

があります。

関数のうち、.ndiff()はDifferの.compare()関数を呼び出しているだけのようです(要するに同じもの)。difflibモジュール自体がPythonで記述されていて、GitHubで確認できます(リンクは以下)。

で、文字列の差分比較(差分を検出して表示する)という目的であれば、Differ、HtmlDiff、.ndiff()、.context_diff()、.unified_diff()を使用するのが、目的に沿うと思われます。ただ、それぞれ、結果として出力する形式が異なるようです。

今回は簡単に使いたいので、 .ndiff() 、.context_diff () 、.unified_diff() 関数をそれぞれ試し、どのような結果が返ってくるのか、比較して記事にしたいと思います。
また、HtmlDiffは結果をHTML形式で出力しますので、これも使用してみます。

.ndiff()関数

上で書いた通り、Diffクラスの.compare() 関数を内部で呼び出しているだけです。公式ドキュメントによると、Differスタイルで結果を返すとありますね。公式マニュアルによれば、Differ形式は、”producing human-readable differences” (人間が読みやすい差異を生成する)のだそうです。確かに、下に上げる例の中では、一番見やすいと思います。

import difflib

str_1 = '''\
1行目
2行目
3行目
'''

str_2 = '''\
先頭行追加
1行目
2行目(変更)
'''

res = difflib.ndiff(str_1.split(), str_2.split())

print('\n'.join(res))

実行すると、こんな結果が出力されます。これが、「Differの形式」ということですかね。
新規追加された行には、行頭に”+”記号が付き、消去された行には、”-“記号が付与されています。修正された行には、”+”と”-“が両方あります。

+ 先頭行追加
  1行目
+ 2行目(変更)
- 2行目
- 3行目

.context_diff()関数

この関数は、diffのコンテキスト形式 (Context Format)で結果を出力するようです。他にも、ユニファイド形式 (Unified Format)があります(こちらは次で解説)。

詳しくは、Wikipediaにありましたので、そちらを参照して頂いたほうが分かりやすいでしょう。

コードは以下の通り。

import difflib

str_1 = '''\
1行目
2行目
3行目
'''

str_2 = '''\
先頭行追加
1行目
2行目(変更)
'''

res = difflib.context_diff(str_1.split(), str_2.split())

print('\n'.join(res))

結果は、下の通り。

***

---

***************

*** 1,3 ****

  1行目
! 2行目
! 3行目
--- 1,3 ----

+ 先頭行追加
  1行目
! 2行目(変更)

Diff形式のほうが見やすいし、分かりやすいですね。

.unified_diff()関数

上で書いた通り、diffの形式の1つだそうです(ユニファイド形式)。

コードは以下の通り。

import difflib

str_1 = '''\
1行目
2行目
3行目
'''

str_2 = '''\
先頭行追加
1行目
2行目(変更)
'''

res = difflib.unified_diff(str_1.split(), str_2.split())

print('\n'.join(res))

結果は以下の通り。

---

+++

@@ -1,3 +1,3 @@

+先頭行追加
 1行目
-2行目
-3行目
+2行目(変更)

上のWikipediaによれば「より少量の出力でより読みやすい形式を追求し生まれた」ものがユニファイド形式だそうで、確かにコンテキスト形式よりは見やすいですが、上で挙げた中ではDiffer形式が一番見やすいですね。

HtmlDiffクラス

こちらは単純なテキスト形式ではなくHTML形式で出力をします。

.make_file()関数

<HTML>タグを含む、HTMLファイル全体の形式で、テキストが出力されます。
コード例は以下。

import difflib

str_1 = '''\
1行目
2行目
3行目
'''

str_2 = '''\
先頭行追加
1行目
2行目(変更)
'''

df = difflib.HtmlDiff()
res = df.make_file(str_1.split(), str_2.split(),)
    
with open('html_diff.html', 'w', encoding='utf-8') as f:
    f.write(res)

今回はファイルに出力していますが、こんな感じでHTMLファイルが出力されますので、

ファイルを開くと、こんな感じで色付きで表示されます。

make_file()の例

HTMLとして出力するなら、こちらのほうが分かりやすそうですね。

.make_table()関数

こちらは、TABLEタグの部分(↑の例の上部分)だけを出力します。

import difflib

str_1 = '''\
1行目
2行目
3行目
'''

str_2 = '''\
先頭行追加
1行目
2行目(変更)
'''

df = difflib.HtmlDiff()
res = df.make_table(str_1.split(), str_2.split())
    
with open('html_diff.html', 'w', encoding='utf-8') as f:
    f.write(res)

styleがないので、だいぶシンプルな表示ですが、こんな感じで出力されます。

プレーンテキストで、かつシンプルな見た目で良いのであれば、.ndff()関数(もしくはDiffクラス)で良さそうです。HTML形式で出したい場合、HtmlDiffクラスを使うと良さそうです。

【応用】変更箇所のみを表示させる

.ndiff()関数だと、変更された行の行頭には必ず、”+”か”-“が付くようなので、それで判断してやれば変更箇所だけを出力できるでしょう(だいぶ簡単な例なので、これで全て思い通りにいくかは分かりませんが)。

import difflib

str_1 = '''\
1行目
2行目
3行目
'''

str_2 = '''\
先頭行追加
1行目
2行目(変更)
'''

res = difflib.ndiff(str_1.split(), str_2.split())

for r in res:
    if r[0:1] in ['+', '-']:
        print(r) 

結果は、こんな感じです。

+ 先頭行追加
+ 2行目(変更)
- 2行目
- 3行目

テキストの差分を比較して、とりあえず見やすい結果だけを表示させたい場合、.ndiff()関数(もしくはDiffクラス)を使用してやれば、簡単に結果が出力できるようです。

以上です。

参考リンク