CSV結合の自動化でつまずく3場面と対処法【全スクリプト無料】

AIしごとラボ

Excel保存のCSV(Shift_JIS形式)をよくあるPythonスクリプトで読むと、1行目で「UnicodeDecodeError」というエラーになり、読み込み自体が止まりました。さらに危険だったのは列名が1つ違うファイルが混ざった場合で、エラーは一切出ないまま結合が「成功」し、250行中50行分の金額が集計から静かに抜け落ちました。

これらはすべて、この記事のために手元で実際に再現・実測した結果です。

この記事は、毎月の売上報告のたびに、支店や部署から届くCSVファイルを1つずつExcelで開いてコピペで貼り合わせている――そんな非エンジニアの方に向けて書いています。CSV結合の自動化スクリプトを実際に運用しようとすると高確率で出会う「3つのつまずき場面」を実測で再現し、それぞれの対処スクリプト(コピペしてフォルダ名を書き換えるだけで動きます)を全文無料で公開します。

筆者は「AIしごとラボ」という、AI×業務効率化のやり方を実際に手元で動かして検証してから公開するラボを運営しています。この記事のエラーメッセージ・行数・件数は、すべて下記の環境で実測したものだけです。実測していない数値を書く場合は必ず「推定」と明記します(この記事には推定値はありません)。

検証環境

  • OS: Windows 11 Pro(Windows-11-10.0.26200-SP0)
  • Python 3.12.10(64bit)/ pandas 3.0.5
  • 検証データ: random.seed(42) で自動生成した架空の売上CSV。列構成は「日付, 商品名, 数量, 単価, 金額」、1ファイル50行
  • 基本セット: UTF-8(BOM付き)のCSV 20個
  • 場面1用: Shift_JIS(cp932)のCSV 5個
  • 場面2用: 2階層のサブフォルダに散らばせたCSV 10個
  • 場面3用: 正常4個+「金額」列だけ「売上金額」になっているファイル1個

※pandas(パンダス)は、表形式データを扱うPythonの定番ライブラリです。

基本形: フォルダのCSVをまとめて結合(100個で平均3.8秒)

基本形は「フォルダ内の *.csv を全部読んで、縦につなげて1ファイルに保存する」だけのスクリプトです。ブログの検証では、CSV100個(5,000行)の結合が平均3.8秒で終わりました。基本形の全文とその実測記録はブログ「AIしごとラボ」で公開済みです(URLは記事末尾)。

ところが、この基本形を現実の業務データに使うと、きれいなテストデータでは起きなかった問題に直面します。ここからが本題です。

場面1: Excel保存のCSVで「UnicodeDecodeError」が出る

症状(実測)

ExcelでCSV形式に保存すると、日本語版Windowsでは文字コード(文字をコンピュータ上の数値で表す方式)がShift_JIS(Pythonでの名前は cp932)になることがあります。このCSVを、UTF-8前提の基本形スクリプト(encoding="utf-8-sig")で読むと、実測では次のエラーで読み込みが即停止しました。

UnicodeDecodeError: 'utf-8' codec can't decode byte 0x93 in position 0: invalid start byte

「position 0」は、1行目の列名「日付」の1バイト目です。つまり文字化けした表が出てくるのではなく、最初の1文字目で処理そのものが止まるタイプの失敗でした。エラーで止まってくれる分、次の場面3よりはずっと安全です。

対処スクリプト: encoding を cp932 に変えるだけ

# -*- coding: utf-8 -*-
"""Excel保存のCSV(Shift_JIS)を encoding="cp932" で読んで結合する対応版"""
from pathlib import Path
import pandas as pd

folder = Path(r"C:\data\csv")   # ←自分のCSVフォルダに書き換える

dfs = []
for f in sorted(folder.glob("*.csv")):
    df = pd.read_csv(f, encoding="cp932")   # ← ここを utf-8-sig から cp932 に変えるだけ
    dfs.append(df)
    print(f"読み込みOK: {f.name}({len(df)}行)")

merged = pd.concat(dfs, ignore_index=True)
out = folder.parent / "merged_cp932.csv"
merged.to_csv(out, index=False, encoding="utf-8-sig")  # 出力はBOM付きUTF-8(Excelで開ける)
print(f"\n結合完了: {len(dfs)}ファイル → {len(merged)}行")
print(f"列名: {list(merged.columns)}")
print(f"保存先: {out}")

