mygems

CSVファイルを2つ比較して変更点を見つける方法

diffは行単位の比較で、並べ替えただけでも全面変更に見えます。キーで比較し、追加、削除、変更を見つける方法です。

執筆: uos ·

diff old.csv new.csv では、知りたいことに答えられません。diff は行単位で比較するため、並び順が違うだけのファイルは「すべて変更」と判定されます。また、1つのフィールドだけを修正した行は、変更ではなく「削除と追加」の組として出力されます。

必要なのは、キーによる比較です。どの行が新しく増えたか、どの行がなくなったか、そして両方にある行ではどのフィールドが違うか。3つの問いには、それぞれ別の答え方が必要です。

まず形を確認する

内容を比べる前に、2つのファイルが比較できる状態かを確認します。

head -n 1 old.csv new.csv
wc -l old.csv new.csv

比較結果がおかしくなる原因で最も多いのは、ヘッダーの変更です。2回のエクスポートの間に列が追加、改名、並べ替えされていると、その点を考慮するまで、フィールド単位の比較はすべて意味を失います。次に見るのが行数です。差が3行なら普通の1週間分ですが、4万行も違うなら、どちらかのエクスポートが途中で切れているか、絞り込まれているのがたいていの原因です。

SQLで行う3つの問いの比較

最初にキーを決めます。同じレコードを両方のファイルで識別できる列です。id、order_id、email などが該当します。キーがない場合は、最後のセクションをご覧ください。

新しいファイルにだけある行:

duckdb -c "SELECT * FROM read_csv('new.csv') n WHERE n.id NOT IN (SELECT id FROM read_csv('old.csv'))"

古いファイルにだけある行:

duckdb -c "SELECT * FROM read_csv('old.csv') o WHERE o.id NOT IN (SELECT id FROM read_csv('new.csv'))"

両方にあり、フィールドが変わった行:

duckdb -c "SELECT o.id, o.status old_status, n.status new_status FROM read_csv('old.csv') o JOIN read_csv('new.csv') n USING (id) WHERE o.status IS DISTINCT FROM n.status"

重要なのは、<> ではなく IS DISTINCT FROM を使う点です。<> はどちらかが NULL のとき NULL を返すため、空欄から入力済みへ、または入力済みから空欄へ変わったフィールドが、気づかないうちに結果から漏れます。探していた変更は、まさにそれである可能性が高いはずです。

列名を1つずつ書かずに、すべての列の変更をまとめて拾うには次のようにします。

duckdb -c "SELECT * FROM read_csv('old.csv') EXCEPT SELECT * FROM read_csv('new.csv')"

EXCEPT は行全体を比較し、順序を無視します。そのため、diff よりずっと優れた diff として使えます。ただし、行が変わったことは分かっても、どのフィールドかは分かりません。見るべき行が絞れたら、上の JOIN と組み合わせてください。

並び順が違うだけのとき

内容が同じかどうかだけ知りたいなら、ソートしてハッシュを取ります。

tail -n +2 old.csv | sort | md5sum
tail -n +2 new.csv | sort | md5sum

ハッシュが同じなら、行の順序に関係なく内容は同じです。ハッシュが違っても、何が変わったかは分かりません。それでも1秒で済み、「これは同じエクスポートか」という疑問に決着がつきます。調査の中身が、実はそれだけだったという場合も少なくありません。

並べ替えに強い行単位の diff は、こうします。

diff <(sort old.csv) <(sort new.csv)

あくまで行単位なので、1フィールドの変更は相変わらず1組の行として表示されますが、少なくとも並び順が出力を埋め尽くすことはなくなります。

専用ツール

csvdiff と daff はどちらもキーで比較し、2行を並べて見せる代わりに「行41で列 status が A から B に変わった」という形で出力します。日常的に比較するなら、どちらかを入れておくとよいでしょう。daff diff old.csv new.csv --output diff.html を実行すると、色付きの表が生成されます。ひと目で読めるのは、ここまでの方法にはない長所です。

Git は、ファイルをバージョン管理している場合に使えます。.gitattributes に CSV 用の diff ドライバーを設定すると、データファイルに対する git diff が役に立たないものから使えるものに変わります。変更はたった2行です。

Excel は、正直なところこの作業が苦手です。よくあるやり方は、列ごとに別シートへ VLOOKUP をかける方法です。小さなファイルなら動きますが、大きなファイルでは処理量が二乗で増え、#N/A だらけの表が読みにくく残ります。Power Query のマージのほうがはるかに優れていますが、ほとんど誰も使っていません。

スマートフォンで差分を読む

実際によくあるのは、1週間空けた2つのエクスポートがメールに届いていて、知りたいのは「照合レポートを作ること」ではなく「何が変わったか」という場面です。

現実的な方法は、diff ではありません。どちらのファイル由来かを示す列を追加し、2つのファイルを縦に連結し、キーで並べ替えて、ペアを読みます。各レコードの2つのバージョンが隣り合うので、変更されたフィールドが横並びで見えます。数十行程度なら、どんなツールより速い方法です。

CSV File Viewer - Smart CSViPhone・iPad · Android

