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で加減する

関連記事

あわせて読むと理解が深まります。

対面・無料|東京・永田町で毎週土曜日13時

1回の参加で、プログラミングの構造がわかる

ゼロからはじめるPython入門講座

8年間・累計600回開催/これまで全国で2万人以上が参加

「やはり対面で聞けるのはとても気づきがあり、価値があると思います」

参加者の声

  • 本当に初心者からの講座です。プログラミングに触ったことがなくて大丈夫です
  • 対面だから、その場で聞けます。つまずいた所を持ち帰らずに解決できます
  • 講座は1時間。そのあとも残って学習を続けられ、講師にも質問できます
  • 独学で挫折した方も。糸口が見つかって、講座のあと会員になる方が多くいます

無料で参加する|開催日程を見る

参加費は無料です