PythonでCSVを読むだけなのに3時間ハマったので、文字化けしない最小手順を残します

CSVを読むだけで3時間ハマりました。

信じたくないですが、本当です。原因は文字コード、BOM、列名の空白、そして私の「まあ読めるだろう」という油断でした。夜中にコーヒーを飲みながら UnicodeDecodeError を見つめると、かなり静かな気持ちになります。

CSVは簡単そうに見えて、現場ではだいたい何かあります。

目次

まずは標準ライブラリで読む

小さなCSVなら、最初は標準ライブラリで十分です。

import csv
from pathlib import Path


def read_csv(path: str) -> list[dict[str, str]]:
    with Path(path).open(encoding="utf-8-sig", newline="") as f:
        reader = csv.DictReader(f)
        return [dict(row) for row in reader]


if __name__ == "__main__":
    rows = read_csv("sample.csv")
    # ハマりポイント: いきなり全件表示せず、件数と先頭1件だけ見る
    print(f"rows={len(rows)}")
    print(rows[0] if rows else "empty")

utf-8-sig にしているのは、BOM付きUTF-8のCSVを読むことがあるからです。これを知らずに、先頭の列名に見えない文字が混ざって30分迷いました。

pandasで読む場合

集計や列操作をするならpandasが便利です。

import pandas as pd


def load_table(path: str) -> pd.DataFrame:
    df = pd.read_csv(path, encoding="utf-8-sig")
    # ハマりポイント: 列名の前後空白を先に消す
    df.columns = [str(col).strip() for col in df.columns]
    return df


if __name__ == "__main__":
    df = load_table("sample.csv")
    print(df.shape)
    print(df.head(3))

ここでも最初に見るのは、全データではなく形です。df.shapedf.head(3) だけで、行数、列数、文字化けの有無がかなり分かります。

3時間の内訳

最初の40分は、文字コードでした。UTF-8だと思ったら違う。違うと思って別の文字コードを試す。こうなると、だんだん勘で作業し始めます。

次の30分は、BOMでした。列名が "name" に見えるのに、実際は先頭に見えない文字が付いていました。これはかなり意地悪です。

次の50分は、列名の空白でした。"price ""price" は別物です。人間の目にはほぼ同じでも、Pythonは許してくれません。

残りの60分は、私の確認不足です。最初に件数と列名を出していれば、もっと早く終わっていました。

最初に必ず出す診断コード

今は、CSVを受け取ったらまずこれを実行します。

import pandas as pd


def inspect_csv(path: str, encoding: str = "utf-8-sig") -> None:
    df = pd.read_csv(path, encoding=encoding)
    df.columns = [str(col).strip() for col in df.columns]

    print(f"rows={len(df)}")
    print(f"columns={list(df.columns)}")
    print(df.head(3).to_string(index=False))
    print(df.dtypes)


if __name__ == "__main__":
    inspect_csv("sample.csv")

この30行弱で、かなりの事故を初期発見できます。特に列名と先頭3行は必ず見ます。

文字コードを当てに行く前に

文字化けした時、すぐに文字コードを総当たりしたくなります。

でも、その前に入手元を確認したほうが速いことがあります。Excelから出したのか、Webシステムから出したのか、別ツールが生成したのか。出どころで候補はかなり絞れます。

それでも分からない場合だけ、候補を試します。

from pathlib import Path


def try_encodings(path: str) -> None:
    data = Path(path).read_bytes()
    for encoding in ["utf-8-sig", "utf-8", "cp932"]:
        try:
            text = data.decode(encoding)
            print(f"OK: {encoding}")
            print(text.splitlines()[0])
        except UnicodeDecodeError:
            print(f"NG: {encoding}")

ハマりポイントは、成功したから正解とは限らないことです。読めても文字が崩れている場合があります。最後は列名と先頭行を目で確認します。

CSVで決めた3つのルール

1つ目、最初に全件処理しない。件数、列名、先頭3行を見る。

2つ目、列名は必ずstripする。見えない空白で条件分岐を壊さないためです。

3つ目、文字コードは推測だけで進めない。入手元と実データの見え方で確認します。

この3つだけで、CSV作業の失敗はかなり減りました。

保存する時も油断しない

CSVは読む時だけでなく、書き出す時にも事故ります。

私は一度、読み込みは成功したのに、保存後のファイルをExcelで開いたら文字が崩れました。原因は保存時の文字コードでした。読むことに集中しすぎて、出口を見ていなかったのです。

Excelで開く可能性があるなら、BOM付きUTF-8で保存することがあります。

import pandas as pd


def save_for_excel(df: pd.DataFrame, path: str) -> None:
    # ハマりポイント: Excelで開く前提ならutf-8-sigが無難な場面がある
    df.to_csv(path, index=False, encoding="utf-8-sig")

逆に、システム連携でBOMが邪魔になる場合もあります。つまり、正解は1つではありません。誰が、どのツールで、何のために開くのかで変わります。

私はCSVを扱う時、入力の文字コード、列名、行数、出力の文字コードの4つをメモするようにしました。たった4行ですが、翌月の自分をかなり助けます。

まとめ

PythonでCSVを読む作業は、簡単なようで地味に罠が多いです。

文字コード、BOM、列名、空白、行数。最初の5分で診断すれば、3時間の迷子はかなり防げます。

CSV処理で一番大事なのは、読み込むコードではなく、読み込めたと思い込まない確認です。

よかったらシェアしてね!
  • URLをコピーしました!
  • URLをコピーしました!

この記事を書いた人

コメント

コメントする

目次