日本語CSVの文字化けを防ぐ:UTF-8 BOMとCP932をPythonで読み分ける
CSVを開く前に文字コードを決める。日本語ヘッダーの「見つからない」を減らす小さな実装例。
なぜ「分類」列が見つからないのか
CSVは文字コードがファイル内で明示されない場合があります。日本語Windows環境で作成されたCSVはCP932の場合があり、UTF-8で読み込むとデコードエラーが起きることがあります。一方、UTF-8の先頭にBOMが付いたCSVを単なるutf-8で読むと、最初のヘッダーが\ufeff分類になり、分類と一致しません。
utf-8-sigで読み込むと、UTF-8 BOMがある場合は除去し、BOMのない通常のUTF-8も読み込めます。
標準ライブラリだけで読み分ける例
from pathlib import Path
import csv
import io
def read_csv(path):
raw = Path(path).read_bytes()
for encoding in ("utf-8-sig", "cp932"):
try:
text = raw.decode(encoding)
break
except UnicodeDecodeError:
continue
else:
raise ValueError("UTF-8 / CP932 として読めません")
reader = csv.DictReader(io.StringIO(text, newline=""))
return list(reader)
# read_csv("sample.csv") # 辞書のリストを返す
この例は「UTF-8(BOMあり・なし)を優先し、失敗したらCP932」の簡易フォールバックです。両方でデコード可能なバイト列では正しい文字コードを確定できません。文字化けが疑われる場合は、生成元のソフトウェアで保存形式を確認し、明示的にエンコーディングを指定してください。
また、読み込めてもCSVとして妥当とは限りません。区切り文字・列数・重複ヘッダー・空行・改行・数値の形式は別途検証が必要です。上の短い例には入力サイズ制限もないため、外部から受け取った大容量ファイルを無条件に処理しないでください。
件数と金額を集計するときの注意
csv.DictReaderで行を辞書として扱い、collections.defaultdictなどで分類別に件数を数えられます。金額はfloatではなくdecimal.Decimalを用いると、10進数の表現誤差を避けやすくなります。ただし、NaNやInfinityなどの特殊値は明示的に拒否し、桁数や指数にも入力上限を設けることが大切です。
入力(UTF-8 BOM付きでも可): 分類,金額 部品,100 工具,50 部品,20 期待する分類別結果: 工具: 1件, 合計50 部品: 2件, 合計120
表計算ソフトでCSVを開く場合、=、+、-、@で始まる文字列が式として扱われる危険にも注意します。文字列セルの出力を適切にエスケープし、データを式として実行させない設計にします。
手元のテストで確認した範囲
個人制作の分類集計CLIをMac上で2026年10月18日に実行し、18件の自動テストが成功しました。UTF-8 BOM、CP932、空欄分類、重複ヘッダー、列数不一致、不正数値、NaN/Infinity、CSV数式注入のエスケープ、入力ファイルの上書き防止、数値桁数などを含みます。
このCLIは入力上限を10MiB・50列・5000データ行とし、失敗時に既存出力を置き換えない設計です。これらはこの試作品の仕様上の上限であり、PythonやCSVの一般的な制限ではありません。この記事の短いサンプルコードは、その完全なCLI実装ではありません。
テスト成功は用意した模擬入力についての結果です。実設備・顧客環境・全ての文字コードを検証したものではありません。
確認の順序
- 元CSVのバックアップを残し、作成元と保存時の文字コードを確認する。
- ヘッダーの先頭にBOMが混入していないかを確認する。
- 数行の模擬CSVで分類・合計値を照合し、不正入力がエラーになることも試す。
- 実データを扱う場合は機密性・許可・保存場所・ファイルサイズ上限を先に決める。