CSVを比べるだけ、が意外と面倒
システム開発やデータ移行、ETL、バッチ処理では、CSVを比較する場面がよくある。処理前後のデータを見たい時もあれば、新旧環境の出力が一致しているか確認したい時もある。
ファイル比較をかけると、大量の差分が出る。でも、よく見ると行順が違うだけだったりする。値が変わっていても、横に長いレコードから変更されたカラムを探すのは手間がかかる。
確認したいのは、同じ顧客や同じ明細について、何が増え、何が消え、何が変わったのかということだ。
そこで、業務キーを指定してレコード単位で比較するCSV Diffを作った。UTILIZABLEに追加した4つ目のツールになる。
サイト全体の構成と公開基盤はEXP-006に記録した。今回は、実際の比較で迷わず使えるようにするための設計を残しておく。
2つのCSVと、1つのキー
入力はBefore / Old CSVとAfter / New CSVの2つ。どちらもCSVファイルの選択とテキスト貼り付けに対応した。
Before・Afterそれぞれでファイルを選択すると、対応する貼り付け欄よりファイルを優先して読み込む。先頭行はヘッダーとして扱う。
読み込み後、左右に共通するカラム名から比較キーを選ぶ。初期版は単一キーに対応し、キーを選ぶまでは比較を実行できない。
例えば、次のCSVを比較する。
Before / Old CSV
ID,NAME,AMOUNT
1001,TANAKA,1000
1002,SUZUKI,2000
1003,SATO,3000
After / New CSV
ID,NAME,AMOUNT
1001,TANAKA TARO,1200
1002,SUZUKI,2000
1004,YAMADA,4000
比較キーにIDを指定すると、結果はこうなる。
Key: ID
Before rows: 3
After rows: 3
Added: 1
Removed: 1
Changed: 1
Unchanged: 1
1004が追加、1003が削除、1001が変更、1002は変更なし。この入力はページ内のExampleボタンでも試せる。
行番号ではなく、キーで対応付ける
BeforeとAfterのレコードをキーで対応付け、4種類に分類する。
| 分類 | 条件 |
|---|---|
| Added | Afterにだけ存在する |
| Removed | Beforeにだけ存在する |
| Changed | 両方に存在するが、キー以外の値が異なる |
| Unchanged | 両方に存在し、値も同じ |
行順が入れ替わっても、同じキーのレコードを比較する。カラムも名前で対応付けるため、列の並び順が違うだけなら差分にはならない。
キーと値は文字列として完全一致で比較する。前後の空白や大文字・小文字は区別し、数値らしい値も勝手に変換しない。例えば、コードの001と1は別の値として扱う。
比較できない時は、止める
同じキーが複数あると、BeforeのどのレコードとAfterのどのレコードを比較するのかが曖昧になる。
先に見つかった行を採用すると、残りの行を見落とすかもしれない。並び順で組み合わせても、それが正しい対応とは限らない。
そのため、Before・Afterのどちらかでキー重複を検出した場合は、エラーを表示して比較を停止する。空のキーもエラーにした。
結果が出たように見えて、その対応付けが間違っている状態は避けたい。比較の前提が成立しない場合は、入力を確認してもらう設計にした。
変わったカラムだけを見る
Changedでは、変更されたカラムとBefore / Afterの値を表示する。先ほどのID 1001なら、確認するのはこの2つだ。
| Column | Before | After |
|---|---|---|
| NAME | TANAKA | TANAKA TARO |
| AMOUNT | 1000 | 1200 |
数十、数百のカラムがあるCSVでも、変更箇所から確認を始められるようにした。AddedにはAfterの行内容、RemovedにはBeforeの行内容を表示する。
片方にしかないカラムも比較対象にする。存在しないカラムと、存在するが空文字のカラムは区別する。
CSVの読み方を揃える
CSVはカンマで分割すれば読める、というわけではない。ダブルクォートで囲まれた値には、カンマや改行が含まれることもある。
CSV Diffでは、既存のCSV Inspectorで使っているCSV解析処理を共有した。同じサイトの中で、ツールによってCSVの読み方が変わらないようにしている。
- ヘッダーありCSV
- ダブルクォートで囲まれた値と、二重引用符によるエスケープ
- ダブルクォート内のカンマ・改行
- 空文字
- CRLF / LFの改行コード
- UTF-8 / UTF-8 BOM
その上で、CSV Diffに必要な検証を追加した。空入力、空・重複ヘッダー、列数不一致、指定キーの欠落、CSV解析エラーを表示する。
先頭行がヘッダーかどうかを内容だけで確実に判定することはできない。入力にはカラム名の行を用意する。ヘッダーだけのCSVは、データ行数0として比較できる。
画面操作を止めないために
CSVの読み込み・解析・比較はWeb Workerで実行する。データが大きくなった時も、できるだけ画面操作を固めないためだ。
ただし、ブラウザのメモリが無制限になるわけではない。初期版では、Before・Afterそれぞれの入力に上限を設けた。
差分の一覧も25件単位で表示する。Unchangedは件数だけにして、大量の変更なしレコードを画面に並べない。
入力を編集した時は、以前のキー選択と比較結果を無効にする。新しい入力を見ながら、古い結果を使ってしまうことも防ぎたかった。
CSVの内容は、ブラウザ内で扱う
UTILIZABLEの基本方針はBrowser-first。業務データを扱う道具なので、CSVの内容をMidnight Benchのサーバ、外部API、AIサービスへ送信せず、ブラウザ内で処理する。
利用状況の把握にはCloudflare Web Analyticsを使っている。ただし、ツールに入力したCSV内容をAnalyticsへ送信する設計にはしていない。
ページ閲覧に伴う通常のアクセス情報と、比較するCSVの内容は別のものとして扱う。この方針はPrivacyページにも記載した。
入力データはCookie、localStorage、IndexedDBにも保存しない。比較結果を持ち出す時は、利用者がCopyやDownloadを操作する。
確認した差分を、次の作業へ渡す
画面での確認と、その後の作業の両方を考えて、次の操作を用意した。
- Summary:Before / Afterの行数と4分類の件数
- All Changes / Added / Removed / Changedのフィルタ
- 25件単位の一覧表示
- Copy summary:件数をクリップボードへコピー
- Download diff CSV:全差分をCSVとしてダウンロード
- Reset / Clear:入力・選択キー・比較結果を初期化
- Example:この記事のサンプルを入力
Download diff CSVは、表示中のフィルタに関係なく全差分を出力する。STATUS、KEY、COLUMN、BEFORE、AFTERに加え、値の存在有無を示すBEFORE_PRESENT、AFTER_PRESENTを持たせた。Added / Removedも1カラム1行で記録する。
ファイルはUTF-8 BOM付き。表計算ソフト向けの数式対策は初期状態でONにし、数式として解釈され得る先頭文字を持つ値にシングルクォートを付ける。元の値のまま出力したい場合はOFFにできる。
差分を確認して終わるだけでなく、調査メモや後続のチェックへ渡せるようにした。Resetは、ダウンロード済みのファイルやコピー済みの内容までは消去しない。
小さな比較でも、境界を確認する
CSV Diff用に13件のテストを追加し、既存の4件と合わせて17件が成功した。
Added、Removed、Changed、Unchangedの分類に加えて、キー重複、キー未指定・欠落、quoted CSV、空文字、列順の違いなどを確認した。
ダブルクォート内のカンマや改行、差分CSV出力時のエスケープ、存在しない列と空文字の区別も対象にしている。サンプルでは4分類が各1件になることを確認した。
ブラウザ上でも、ファイルの読み込み、フィルタ、出力、ページ切り替え、Reset、スマートフォン幅での表示を確認した。
業務の確認に使うなら、まず基本の対応付けが崩れないことが大事になる。CSV解析の共通処理を変更せず、既存ツールのテストも通る状態にした。
道具が4つになった
UTILIZABLEの現在のツールは、この4つ。
- 01 — SQL Formatter
- 02 — SQL IN Generator
- 03 — CSV Inspector
- 04 — CSV Diff
次に何を作るかは、まだ決めていない。データエンジニアリングの現場で繰り返す、小さいけれど面倒な作業から選んでいく。
CSV Diffも実際に使いながら、比較しづらいケースや操作の引っかかりを直していきたい。