実測では、Shift_JISのCSV 5ファイルすべてが「読み込みOK(50行)」となり、「結合完了: 5ファイル → 250行」。列名も ['日付', '商品名', '数量', '単価', '金額'] と正しく読めました。出力はBOM付きUTF-8にしてあるので、結合後のファイルはExcelでダブルクリックしてもそのまま日本語で開けます。

場面2: CSVがサブフォルダに散らばっている

症状(実測)

「2026\01」「支店A\東京」のように、月別・支店別のサブフォルダにCSVが整理されているケースです。基本形で使う glob("*.csv") はフォルダ直下しか探さないため、実測では、2階層のサブフォルダ(2026\01〜03、支店A\東京・大阪、支店B\名古屋・福岡)に10個のCSVを置いた構成で、次の結果になりました。

glob('*.csv')  で見つかった数: 0個
rglob('*.csv') で見つかった数: 10個

直下にCSVがない構成では、従来版はエラーも出さずに0件で空振りします。

対処スクリプト: rglob 版

# -*- coding: utf-8 -*-
"""サブフォルダの中のCSVもまとめて拾う rglob 版"""
from pathlib import Path
import pandas as pd

folder = Path(r"C:\data\csv")   # ←自分のCSVフォルダに書き換える

# glob("*.csv") は直下だけ / rglob("*.csv") はサブフォルダの中まで全部探す
files_glob = sorted(folder.glob("*.csv"))
files_rglob = sorted(folder.rglob("*.csv"))
print(f"glob('*.csv')  で見つかった数: {len(files_glob)}個")
print(f"rglob('*.csv') で見つかった数: {len(files_rglob)}個")

dfs = []
for f in files_rglob:
    df = pd.read_csv(f, encoding="utf-8-sig")
    # どのフォルダのファイルか分かるように、元ファイルの場所を列に残す
    df["元ファイル"] = str(f.relative_to(folder))
    dfs.append(df)
    print(f"  読み込み: {f.relative_to(folder)}({len(df)}行)")

merged = pd.concat(dfs, ignore_index=True)
out = folder.parent / "merged_nested.csv"
merged.to_csv(out, index=False, encoding="utf-8-sig")
print(f"\n結合完了: {len(dfs)}ファイル → {len(merged)}行")
print(f"保存先: {out}")

実測では、rglob版が10ファイル全部(「支店A\東京\sales_n06.csv」のような日本語フォルダ名も含む)を読み込み、「結合完了: 10ファイル → 500行」。さらに「元ファイル」列に「2026\01\sales_n01.csv」のような相対パスが残るので、結合後のExcel上でも「この行はどのフォルダのファイル由来か」を追跡できることを確認しました。

場面3: 列名がズレたファイルが混ざる(最も危険)

症状(実測): エラーなしで50行分の金額が消える

誰かが手作業で列名を「金額」から「売上金額」に変えてしまったファイルが1個だけ混ざっている――という状況を再現しました。正常4個+列名違い1個をそのまま pd.concat で結合すると、実測ではエラーは一切出ずに黙って成功し、列が6列に増えました(['日付','商品名','数量','単価','売上金額','金額'])。結果、250行のうち「金額」列の欠損が50件、「売上金額」列の欠損が200件。この状態で金額を合計すると、50行分が警告なしで集計から抜け落ちます。3場面の中で一番危険なパターンです。

設計の落とし穴も実測で発見

最初は「1つ目のファイルの列名を基準にする」方式で検知を作りました。ところがファイル名のソート順で異常ファイル(sales_ng_5.csv)が先頭に来てしまい、正常な4ファイルの方が全部警告・除外される逆転が実際に起き、最後の集計行で KeyError: '金額' も発生しました。この失敗を踏まえて、「期待する列名を自分で明示する」方式に修正したのが下のスクリプトです。

対処スクリプト: 期待する列名を明示してチェックする版

# -*- coding: utf-8 -*-
"""結合前に列名のズレを検知して警告する版"""
from pathlib import Path
import pandas as pd

folder = Path(r"C:\data\csv")   # ←自分のCSVフォルダに書き換える

# 期待する列名を自分で決めて書いておく(「最初のファイルを基準にする」方式だと、
# たまたま先頭に来たファイルが異常だった場合に正常ファイルの方が除外されてしまう)
EXPECTED_COLS = ["日付", "商品名", "数量", "単価", "金額"]

