すべてのファイルで列の構成と順序が同じなら、最速で正しい結合方法はシェルのワンライナーです。最初のヘッダーだけ残し、残りは捨てます。
head -n 1 first.csv > merged.csv
tail -q -n +2 *.csv >> merged.csv
このページの残りは、「列が同じ順序で並んでいる」という前提が、思われているよりずっと頻繁に成り立たないことと、その失敗に気づけないことについての話です。2つの列がずれたまま結合すると、問題なく開けて、見た目も普通で、中身だけが間違ったファイルができあがります。
まず、ヘッダーが一致しているか確認する
結合の後ではなく前に行ってください。このあとの問題はすべて、これの変形です。
for f in *.csv; do echo "$f: $(head -n 1 "$f")"; done
出力から、次の4点を確認します。
- 列の順序が違う。 片方が
name,email,date、もう片方がname,date,emailの場合です。単純に連結すると日付がメール列に入りますが、エラーは出ません。 - 同じ項目なのに名前が違う。
Email、email、E-mail、Email Addressなど。別々のエクスポートで作られたファイルや、年が違う同じエクスポートでよく起こります。 - 列が増えている、または足りない。 新しいエクスポートで項目が増えると、行ごとに幅が変わります。
- 先頭のファイルにバイトオーダーマーク(BOM)がある。
nameはnameではありません。どのエディタでも見えないため、列名での結合が壊れます。
ヘッダーがバイト単位ですべて同一なら、上の2行で済ませてここで読むのをやめて構いません。そうでなければ、連結という方法が間違いで、列名で結合できるツールが必要です。
列名で結合する
DuckDB:ファイルが大きい場合、またはヘッダーが違う場合
1つのコマンドで、列の順序の違いと列の欠落の両方を処理できます。位置ではなく名前で揃えるためです。
duckdb -c "COPY (SELECT * FROM read_csv('*.csv', union_by_name=true, filename=true)) TO 'merged.csv' (HEADER)"
実際に働いているのは union_by_name=true です。列はヘッダーで対応づけられ、列のないファイルの行は、ずれるのではなく NULL になります。filename=true は各行の出どころのファイル名を列として追加します。思った以上に必要になります。「この不良行はどのエクスポートから来たのか」を後から調べる唯一の方法だからです。
DuckDB はストリーミングで処理するので、扱えるサイズの上限はRAMではなくディスクで決まります。合計数GBの100ファイルでも珍しくありません。
Python:先に列名をそろえる必要がある場合
あるファイルでは Email、別のファイルでは E-mail Address と呼ばれている場合、どんなツールにも推測はできません。明示的に正規化します。
import glob, pandas as pd
rename = {"E-mail Address": "email", "Email": "email"}
frames = []
for path in glob.glob("*.csv"):
df = pd.read_csv(path, dtype=str)
df.columns = [c.strip().lstrip("") for c in df.columns]
frames.append(df.rename(columns=rename).assign(source=path))
pd.concat(frames, ignore_index=True).to_csv("merged.csv", index=False)
結合そのものより重要な点が2つあります。dtype=str は、pandas がファイルごとに型を推測するのを止めます。これがないと、あるファイルでは数値、別のファイルでは英数字になっている顧客ID列が、1024 と "01024" の混在になり、先頭のゼロは二度と戻りません。もう1つ、.assign(source=path) は、DuckDB が標準で付けてくれる出どころの列と同じものです。
Power Query:ターミナルのないデスクトップ環境
Excel の データ → データの取得 → フォルダーから は、ディレクトリを指定すると、その中のすべてのファイルをヘッダーで対応させて結合します。この作業が得意で、フォルダーに新しいファイルが入れば更新でき、毎月同じ結合を繰り返す人にはこれが正解です。
限界は Excel の限界です。結果をシートに載せるには、結合後も 1,048,576 行に収まる必要があります。代わりにデータモデルに読み込めばシートを経由せずに済み、上限がかなり上がります。知らない人が多い機能です。
追加(アペンド)と結合(ジョイン)の違い
「マージ」には2つの別の操作があり、選び間違えると午後が丸ごと無駄になります。
| 手元にあるもの | 欲しいもの | 操作 |
|---|---|---|
| 同じ列の月次エクスポート12個 | 行が増えた1つのファイル | アペンド(積み重ね) |
| 顧客のファイルと注文のファイル | 列が増えた1つのファイル | ジョイン(キーで照合) |
ここまでの内容はすべてアペンドです。ジョインには共通のキーと、一致しない行をどうするかの判断が必要で、重複が生まれるのもここです。両側でキーが一意でないと、行が静かに増えるからです。ジョイン後の行数がどちらの入力よりも多いなら、それが起きています。
ジョインには、SQL が最も確実な道具です。
duckdb -c "COPY (SELECT c.*, o.order_total FROM read_csv('customers.csv') c LEFT JOIN read_csv('orders.csv') o USING (customer_id)) TO 'merged.csv' (HEADER)"
スマートフォンで行う場合
現実的なスマートフォンの場面は、メールやファイルアプリにあるいくつかのファイル(3つの地域別エクスポートや数か月分の明細)を、月曜までパソコンなしで処理する、というものです。
ここで役に立つことが2つあります。ヘッダーが同じファイルの追加は、コピーして貼り付けるだけです。ただし、アプリが1セルずつではなく行のまとまりを貼り付けられる必要があります。そして肝心の確認作業、つまりヘッダーの比較は、スマートフォンのほうがどこよりも速くできます。各ファイルを開いて、先頭行を読むだけです。
各エクスポートを数秒で開き、行の範囲を別のシートに貼り付けられます。無料版でも保存時にすべての行を書き戻します。列と行の挿入ができるので、項目が増えたファイルの調整も回避策ではなく、きちんとした編集になります。
できることファイルが数個を超えるなら、パソコンが使えるまで待ってください。30個のエクスポートを手で結合するのはタッチ画面でやることではありません。面倒だからではなく、ミスが増えるからです。
結合後の4つのチェック
毎回行ってください。1分で済み、問題になる結合の失敗はほぼすべて見つかります。
- 行数。 結合後の合計は、すべての入力の行数の合計からファイル数分のヘッダーを引いた数になるはずです。結合前に
wc -l *.csv、結合後にwc -l merged.csvを実行します。 - ヘッダーの数。
grep -c "^name," merged.csv(先頭列に合わせて変えてください)。1より多ければ、ヘッダーがデータ行として紛れ込んでいます。 - 列の幅。 すべての行で項目数が同じはずです。幅がそろわないのは、どこかにクォートされていないカンマがあるということで、結合のバグではありません。
- 境界を目で確認する。 結合後のファイルで、1つ目のファイルの最終行と2つ目のファイルの先頭行を見ます。ずれはまずそこに現れます。
2番目のチェックには落とし穴があります。ヘッダー行がデータに紛れ込むと、並べ替えでファイルの途中に散らばり、後から探すのが非常に難しくなります。今のうちに見つけてください。
結合したファイルに直接SQLを実行できます。source列でGROUP BYすれば、各ファイルが本来の行数を提供したかが分かり、検証がひとつのクエリで終わります。
できること文字コード:すべてのチェックをすり抜ける失敗
異なるシステムのファイルは文字コードが違うことが多く、連結しても変換はされません。UTF-8 のファイルを Windows-1252 のファイルに追加すると、どちらでもないファイルができます。前半では名前が正しく表示され、後半では é のようになります。
結合の後ではなく前に変換してください。
iconv -f WINDOWS-1252 -t UTF-8 old.csv > old-utf8.csv
file -I *.csv で推測はできますが、短いファイルでは当たりません。確実なのは、各ファイルを開いて、ASCII 以外の文字を含む行を見ることです。