【Python】設定ファイルの変更箇所を差分で出す:difflib.unified_diff

PythonのTopに戻る

変更前と変更後を行単位で比べる

設定ファイルのどこが変わったかを確認したいときは、difflib.unified_diff()で追加行・削除行・前後の文脈をまとめて表示できる。文字列全体の一致判定だけでは変更箇所が分からないが、差分なら変えた値と周囲の設定を一緒に見られる。ここでは二つの設定文字列から、人が確認するためのレポートを作る。ファイルの書き換えや差分の自動適用は行わない。

差分はテキストの行を比較した結果であり、設定の意味を理解しているわけではない。コメントの追加や並べ替えも変更になる。設定値の意味だけを比較したい場合は、その形式のパーサーで読み込んだ後に値を比較する。テキスト差分と意味の比較を使い分けると、「見た目が変わっただけ」なのかを判断しやすい。

改行を整理して読みやすく出力する

example.py

from difflib import unified_diff


def change_report(before, after):
    lines = unified_diff(
        before.splitlines(),
        after.splitlines(),
        fromfile="before.ini",
        tofile="after.ini",
        n=1,
        lineterm="",
    )
    return "\n".join(lines)


before = "[run]\nsteps = 10\nmethod = fast\nseed = 7\n"
after = "[run]\nsteps = 20\nmethod = stable\nseed = 7\n"
report = change_report(before, after)
assert "-steps = 10" in report
assert "+steps = 20" in report
assert " seed = 7" in report
assert change_report(before, before) == ""
# This line-oriented report deliberately ignores a final newline difference.
assert change_report("a\n", "a") == ""
print(report)
print("final newline changed:", before.endswith("\n") != after.endswith("\n"))

実行結果

--- before.ini
+++ after.ini
@@ -1,4 +1,4 @@
 [run]
-steps = 10
-method = fast
+steps = 20
+method = stable
 seed = 7
final newline changed: False

差分の記号と文脈行の読み方

先頭の—と+++は比較する文書名であり、古い側がbefore.ini、新しい側がafter.iniである。@@で囲まれた部分は比較範囲の行位置と行数を示す。本文の先頭が-なら古い側から削除された行、+なら新しい側へ追加された行、半角スペースなら両方に存在する文脈行である。設定値の変更は通常、古い行の削除と新しい行の追加の組で表示される。

n=1は変更箇所の前後を1行ずつ表示する指定である。値を増やすと位置が分かりやすくなる一方、レポートは長くなる。変更の近くに節名がない設定ファイルでは、文脈行を増やした方がどの項目なのか分かりやすい。逆に全体が小さなファイルなら、全文を併記した方が確認しやすいこともある。

splitlines()で各行から改行を外し、lineterm=""で差分の制御行にも改行を付けず、最後に一度だけ改行で連結している。各行がすでに改行付きなのにprint()を繰り返すと、空行が余計に入ることがあるためである。unified_diff()の戻り値はイテレーターなので、この例ではjoin()の段階で内容を消費している。もう一度必要なら結果を保存しておく。

このレポートが区別しない変更

この方法では末尾改行の有無やCRLFとLFの違いは行内容の差分から消える。例でも"a\n"と"a"の差分が空になることをassertで明示している。末尾改行が重要な場合はendswith()の比較を別に報告するか、改行を保持した読み込みと出力方式を選ぶ。OSの改行規則まで保存する必要があるなら、読み込み時のnewline設定にも注意する。

ファイルを読む場合は両方のencodingを明示し、文字化けした状態で比較しない。文字コードが違う二つのファイルでも、正しく文字列に戻してからなら内容を比較できる。ただしその差分は元のバイト列が同じかの証明にはならない。ハッシュの比較と役割が違うことを意識しておく。

同じ行が何度も現れる文書では、変更の対応付けが人の想像と違うこともある。差分は変更候補を読み取る助けになるが、特定の編集操作を完全に再現する記録ではない。また、巨大な文書を二つともメモリへ読み込む方式なので、行単位のストリーミング処理と同じメモリ特性ではない。入力サイズの上限を設けると扱いやすい。

設定内容を外へ出す前に確認する

差分には変更した値だけでなく、その前後の行も含まれる。パスワードや接続情報を含む設定をレビューへ送るときは、差分にも秘密が入っていないか確認する。伏字にして比較する場合も、両側に同じ伏字の規則を適用しないと、秘密ではない場所まで変更として見える。まず手元で差分を作り、必要な範囲だけを共有するのがよい。

実行環境と関連情報

掲載コードはLinux上のCPython 3.12.14で実行した。OS固有のファイル操作や対話環境の違いは、本文に記した条件に従って扱う。

関連:保存途中で元ファイルを壊さない:一時ファイルから置き換える / 一致した内容で置換を変える:re.subに関数を渡す

PythonのTopに戻る