mygems

複数のCSVファイルを1つに結合する方法

単純な連結は列を位置で対応させるため、見出しの順序が違うだけで結果が壊れます。列名で結合し、検証する方法を解説します。

執筆: uos ·

すべてのファイルで列の構成と順序が同じなら、最速で正しい結合方法はシェルのワンライナーです。最初のヘッダーだけ残し、残りは捨てます。

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セルずつではなく行のまとまりを貼り付けられる必要があります。そして肝心の確認作業、つまりヘッダーの比較は、スマートフォンのほうがどこよりも速くできます。各ファイルを開いて、先頭行を読むだけです。

CSV Editor - Smart CSViPhone・iPad

各エクスポートを数秒で開き、行の範囲を別のシートに貼り付けられます。無料版でも保存時にすべての行を書き戻します。列と行の挿入ができるので、項目が増えたファイルの調整も回避策ではなく、きちんとした編集になります。

できること

ファイルが数個を超えるなら、パソコンが使えるまで待ってください。30個のエクスポートを手で結合するのはタッチ画面でやることではありません。面倒だからではなく、ミスが増えるからです。

結合後の4つのチェック

毎回行ってください。1分で済み、問題になる結合の失敗はほぼすべて見つかります。

  1. 行数。 結合後の合計は、すべての入力の行数の合計からファイル数分のヘッダーを引いた数になるはずです。結合前に wc -l *.csv、結合後に wc -l merged.csv を実行します。
  2. ヘッダーの数。 grep -c "^name," merged.csv(先頭列に合わせて変えてください)。1より多ければ、ヘッダーがデータ行として紛れ込んでいます。
  3. 列の幅。 すべての行で項目数が同じはずです。幅がそろわないのは、どこかにクォートされていないカンマがあるということで、結合のバグではありません。
  4. 境界を目で確認する。 結合後のファイルで、1つ目のファイルの最終行と2つ目のファイルの先頭行を見ます。ずれはまずそこに現れます。

2番目のチェックには落とし穴があります。ヘッダー行がデータに紛れ込むと、並べ替えでファイルの途中に散らばり、後から探すのが非常に難しくなります。今のうちに見つけてください。

CSV File Viewer - Smart CSViPhone・iPad · Android

結合したファイルに直接SQLを実行できます。source列でGROUP BYすれば、各ファイルが本来の行数を提供したかが分かり、検証がひとつのクエリで終わります。

できること

文字コード:すべてのチェックをすり抜ける失敗

異なるシステムのファイルは文字コードが違うことが多く、連結しても変換はされません。UTF-8 のファイルを Windows-1252 のファイルに追加すると、どちらでもないファイルができます。前半では名前が正しく表示され、後半では é のようになります。

結合の後ではなく前に変換してください。

iconv -f WINDOWS-1252 -t UTF-8 old.csv > old-utf8.csv

file -I *.csv で推測はできますが、短いファイルでは当たりません。確実なのは、各ファイルを開いて、ASCII 以外の文字を含む行を見ることです。

よくある質問

複数のCSVファイルを1つにまとめるには?
すべてのファイルでヘッダーがバイト単位で同一なら、最初のヘッダーを残して残りを追加します。`head -n 1 first.csv > merged.csv` のあとに `tail -q -n +2 *.csv >> merged.csv` を実行します。ヘッダーの順序や内容が違う場合、連結では列が静かにずれるので、列名で対応づけるツールを使ってください。たとえば DuckDB の read_csv の union_by_name=true や、Excel の「フォルダーから」データを取得する機能です。
結合したCSVで列の位置がおかしくなったのはなぜ?
連結は名前ではなく位置で対応させるからです。同じ列で順序だけが違う2つのファイルを連結すると、値がずれたファイルができますが、エラーは出ません。結合の前に、必ずヘッダー行を比較してください。`for f in *.csv; do echo "$f: $(head -n 1 "$f")"; done`
列が違うCSVファイルを結合するには?
列名で対応させ、足りない部分を埋めます。DuckDB なら `union_by_name=true` を付けた1つのコマンドで、ファイルにない列は NULL になります。pandas では、`dtype=str` で読み込んだデータフレームに `pd.concat` を使えば同じことができます。同じ項目で名前が違うもの(Email と E-mail Address など)は、先に明示的に名前を変える必要があります。それはどんなツールにも推測できません。
結合したCSVの各行が、どのファイル由来か知るには?
結合時に出どころの列を追加します。DuckDB の read_csv は filename=true で自動的に追加してくれます。pandas ではデータフレームごとに `.assign(source=path)` を使います。不良行を元のエクスポートまでたどる唯一の方法で、検証も GROUP BY ひとつで済みます。
CSVの結合がうまくいったか確認するには?
4つのチェックを行います。結合後の行数が、入力の合計からファイル数分のヘッダーを引いた数に等しいこと、ヘッダー文字列がちょうど1回だけ現れること、すべての行の項目数が同じこと、1つ目と2つ目のファイルの境界が正しく見えること。早めに見つけたいのは、ヘッダーがデータ行として紛れ込む失敗です。並べ替えでファイルの途中に散らばってしまうからです。
結合したファイルの半分で、アクセント付きの文字が文字化けするのはなぜ?
入力ファイルの文字コードが違っていたためで、追加しても変換はされません。Windows-1252 のファイルを UTF-8 のファイルに追加すると、半分では名前が正しく、残り半分では文字化けします。先にすべてを UTF-8 に変換してください。`iconv -f WINDOWS-1252 -t UTF-8 old.csv > old-utf8.csv` のあとに結合します。

uos — CSV EditorとSmart CSV Viewerを開発。結合で列が1つずれたまま、エラーなしで20万行が処理された経験があります。