ファイルに直接SQLを書くだけで比較は完結します。キーでのJOINとIS DISTINCT FROMで変更されたフィールドが見つかり、SQLを書くより言葉で聞きたい質問はAIアシスタントが引き受けます。

できること

数値、日付、そして偽の差分

比較すると、実際は変更ではないものが大量に「変更」として出てきます。調べる前に、これらを除外しましょう。

  • 末尾の空白。 "active" と "active " は別の値です。空白に意味がある場合を除き、trim() した値で比較します。意味があることはほぼありません。
  • 数値の書式。 1000、1,000、1000.00 は、1つの数値でありながら3つの文字列です。比較の前に、両側を数値型にキャストします。
  • 日付の書式。 01/02/2026 と 2026-02-01 の違い、さらに厄介なのは、同じ 01/02/2026 が片方のファイルでは2月、もう片方では1月を意味するケースです。文字列として比べず、両方を日付として解析します。
  • 浮動小数点の誤差。 0.1 + 0.2 は、どの言語でも 0.3 にはなりません。小数を比較する前に、必要な精度に丸めます。
  • 文字コード。 片方のファイルで José、もう片方で José と表示される名前は、文字コードの違いであり、データの変更ではありません。

こうした問題は、大きなファイルでは1つだけで数千件の差分を生みます。比較結果が「すべて変更あり」になってしまうのは、そのためです。

CSV Editor - Smart CSViPhone・iPad

比較が修正作業に変わる場面、たとえば列の正規化、小数の丸め、実際に変わった行の修正では、数式列で全行に一括適用できます。結果はCSVの中にそのまま保存されます。

できること

キーがない場合

ログのエクスポートが2つ、取引の一覧が2つといったように、識別子のないファイルもあります。選択肢は2つで、どちらも妥協を含みます。

複合キーを作る。 行を一意に決められる複数の列を組み合わせます。たとえば、日付と金額と摘要です。組み合わせが本当に一意なら有効ですが、そうでなければ気づかないうちに失敗します。同じ日の同一取引が2件あると1件にまとまり、比較は存在しない「削除」を報告します。

多重集合として比較する。 各ファイルで、重複を除いた各行の出現回数を数え、その回数を比べます。ログや取引のように、同じ行が正当に何度も現れ、意味のある変更が何回現れるかである場合は、この方法が適しています。

duckdb -c "SELECT *, count(*) n FROM read_csv('old.csv') GROUP BY ALL"

これを両方のファイルに実行して、n 列を比べます。重複の多いデータを正しく扱えるのは、この方法だけです。

よくある質問

2つのCSVファイルを比較して違いを見つけるには?
行単位ではなく、キーで比較します。3つの問いに分けて考えます。新しいファイルにだけあるキーはどれか、古いファイルにだけあるキーはどれか、両方にあるキーでどのフィールドが違うか。SQLでは、アンチ結合2つと、列ごとに `IS DISTINCT FROM` を使う結合1つで表せます。単純な `diff` は、1フィールドの変更を削除と追加の組として報告し、並べ替えたファイルを全面変更として報告します。
データが同じに見えるのに、diffがすべての行が変更されたと言うのはなぜ?
ほとんどの場合、2つのエクスポートの並び順が違うためです。`diff` は行を順番に比較するので、並べ替えたファイルは全面的に書き換えられたように見えます。`diff <(sort old.csv) <(sort new.csv)` ならこの影響をなくせます。`SELECT * FROM old EXCEPT SELECT * FROM new` は、行の順序を完全に無視します。
ExcelでCSVファイルを2つ比較するには?
VLOOKUPではなく、Power Queryのマージを使います。列ごとに別シートへVLOOKUPをかける方法は数百行なら動きますが、大きなファイルでは処理量が二乗で増え、#N/Aだらけの読みにくい表になります。Power Queryはキーで結合し、各列の両バージョンを横に並べて保持し、ファイルを差し替えると更新されます。
CSVの比較で、偽の変更が何千件も出るのはなぜ?
データの違いではない書式の違いが原因です。末尾の空白、桁区切り、`1000` と `1000.00`、2種類の日付書式、浮動小数点の誤差、文字コードの不一致などが挙げられます。比較の前に両側を正規化してください(trim、数値型へのキャスト、日付としての解析、小数の丸め)。そうしないと、これらすべてが、該当する各行で変更として表示されます。
ID列のないCSVファイルを比較するには?
行を一意に決める複数の列から複合キーを作るか、ファイルを多重集合として比較します。後者は、両側で各行の出現回数を数え、その回数を比べる方法です。ログや取引のように、同じ行が正当に繰り返され、知りたい変更が出現回数である場合は、後者が適しています。
2つのCSVファイルが同一かどうかを最も速く確認するには?
ヘッダーを除き、ソートしてハッシュを取ります。それぞれに `tail -n +2 file.csv | sort | md5sum` を実行します。ハッシュが一致すれば、行の順序に関係なく内容は同一です。何が違うかは分かりませんが、1秒で済み、「2つのエクスポートは同じものか」という疑問に、たいていはこれで決着がつきます。

uos — CSV EditorとSmart CSV Viewerの開発者。多くのエクスポートを照合してきたため、行単位のdiffは信用していません。