TECHNICAL JOURNAL
Pythonで参加者CSVを点検する|空欄・ID重複・人数の表記違いを見つける
受付名簿を作る前に、参加者CSVの必須欄、ID重複、人数の表記をPythonで確認します。架空データ10件の検証結果と、そのまま試せるコードを掲載。先頭ゼロを残す理由や、元ファイルを変更しない設計も説明します。
主カテゴリー:アプリ開発・システム運用
検証環境
- Python 3.12.2
- Windows 11(build 26200)
- Python標準ライブラリのみ
イベントの申込みデータから受付名簿を作るときは、名前の並べ替えや印刷の前に、空欄や重複を確かめておきたいものです。たとえば、同じ参加者IDが二度出てきても、二重申込みなのか、申込み内容を更新した結果なのかは、CSVだけでは分かりません。担当者が判断するためには、まず該当するデータを見つける必要があります。
そこで、元のCSVを変更せず、確認が必要な箇所だけを表示するPythonの小さなプログラムを作りました。架空データ10件を使い、必須欄の空欄、参加者IDの重複、人数欄に混ざった文字や小数表記を点検します。追加ライブラリは使いません。
対象は、参加者データの整理を担当し、Pythonのファイルを保存して実行したことがある方です。本文のCSVとコードを手元に保存すれば、同じ結果を確かめられます。自動修正の前に、何を問題として扱うかを決めるところから始めましょう。
目次
課題:人数は数として、参加者IDは文字列として確かめる
今回のCSVには、参加者ID、名前、人数の3列を用意します。一つの申込みに複数人が含まれる想定で、人数を party_size としました。顧客の名簿ではなく、動作確認用に作った架空のデータです。
人数が 2、二、2.0 と書かれていれば、人は同じ人数を表していると読めるかもしれません。ただ、次に取り込むシステムが半角の整数を求めるなら、表記を確認する必要があります。今回は「1以上の整数を、先頭に0を付けず半角数字で書く」と決め、条件に合わない値を表示します。
一方、参加者IDの 001 は識別のための記号です。数値へ変換して 1 にすると、元の表記を失います。Pythonの csv.reader は、通常の読込みでは各欄を文字列として返します。今回は数値に変換せず、前後の空白だけを除いて比較します。001 と 1 は別のIDとして扱う設計です。Python公式のCSV読込み仕様
よく「型混在」と呼ばれる状態も、CSVを読んだ時点では文字列同士の違いです。この例では、人数欄に適用する表記ルールを明示して、そこから外れる値を見つけます。
検証条件:意図的に問題を入れた10件を3回点検する
2026年9月27日に、Windows 11(build 26200)、Python 3.12.2で実行しました。CSVはUTF-8で保存し、ヘッダーを除く10件を同じコードで3回読み込みました。確認したのは、出力が期待した内容と一致することと、入力ファイルの内容が変わらないことです。処理時間や、現場での作業時間の削減は測定していません。
3列すべてを必須にします。判定時は前後の空白を除き、空になった欄を「空欄」と数えます。この処理は読込み後の値に対して行い、CSVには書き戻しません。重複IDは2回目以降のデータを指摘し、最初に出現した通番も添えます。
入力の一部には、名前の空欄や、同じIDを持つ別の名前を入れました。人数欄には 二、2.0、空欄、0、全角の 2 を置き、先ほどのルールで見つけられるかを確かめます。
結果:空欄2件、重複1件、人数の表記違い4件を検出した
3回とも、表1の結果になりました。通番はヘッダーの次を1件目として数えています。
表1:架空データ10件に入れた問題と、プログラムの出力
| 確認項目 | 期待した件数 | 実際の件数(各回) | 対象の通番 |
|---|---|---|---|
| 必須欄の空欄 | 2 | 2 | 3、7 |
| 参加者IDの重複(2回目以降) | 1 | 1 | 4 |
| 人数の表記ルールに合わない値 | 4 | 4 | 5、6、9、10 |
この入力では、予定していた7件の指摘をすべて確認でき、余分な指摘はありませんでした。対象データも7件です。人数の空欄は最初の項目だけで数え、表記違いとして重ねて数えていません。別のCSVで一つのデータに複数の問題があれば、指摘件数と対象データ数は異なります。
入力ファイルについても、実行前後で内容から計算する照合値(SHA-256)が一致しました。ただし、これは用意した小さな入力での確認です。未知の名簿でも検出漏れや誤検出がない、という結果ではありません。
再現手順:CSVとコードを同じフォルダーに保存する
架空のCSVを用意する
作業用の空のフォルダーを作り、次を participants.csv という名前でUTF-8保存してください。本文と同じ文字を保存するため、テキストエディターを使います。8件目の人数には、判定時に前後の空白を無視することを確かめるため、3 の前に半角スペースを入れています。
participant_id,name,party_size
001,SAMPLE-A,1
002,SAMPLE-B,2
003,,1
002,SAMPLE-C,3
004,SAMPLE-D,二
005,SAMPLE-E,2.0
006,SAMPLE-F,
007,SAMPLE-G, 3
008,SAMPLE-H,0
009,SAMPLE-I,2
検査コードを保存して実行する
同じフォルダーに、次のコードを check_participants.py としてUTF-8保存します。コードは入力を読込み用に開き、結果を画面に表示します。CSVの出力処理や、外部への通信処理は含めていません。
"""Inspect a fictional participant CSV without modifying or printing its values."""
import argparse
import csv
import re
import sys
from pathlib import Path
FIELDS = ["participant_id", "name", "party_size"]
def inspect_csv(path):
issues = []
seen = {}
count = 0
with Path(path).open(encoding="utf-8-sig", newline="") as source:
reader = csv.reader(source, strict=True)
if next(reader, None) != FIELDS:
raise ValueError("header must be participant_id,name,party_size")
for count, cells in enumerate(reader, start=1):
if len(cells) != len(FIELDS):
raise ValueError(f"record {count}: expected 3 columns")
row = dict(zip(FIELDS, (cell.strip() for cell in cells)))
for field in FIELDS:
if not row[field]:
issues.append((count, field, "missing"))
participant_id = row["participant_id"]
if participant_id:
if participant_id in seen:
first = seen[participant_id]
issues.append(
(count, "participant_id", f"duplicate(first={first})")
)
else:
seen[participant_id] = count
size = row["party_size"]
if size and re.fullmatch(r"[1-9][0-9]*", size) is None:
issues.append((count, "party_size", "invalid_integer"))
return count, issues
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("csv_path", type=Path)
args = parser.parse_args()
try:
count, issues = inspect_csv(args.csv_path)
except (OSError, UnicodeError, csv.Error, ValueError) as error:
print(f"inspection failed: {error}", file=sys.stderr)
return 2
affected = len({record for record, _, _ in issues})
print(f"records={count} issues={len(issues)} affected_records={affected}")
for record, field, rule in issues:
print(f"record={record} field={field} rule={rule}")
return 1 if issues else 0
if __name__ == "__main__":
raise SystemExit(main())
strip() は判定用の値の前後から空白を除く処理です。人数を調べる re.fullmatch() は、値全体が指定したパターンに一致するかを確認します。ここでの [1-9][0-9]* は「最初の1文字が1〜9、その後は0〜9が0文字以上続く」という条件です。空白を除く処理の仕様、値全体を照合する処理の仕様
作業フォルダーでターミナルを開き、次を実行します。Windowsで python が見つからない場合は、Pythonをインストールした際の実行環境を確認してください。
python --version
python check_participants.py participants.csv
期待する出力は次のとおりです。
records=10 issues=7 affected_records=7
record=3 field=name rule=missing
record=4 field=participant_id rule=duplicate(first=2)
record=5 field=party_size rule=invalid_integer
record=6 field=party_size rule=invalid_integer
record=7 field=party_size rule=missing
record=9 field=party_size rule=invalid_integer
record=10 field=party_size rule=invalid_integer
最初の行は、読んだデータが10件、指摘が7件、指摘されたデータが7件という意味です。duplicate(first=2) は「2件目と同じIDが再び出た」という表示です。名前やIDそのものは結果に表示せず、担当者が元CSVの該当箇所を確かめる形にしています。
今回は確認対象があるため、終了コードは1になります。0なら指摘なし、2なら文字コードや列構成などの理由で点検を完了できなかった状態です。2が返った場合は「問題なし」と扱わず、エラー表示を確認してください。
限界:重複を見つけても、残す申込みまでは決められない
このコードは、IDが重複した理由や、名前の正しさを判断できません。キャンセル済みの申込みや、人数の上限も見ていません。2 や 2.0 を許可する運用であれば、今回の人数ルールをそのまま適用することもできません。
入力はUTF-8と指定の3列に限定しています。列名や順序が違うファイルは停止します。また、すべてのCSV形式の誤りを検出するものではなく、大量データでの速度やメモリ使用量も未検証です。引用符内の改行を含む場合、出力するデータの通番と、エディター上の行番号は一致しません。
まずは架空データの人数を一つ書き換え、どの表示が変わるかを試してみてください。そのうえで実際の名簿へ応用する際は、申込みを管理する担当者と「必須の欄」「同じ申込みとみなすID」「受け付ける人数の表記」を確認します。どの値を直すか判断できる状態を作ってから、自動修正の範囲を考えると、元の情報を失う変更を避けやすくなります。
制作について:本記事は、資料の調査、構成案の作成、執筆・校正に生成AIを使用しています。検証した内容と確認できていない範囲を本文に示し、当社担当者の公開承認を経て掲載しています。