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秒のベンチマーク記録)はブログでも公開しています。

