PythonのCSVで列がずれたら?DictReaderの余分な列と不足列を検出する

プログラミング・Web開発カテゴリを表すパンダのイラスト プログラミング・Web開発

この記事について
この記事は、生成AIを活用した自動生成フローで作成しています。Python公式のcsvモジュール仕様を参照し、架空のCSVで列数の不一致を見つける手順を整理しました。

検証ステータス:📘 公式仕様確認済み・掲載コードの実機実行は未確認

PythonのCSVで列がずれたら?DictReaderの余分な列と不足列を検出する

CSVを辞書として読み込めば、列番号を覚えずに「商品コード」「数量」などの名前で値を取り出せます。ところが、取引先から届いたファイルに余計なカンマが混ざっていたらどうなるでしょう。画面では普通に読み込めても、数量が別の列に入っているかもしれません。もっと厄介なのは、読み込み処理が例外を出さずに進み、後工程で初めて不整合が発覚することです。

Pythonのcsv.DictReaderは、列数が見出しと合わない行を必ずしもエラーにしません。余分な値と不足した値をどこへ入れるかという仕組みを持っています。今回は実在の顧客データを使わず、文字列の中だけで再現し、読み込んだ直後に不正行を隔離する方法まで確認します。結果例は公式仕様から予想したもので、ここではコードの実機実行を行っていません。

対象環境と今回のゴール

項目条件
言語Python 3系、標準ライブラリcsvとio
準備追加パッケージ、ファイル作成、ネットワーク通信は不要
入力架空の商品コードと数量
検証範囲Python公式仕様を参照。掲載コードは未実行
ゴール見出しに対する列の過不足を行番号付きで検出

最初に「CSVの列」とは何かを整理します。先頭行をcode,qtyとした場合、データ行には二つのフィールドが必要です。ところがA01,3,urgentには三つ、B02には一つしかありません。ここで重要なのは、カンマが何個あるかを単純に数えることではありません。引用符で囲まれた"東京,本社"はカンマを含んでも一つのフィールドです。CSVを文字列分割だけで検査すると、正常な行まで誤判定します。

最初の実験:余分な列は消えるのか

次のコードはio.StringIOを使い、文字列をファイルのように扱います。実ファイルの読み書きは行いません。まずは余分な列と不足列を意図的に混ぜます。

import csv
import io

sample = "code,qty\nA01,3,urgent\nB02\nC03,5\n"
reader = csv.DictReader(io.StringIO(sample))
for line_number, row in enumerate(reader, start=2):
    print(line_number, row)

公式仕様に基づく期待結果では、二行目のA01の余分なurgentは、通常の列名ではなくNoneというキーに対応するリストに格納されます。三行目のB02はqtyの値がNoneになります。四行目のC03は二つの列に普通に対応します。つまり、余った値が無言で捨てられるわけではありませんが、例外も自動では発生しません。

この違いを知らずにrow["qty"]だけを使うと、余分な列が存在する事実を見落とす可能性があります。逆に、不足列のNoneを空文字と同じだと決めつけると、入力に列そのものがなかったのか、空欄が指定されたのかを区別できなくなります。最初の観察では値だけでなく、辞書のキーの集合と値の型を見てください。

なぜ辞書にNoneが現れるのか

DictReaderはヘッダーの名前と各行の値を対応させます。余分な値を入れる名前がないときはrestkey、値が足りないときはrestvalという設定を使います。両方とも既定値はNoneですが、役割はまったく違います。前者は余分な値の入れ物となるキー、後者は足りないフィールドを埋める値です。

次の図では一行のフィールド数を見出しと比較し、どこへ格納するかだけを表します。CSVの文字列解析そのものを説明する図ではありません。

flowchart TD
    A["見出しの列数とデータ行の列数を比較"] --> B{"列数は一致?"}
    B -->|一致| C["各見出し名へ値を対応"]
    B -->|データが多い| D["余剰値をrestkeyのリストへ"]
    B -->|データが少ない| E["不足箇所へrestvalを設定"]

図の右側の二つの分岐が今回の落とし穴です。どちらも辞書という形式には収まるので、「辞書ができたから正常」とは判断できません。CSVの仕様上、空欄が正しいデータであることもあります。列数不一致の検出と、値が空欄でもよいかという業務ルールの検証は分けて考える必要があります。

1条件変える:検出専用の目印を付ける

既定のNoneを使う代わりに、余分な列には専用のキー、不足列には専用の値を指定できます。これで何が起きたかを人間にも読み取りやすくします。

import csv
import io

sample = "code,qty\nA01,3,urgent\nB02\nC03,5\n"
reader = csv.DictReader(
    io.StringIO(sample),
    restkey="__EXTRA__",
    restval="__MISSING__"
)
for line_number, row in enumerate(reader, start=2):
    problems = []
    if "__EXTRA__" in row:
        problems.append("余分な列")
    if "__MISSING__" in row.values():
        problems.append("不足した列")
    print(line_number, problems or ["列数は一致"])

