この記事について
この記事は、生成AIを活用した自動生成フローで作成しています。Python公式のcsvモジュール仕様を確認し、架空の備品一覧で「読み込めた」と「業務上正しい」を分けて実験しました。検証ステータス:🧪 Python 3.13.5で掲載ロジックの4ケースを実行確認済み。実際の業務CSVや各OSでの動作は未確認
CSVの列が一つずれたら?PythonのDictReaderで「読めた」と「正しい」を分ける
CSVをPythonで読み込むとき、プログラムがエラーを出さなければ安心してしまいがちです。しかし、入力に列が一つ増えたり、途中の行だけ値が欠けたりしても、読み取り自体は成功することがあります。売上や在庫の処理では、読み込めたという事実だけで次の計算へ進むと、数量の取り違えや欠損に気付かないまま結果を確定してしまいます。
今回は、品名・数量・担当区分の3列だけを持つ架空の備品表を使います。標準ライブラリのcsv.DictReaderがどう値を割り当てるかを観察し、列数を意図的に変えて比較し、どこに検証を追加すべきかを確認します。外部ネットワークへのアクセスや実データの用意は必要ありません。
まず「読み取り成功」の意味を分ける
CSVは行と列を扱うための形式ですが、そのファイルが業務で必要な条件を満たすかどうかは別問題です。たとえば「item,qty,section」という見出しと「pen,3,A」という行があれば、品名pen、数量3、区分Aと読み取れます。この表を処理するプログラムでは、列名が合っていること、各行に3つの値があること、数量が正の整数であることの三点を確認したいはずです。
ところがCSVの構文として読み取れることと、これらの業務条件を満たすことは一致しません。csv.DictReaderは見出しを辞書のキーにして値を並べる便利な道具です。一方、想定より多い値や少ない値にどう対応するかも仕様で決まっており、必ずしも例外を発生させません。読み取り処理に業務検査まで任せたつもりになるのが落とし穴です。
今回の合格条件は明確にします。見出しが指定した順序の3列であること、データ行が1行以上あること、余分な列と不足した値がないこと、数量が0より大きい整数であることです。単にDictReaderから辞書を取得できた、という条件より厳しくなります。どこまでを合格条件にするかは、ファイルの用途によって変更してください。
最小例:列が増えても辞書を返せる
まず通常のファイルを開く代わりに、io.StringIOを使ってメモリの中に小さなCSVを用意します。これならファイルを作成・削除する必要がなく、既存のデータを変更する心配もありません。次のコードは列が4個ある行を、3列の見出しで読み込みます。
import csv
from io import StringIO
text = "item,qty,section\npen,3,A,unexpected\n"
reader = csv.DictReader(StringIO(text))
for row in reader:
print(row)
この例では、見出しにない4つ目の値が辞書の特別なキーに格納されることが期待されます。Python公式仕様では、見出しより値が多いと、その余りはrestkeyで指定したキーにリストとして入ります。指定しない場合、restkeyの既定値はNoneです。したがって「辞書が返ったので3列の正常な入力だ」と判断してしまうのは誤りです。
ここで見るべきなのは、画面に辞書が表示されたことではありません。どのキーが増え、余分な値がどこに保存されたかです。エラーが起きないのは、処理が無視したからではなく、DictReaderが規定の方法で余りを表現したためです。元のCSVが不正なのか、業務側が追加列を許可するのかは、読み手のルールで決めます。
4パターンを一度に比較する
次はrestkeyとrestvalを明示して、想定を外れた入力を検出しやすくします。restkeyは値の数が多かった場合の格納先、restvalは値の数が足りない場合の補完値です。見出しそのものが変わってしまった場合も判定できるよう、先頭行を想定リストと比較します。
import csv
from io import StringIO
cases = {
"OK": "item,qty,section\npen,3,A\n",
"EXTRA": "item,qty,section\npen,3,A,unexpected\n",
"MISSING": "item,qty,section\npen,3\n",
"EMPTY": "item,qty,section\npen,,A\n",
}
for label, text in cases.items():
reader = csv.DictReader(
StringIO(text), restkey="__EXTRA__", restval="__MISSING__"
)
rows = list(reader)
valid = (
reader.fieldnames == ["item", "qty", "section"]
and bool(rows)
and all(
"__EXTRA__" not in row
and "__MISSING__" not in row.values()
and row["qty"].isdigit()
and int(row["qty"]) > 0
for row in rows
)
)
print(label, "OK" if valid else "CHECK")
Python 3.13.5でこのロジックを実行したところ、OKはOK、EXTRA・MISSING・EMPTYはCHECKと表示されました。これは今回の架空の4ケースでの実測であり、あらゆるCSVの正しさを保証するものではありません。特に数量の検査は正の整数だけを許可する簡易条件です。小数やマイナスを許す業務には適用しません。
EXTRAでは4つ目の値が「__EXTRA__」に入り、MISSINGでは区分の値が「__MISSING__」として補われます。EMPTYでは数量の欄自体は存在するので、不足列用の値には置き換わりません。この違いが重要です。値が存在しないことと、列は存在するが中身が空であることを、別の事象として扱えるからです。
ここで一つだけ条件を変えてみてください。EXTRAの行末から「,unexpected」を消せば、余分な列がなくなり判定はOKに戻るはずです。逆にMISSINGの行へ「,A」を足すと不足が解消します。各回に変更するのは一か所だけにすると、どの入力差分が検査結果を変えたのか追いかけやすくなります。
図で分かる「読めた」と「使ってよい」の境界
処理の分岐を図で見ると、DictReaderの役割と、業務で追加する検査の役割を区別できます。次の図が示すのは、「読み込みで例外が出なかった」という事実の先にも、複数の判断が残っていることです。
flowchart TD
A[架空CSVを読み込む] --> B[DictReaderで辞書に変換]
B --> C{見出しと列数が想定どおりか}
C -->|いいえ| H[原因を記録し処理対象から外す]
C -->|はい| D{空欄や値の形式は許容範囲か}
D -->|いいえ| H
D -->|はい| E[業務処理へ渡す]
最初の変換段階はCSVの構造を辞書にする処理です。その後に見出し、列数、値の形式という検査を入れます。図の二つの分岐を通過して初めて、今回の備品表として処理を進められます。どちらかが不合格なら、レコードをそのまま在庫計算へ渡さず、入力を修正するか隔離する判断に進みます。
図にない補足として、読み取り前に文字コードやファイルサイズなどの条件が問題になる場合もあります。実ファイルを扱う場合はopenでencodingとnewlineを明示し、想定する区切り文字と引用符の扱いを確認してください。今回StringIOを使うのは列の扱いだけを観察するためで、文字コード問題を同時に検証したわけではありません。
似た現象でも原因は違う
「列数が合わない」と言われても、元の行に余分なカンマがあるのか、引用符の扱いが誤っているのか、そもそも区切り文字がタブやセミコロンなのかでは対応が違います。たとえば品名にカンマを含めたいときは、CSV形式に従ってその欄を引用符で囲む必要があります。単純にテキストのカンマ個数だけを数えると、引用符の中のカンマまで列境界と誤認します。
区切り文字がセミコロンなのにカンマで読み込めば、見出し全体が一列として解釈されることがあります。その場合は、データを勝手に分割する前に元システムの出力仕様を確認し、DictReaderのdelimiterを合わせます。Excelで開いて見える列の配置は、Pythonがどの設定で解析したかを保証しません。表示アプリの自動判定と、実際のプログラムの設定は別だと考えます。
また、restkeyやrestvalに使った「__EXTRA__」「__MISSING__」は、説明のために選んだ文字列です。元データに同じ文字列が存在すると、検査が過剰に不合格とする可能性があります。実際の業務では入力と衝突しない表現を採用し、検査用の値と業務データを混同しない設計にします。実装上の小さな工夫でも、異常系の検出には影響します。
数量の検査にも限界があります。文字列のisdigitは空欄や小数点付きの値を正の整数として扱いませんが、全角数字なども含め、数値の表記条件を完全に決めるものではありません。必要ならASCIIの数字だけを許す、上下限を設ける、先頭ゼロを禁止するなど、入力契約を明示してください。実務ではPythonが変換できるかだけでなく、数量として許せる範囲かまで検査します。
業務に組み込むなら
日次の備品集計を想像してみてください。CSVを読み込んだ直後に表の件数を表示するだけでは、空欄や余分な列を持つ行が混ざっていても成功に見えます。最初に見出しを固定し、次に行ごとの列数と必須値を検査し、最後に合格行だけを集計すれば、誤ったデータを業務処理へ渡しにくくなります。検査で不合格になった行は、元ファイルの所在と問題の種類を記録し、担当者が修正できる形で返すのがよいでしょう。
ただし検査ログに実際の顧客名、住所、内部識別子をそのまま書き出すべきではありません。今回のように列の状態と行番号だけで原因を特定できるなら、入力値を丸ごと記録する必要はありません。ログの保管期間や閲覧権限も、他の業務ログと同じく管理します。
複数のシステムからCSVを取り込む場合、共通の検査ルールを一つだけ設けるより、送信元ごとの列仕様と許容値を明示する方が安全です。あるシステムでは追加列を許可し、別のシステムでは厳密な3列固定とすることも考えられます。検査が厳しいこと自体が正解ではなく、入力契約と実際の後続処理に一致していることが重要です。
もし集計結果が急に変わったら、まず数値の計算式を疑うのではなく、今回のような入力検査を通して、列の増減や空欄の増加を確認できます。特にデータ提供側の仕様変更があった直後は、ファイル自体は正常に生成されていても受信側の想定から外れることがあります。受入時点で差異を検知できれば、後続の帳票や分析結果を訂正する手間を減らせます。
まとめ:成功の基準を自分で決める
csv.DictReaderは、CSVの値を列名で扱いやすくする便利な標準機能です。しかし、読み取れた行がそのまま仕事に使える正しい行であることまで保証しません。余剰列はrestkeyへ、不足列はrestvalへ入り、空欄は別の状態として残ることがあります。
安全な運用への第一歩は、入力に期待する列名・列数・必須値・許容する値の範囲を言葉にすることです。小さなダミーデータで一条件ずつ変えて試すと、例外が起きない入力でも不合格にすべき理由が見えてきます。処理が正常終了したかどうかだけでなく、求めるデータ品質を検証したかどうかで成功を判断してください。
