先に、答え。

各行をタプルにし、Counterで出現件数を比較します。並び順は無視し、同じ行が2件から1件へ減った場合も差分として残します。ID単位の更新判定とは別の処理です。

この記事の内容

対象・検証範囲:CPython 3.12.14 / Linux。掲載サンプルと期待する出力を照合。標準ライブラリのみ。

この比較で分かること

商品リストの並び順だけが変わったとき、テキストのdiffでは大量の変更に見える場合があります。ここではCSVを値として読み、行順を無視して「増えたレコード」「減ったレコード」を求めます。

同じ行が重複している可能性があるため、集合のsetではなくCounterを使います。たとえば同じ行が2件から1件へ減った場合、集合の要素は変わらなくても、件数には差があります。

重複を保つ比較コード

文字コードは読み込み前に合わせてください。次の関数は列名・列順が同じ入力を対象とし、値の前後の空白や大文字・小文字も区別します。

import csv
import io
from collections import Counter

def parse(text):
    reader = csv.reader(io.StringIO(text, newline=""), strict=True)
    header = next(reader, None)
    if not header or len(set(header)) != len(header):
        raise ValueError("有効なヘッダーが必要です")
    if any(not name.strip() for name in header):
        raise ValueError("列名が空です")
    records = []
    for row in reader:
        if not row:
            continue
        if len(row) != len(header):
            raise ValueError("列数が一致しません")
        records.append(tuple(row))
    return header, Counter(records)

def diff_csv(before, after):
    before_header, old = parse(before)
    after_header, new = parse(after)
    if before_header != after_header:
        raise ValueError("ヘッダーが一致しません")
    return new - old, old - new

before = "id,name\n001,ノート\n001,ノート\n002,ペン\n"
after = "id,name\n002,ペン\n001,ノート\n003,ファイル\n"
added, removed = diff_csv(before, after)
for row, count in sorted(added.items()):
    print("追加", row[0], row[1], count)
for row, count in sorted(removed.items()):
    print("削除", row[0], row[1], count)
assert diff_csv("id\n001\n002\n", "id\n002\n001\n") == (Counter(), Counter())

期待する結果

追加 003 ファイル 1
削除 001 ノート 1

002の並びが変わったことは差分になりません。001,ノートは1件だけ減っているので、削除件数として1を返します。これは「001のデータがすべてなくなった」という意味ではありません。

実ファイルを比較するには、両方をutf-8-sigなど正しい文字コードで読み、文字列をdiff_csv()に渡します。返されたCounterの値が件数、キーが各列の値を並べたタプルです。出力もCSVにする場合は、文字列をカンマでつなげず、writerを使ってください。

更新された行を知りたい場合

旧データが001,ノート、新データが001,ノート改訂版なら、この比較では旧行の削除と新行の追加になります。同じIDの値が変わったという「更新」には分類しません。

更新を分類するには、IDが一意で空でないことを確かめたうえで、IDをキーに残りの列を比較する別の設計が必要です。重複IDを辞書にそのまま入れると後の行で上書きされるため、検出して止める処理を入れます。

差分が多すぎるときの確認

  • 列名または列順が違う場合はエラーになります。
  • 11.00011は別の文字列です。
  • 前後の空白や、セル内のLFとCRLFの違いも値の違いになります。
  • 両方のCSVが同じように壊れていると、差分なしでも正しいとは限りません。

この実装は小〜中規模のデータをメモリー上で比較するものです。差分が少ないことと、データの内容が正しいことは別に確認します。列数とキーの検査も併用してください。

参考にした公式資料

掲載サンプルは記載のPython環境で実行確認しています。すべてのOS・入力データを保証するものではありません。 編集・検証方針