先に、答え。

UTF-8のCSVにBOMが付いている場合は、encoding="utf-8-sig"で読み込みます。先頭のBOMを読み飛ばすため、DictReaderのキーをidとして参照できます。

この記事の内容

対象・検証範囲:CPython 3.12.14 / Linux。掲載サンプルと期待する出力を照合。標準ライブラリのみ。

先頭の列だけ参照できないとき

見た目にはid,nameというヘッダーなのに、row["id"]KeyErrorになる。そんなときはrepr(reader.fieldnames)でキーの実体を確認します。先頭が'\ufeffid'なら、BOMが文字として残っている可能性があります。

この対処が使えるのはUTF-8のファイルです。CP932などで作られたCSVを、utf-8-sigで読めるようにする方法ではありません。

BOMあり・なしを同じコードで読む

次のコードは、BOM付きの入力を作り、通常のUTF-8とutf-8-sigの違いを確かめる自己完結した例です。ファイルは作らず、メモリー上のバイト列を使用します。

import csv
import io

raw = b"\xef\xbb\xbf" + "id,name\n001,ノート\n".encode("utf-8")
wrong = next(csv.reader(io.StringIO(raw.decode("utf-8"))))
print(repr(wrong[0]))

def read_utf8_csv(data):
    text = data.decode("utf-8-sig")
    return list(csv.DictReader(io.StringIO(text, newline="")))

rows = read_utf8_csv(raw)
print(rows[0]["id"], rows[0]["name"])
assert read_utf8_csv(raw[3:]) == rows
assert rows[0]["id"] == "001"
print("BOMあり・なしの結果が一致")

期待する結果と実ファイルへの適用

'\ufeffid'
001 ノート
BOMあり・なしの結果が一致

実ファイルではopen("input.csv", encoding="utf-8-sig", newline="")で開いたファイルをcsv.DictReaderへ渡します。処理の途中で数値に変換していないため、この例のIDは001のままです。

Excelに渡すファイルを新規作成する場合は、書き込み側のopen()encoding="utf-8-sig"を指定できます。既存ファイルを上書きしたくなければモードに"x"を使います。CSVの内容はcsv.writerで書き出します。

BOMを消す処理で避けたいこと

文字列全体のreplace("\ufeff", "")は、データ中の同じ文字まで消します。また、先頭3バイトを無条件に削ると、BOMのないファイルのデータを壊します。上のraw[3:]はBOMを明示的に作った検証用データでだけ使っています。

実処理ではデコーダーに任せ、BOMを付けるかどうかは受け取り先の仕様で決めます。BOM付きUTF-8は、すべてのソフトに対して必須の形式ではありません。

エラーが残る場合

UnicodeDecodeErrorなら、まず出力元の文字コードを確認します。CP932からUTF-8へ変換する例も参考になります。KeyErrorだけが残る場合は、列名の前後の空白、大文字・小文字、全角文字の違いを確認してください。

この例はBOMと先頭キーの関係だけを検証しています。列数の不一致や壊れた引用符をすべて検出するものではありません。

参考にした公式資料

掲載サンプルは記載のPython環境で実行確認しています。すべてのOS・入力データを保証するものではありません。 編集・検証方針