Python difflibとは?差分と文字列の類似度を求める方法
difflibは、2つの列の違いを求める標準ライブラリです。テキストの差分表示と、文字列の類似度計算に使います。
追加インストールは不要で、import difflibだけで使えます。
差分を表示する(unified_diff)
gitの差分と同じ形式で出ます。
import difflib
a = ["りんご\n", "みかん\n", "ぶどう\n"]
b = ["りんご\n", "いちご\n", "ぶどう\n", "もも\n"]
print("".join(difflib.unified_diff(a, b, fromfile="old.txt", tofile="new.txt")), end="")
実行結果:
--- old.txt
+++ new.txt
@@ -1,3 +1,4 @@
りんご
-みかん
+いちご
ぶどう
+もも
入力は行のリストで、各行に改行を付けておきます。改行が無いと差分の表示が崩れます。ファイルから読むならf.readlines()がそのまま使えます。
unified_diff()はジェネレータを返すので、print()にそのまま渡さず"".join()かforで回します。
ndiff はもっと細かく出る
print("".join(difflib.ndiff(a, b)), end="")
実行結果:
りんご
- みかん
+ いちご
ぶどう
+ もも
行の中のどの文字が違うかまで出る場合は?で始まる行が付きます。人が読むなら ndiff、機械やレビューに回すなら unified_diffという使い分けになります。
類似度を数値で出す(SequenceMatcher)
pairs = [("プログラミング", "プログラマ"), ("abcdef", "abcdxf"), ("python", "Python")]
for x, y in pairs:
r = difflib.SequenceMatcher(None, x, y).ratio()
print(f"{x!r} vs {y!r} -> {r:.4f}")
実行結果:
'プログラミング' vs 'プログラマ' -> 0.6667
'abcdef' vs 'abcdxf' -> 0.8333
'python' vs 'Python' -> 0.8333
ratio()は0.0〜1.0を返します。1.0が完全一致です。
大文字と小文字は別の文字として扱われます。pythonとPythonが0.8333しかないのはそのためです。区別したくなければ事前にlower()で揃えます。
quick_ratio は順序を見ていない
sm = difflib.SequenceMatcher(None, "プログラミング学習", "学習プログラミング")
print("ratio :", round(sm.ratio(), 4))
print("quick_ratio:", round(sm.quick_ratio(), 4))
実行結果:
ratio : 0.7778
quick_ratio: 1.0
並び替えただけの文字列が quick_ratio では 1.0 になります。quick_ratio()は使われている文字の種類と数だけを見る高速版なので、順序が重要な場面では使えません。
ふるい落とし用にquick_ratio()で粗く絞り、残ったものだけratio()で正確に測る、という使い方が本来の用途です。
似た候補を提示する(get_close_matches)
打ち間違いに対して「もしかして」を出せます。
words = ["append", "extend", "insert", "remove", "reverse", "sort"]
print(difflib.get_close_matches("apend", words))
print(difflib.get_close_matches("sorted", words, n=2, cutoff=0.5))
print(difflib.get_close_matches("zzz", words))
実行結果:
['append']
['sort', 'insert']
[]
n… 返す最大件数(既定3)cutoff… この類似度未満は捨てる(既定0.6)- 候補が無ければ空のリスト。
Noneではない
cutoffを下げるとsortedにinsertまで出てきました。緩めると無関係なものが混ざるので、0.6前後から調整します。
どこがどう変わったかを取る(get_opcodes)
sm = difflib.SequenceMatcher(None, "abcdef", "abXdefY")
for tag, i1, i2, j1, j2 in sm.get_opcodes():
print(f"{tag:7} a[{i1}:{i2}]={'abcdef'[i1:i2]!r:9} b[{j1}:{j2}]={'abXdefY'[j1:j2]!r}")
実行結果:
equal a[0:2]='ab' b[0:2]='ab'
replace a[2:3]='c' b[2:3]='X'
equal a[3:6]='def' b[3:6]='def'
insert a[6:6]='' b[6:7]='Y'
自分で差分表示を作りたいときはこれが土台になります。tagはequal・replace・insert・deleteの4種類です。
HTMLの差分表
table = difflib.HtmlDiff().make_table(a, b)
print(len(table))
print(table[:40])
実行結果:
1487
<table class="diff" id="difflib_chg
make_file()ならCSS込みの完全なHTMLが出ます。レビュー画面を手早く作るときに便利です。
行ではなく文字で比較するとき
SequenceMatcher に渡すのは「列」であれば何でもよいので、文字列をそのまま渡せば文字単位、リストを渡せば要素単位の比較になります。
逆にunified_diff()とndiff()は行のリストを前提にしています。文字列をそのまま渡すと1文字ずつの差分になってしまうので注意してください。
まとめ
- 差分表示は
unified_diff()、人が読むならndiff() ratio()は0.0〜1.0。大文字小文字は別物quick_ratio()は順序を見ない。絞り込み用- 候補の提示は
get_close_matches()。cutoffで加減する
関連記事
あわせて読むと理解が深まります。