期待される判定は二行目が「余分な列」、三行目が「不足した列」、四行目が「列数は一致」です。設定を変えてもデータそのものが修復されるわけではありません。目印を付けることで、入力の問題を見つけやすくしただけです。検査に合格した行だけを後続の処理へ渡す、という設計につなげます。

ただし、このサンプルは説明のために文字列を目印にしています。実データに__MISSING__という文字列が正当な値として入る可能性があれば、値だけでの判定は衝突します。さらに、ヘッダー自体に__EXTRA__がある場合も混同します。本番で堅牢に扱うなら、専用の一意なオブジェクトを不足値に使う、あるいは辞書へ変換する前にcsv.readerで列数を検査する方法を選びます。

実務向け:列数を先に数えてから辞書にする

列の過不足だけを確実に検査したい場合は、csv.readerでヘッダーと各行を配列として読み、長さを比べる方法が明快です。見出し名が重複していないかも別途点検します。以下は安全な架空データを使い、問題行を採用しない最小例です。

import csv
import io

sample = "code,qty\nA01,3,urgent\nB02\nC03,5\n"
reader = csv.reader(io.StringIO(sample, newline=""))
header = next(reader)
if not header or len(set(header)) != len(header):
    raise ValueError("見出しが空、または重複しています")

accepted = []
rejected = []
for record_number, fields in enumerate(reader, start=2):
    if len(fields) != len(header):
        rejected.append((record_number, len(fields)))
        continue
    accepted.append(dict(zip(header, fields)))

print("採用:", accepted)
print("保留:", rejected)

仕様から予想される結果は、C03の行だけが採用され、二行目と三行目が保留になることです。ここではzipを使っていますが、長さを比較してから変換しているため、余った値を黙って切り捨てることはありません。見出しの重複も先に止めています。実際のファイルでは、レコード内に改行が含まれることがあるため、ここで表示する番号はCSVレコードの順番であり、常に物理的なファイル行番号とは限りません。実際の読み込み位置が必要ならreader.line_numも確認します。

境界条件:列数が一致しても正しいとは限らない

ここで一つ意外な比較をします。D04,という行は、二つ目の値が空でもフィールド数は二つです。したがって列数検査には合格します。一方、D04は一つのフィールドしかないため不合格です。「数量が空欄」と「数量列が存在しない」は異なる問題なのです。数量が必須なら、列数検査の次にqtyが空文字でないかを確認する必要があります。

もう一つは引用符です。"E,05",7は商品コードの中にカンマを含みますが、CSVとしては二つのフィールドです。文字列のsplit(",")なら三つに見えてしまいます。だからこそ、列数の確認もCSVパーサーの結果を使います。CSVの区切り文字や引用符のルールが取引先ごとに違う場合は、delimiterやquotecharの指定も検討してください。文字コードが違えば、CSVとして解析する前の読み取り段階で問題が起こることもあります。

空行、見出しの空文字、重複見出し、数量に数値以外が入るケースも別の検査対象です。たとえばqtyがthreeでも列数は一致します。列数チェックを「CSVの品質保証が完了した」という意味で使わないことが重要です。また、実際のファイルを開くときはPython公式ドキュメントに従いnewline=""を指定し、必要に応じてencodingを明示してください。

仕事ではどちらを使うべきか

簡単な探索や、余剰値を記録しながら辞書で扱いたい場合にはDictReaderのrestkeyとrestvalが便利です。一方、受注データや在庫データのように列ずれを許容できない取り込みでは、csv.readerで列数を検査し、不一致行を隔離してから辞書化する方法が判断しやすくなります。前者は「読み込み時に情報を保持する」、後者は「不正行を採用しない」という違いがあります。

運用に組み込む際は、取り込み前の原本を保持し、保留件数と理由を記録し、異常がある場合に後工程へ進ませない設計を推奨します。問題行を勝手に補完して正常データとして扱うと、数量や金額の誤登録につながり得ます。最初は読み取り専用の小さなサンプルで期待結果を確かめ、実データへの適用は文字コード、列名、空欄ルール、例外処理まで確認してからにしましょう。

参考資料

文書情報

記事タイトル
PythonのCSVで列がずれたら?DictReaderの余分な列と不足列を検出する
作成日
更新日
Source URL
https://papanda925.com/?p=18516

ライセンス: 本記事のうち、当サイトが権利を有する本文・自作図表は、特記なき限り CC BY 4.0 で利用できます。生成AIを活用して作成・編集した内容を含みます。コードについて、別途ライセンス表示またはリンク先GitHubリポジトリのライセンスがある場合は、その条件を優先します。引用・第三者資料・画像・商標等は本ライセンスの対象外です。 利用ポリシー

タイトルとURLをコピーしました