files = sorted(folder.glob("*.csv"))
dfs = []
ng_files = []   # 列名がズレていたファイルの記録

print(f"期待する列名: {EXPECTED_COLS}\n")
for f in files:
    df = pd.read_csv(f, encoding="utf-8-sig")
    cols = list(df.columns)
    if cols != EXPECTED_COLS:
        # ズレた列を特定して警告(このファイルは結合から除外)
        diff = set(cols) ^ set(EXPECTED_COLS)   # どちらか片方にしかない列名
        print(f"【警告】{f.name} の列名が期待と違います!")
        print(f"  このファイルの列名: {cols}")
        print(f"  ズレている列: {sorted(diff)}")
        print(f"  → このファイルは結合から除外します。列名を直してから再実行してください。")
        ng_files.append(f.name)
        continue
    dfs.append(df)
    print(f"OK: {f.name}({len(df)}行)")

merged = pd.concat(dfs, ignore_index=True)
out = folder.parent / "merged_checked.csv"
merged.to_csv(out, index=False, encoding="utf-8-sig")
print(f"\n結合完了: {len(dfs)}ファイル → {len(merged)}行(除外: {ng_files})")
print(f"金額列の欠損数: {merged['金額'].isna().sum()}(チェックなしで結合するとここが欠損だらけになる)")
print(f"保存先: {out}")

実際の警告出力

修正版を実行したときの実際の出力(抜粋)がこちらです。

【警告】sales_ng_5.csv の列名が期待と違います!
  このファイルの列名: ['日付', '商品名', '数量', '単価', '売上金額']
  ズレている列: ['売上金額', '金額']
  → このファイルは結合から除外します。列名を直してから再実行してください。

異常ファイルだけを正しく検知し、正常4ファイルはOK判定。「結合完了: 4ファイル → 200行(除外: ['sales_ng_5.csv'])」「金額列の欠損数: 0」となりました。どのファイルの、どの列がズレているかまで表示されるので、直すべき場所がすぐ分かります。

まとめ: 3スクリプトの使い分け早見表

状況見分け方(実測した症状)使うスクリプト
Excel保存のCSVを読むUnicodeDecodeError で即停止場面1: cp932版
CSVがサブフォルダにあるglob が0個で空振り(エラーなし)場面2: rglob版
列名の違うファイルが混入エラーなしで欠損だらけになる場面3: 列名チェック版

実務では「場面2+場面3」のように組み合わせが必要になることも多いはずです。その場合は、場面2の読み込み部分に場面3の列名チェックを足せば両対応になります。

このラボでは今後も、「実際に動かして、エラーも失敗もそのまま見せる」検証記事を無料・低価格で公開していきます。数字を盛らず、実測できたことだけを書くのがこのラボの方針です。

検証の続き(基本形スクリプトの全文と、CSV100個・平均3.8秒のベンチマーク記録)はブログでも公開しています。


あなたも記事の投稿・販売を
始めてみませんか?

Tipsなら簡単に記事を販売できます!
登録無料で始められます!

Tipsなら、無料ですぐに記事の販売をはじめることができます Tipsの詳細はこちら
 

この記事のライター

AIしごとラボ

AI×業務効率化の検証ラボです。生成AI・スプレッドシート/GAS・Pythonを「実際に動かして、測って、確かめる」ことに特化。実測データと再現手順、コピペで使える完成品だけを公開します。誇大な表現なし・未検証のことは書かない方針です。検証記録はブログでも公開中→ https://aishigotolab.blogspot.com/

このライターが書いた他の記事

  • 「AIに丸投げ」は本当に一瞬か、測ってみた【CSV1,000個で実測】

関連のおすすめ記事

  • 【AI自動化・マネタイズ実例書】たった2週間〜機械オンチなママでもできた全作業過程

    ¥35,800
    1 %獲得
    (358 円相当)
    みお

    みお

  • Claude Code 業務効率化の教科書

    ¥4,980
    1 %獲得
    (49 円相当)
    くろだ@海外ガジェット販売

    くろだ@海外ガジェット販売

  • 【5年更新型コンテンツ】AIを最大活用するためのリテラシー強化バイブル

    ¥59,800
    1 %獲得
    (598 円相当)
    こはく

    こはく