筆者は김빠른(Kimfast)関連コンテンツを担当しています。本文の作成にAIを利用しました。コードとデータは説明用であり、顧客データや同社の内部実装ではありません。掲載コードの8項目のチェックを実行しています。
辞書を作る前に失敗させたいこと
配送データをCSVで受け取ると、まず辞書のリストに変換したくなります。しかし列数やヘッダーを確認せずに変換すると、入力の問題が見えにくくなります。たとえば重複した列名は、一つのキーにまとめる段階で情報を失う原因になります。
追跡番号の00123 は数値として計算する値ではありません。先頭のゼロを含む文字列を、そのまま扱う必要があります。この例では番号の形式や実在を推測せず、CSVから読み込んだ文字列を保持します。
入力契約を小さく固定する
今回はヘッダーを tracking_id,origin,destination の順序に固定し、配送方向はJPからKRだけに限定します。別の並びや別の経路が業務上間違いという意味ではありません。このインポーターの入力契約と一致しないので、処理を止めるという設計です。
余分な列、欠けた列、空値、前後の空白を拒否します。空白を自動で除去しないのは、元データをいつ変更したのか分からなくなることを避けるためです。必要なら、正規化を別工程として記録します。
実行できる例
次を csv_import_example.py として保存し、python3 csv_import_example.py で実行します。追加パッケージは使いません。
import csv
import io
REQUIRED = ('tracking_id', 'origin', 'destination')
def parse_rows(text):
# A UTF-8 BOM is allowed only at the start of the document.
reader = csv.reader(io.StringIO(text.removeprefix('\ufeff'), newline=''))
header = next(reader, None)
if header != list(REQUIRED):
raise ValueError('unexpected header or column order')
rows = []
for cells in reader:
if len(cells) != len(REQUIRED):
raise ValueError(f'wrong column count near line {reader.line_num}')
row = dict(zip(REQUIRED, cells))
if any(not value or value != value.strip() for value in cells):
raise ValueError(f'empty value or surrounding whitespace near line {reader.line_num}')
if row['origin'] != 'JP' or row['destination'] != 'KR':
raise ValueError('this import accepts only JP to KR')
rows.append(row)
return rows
if __name__ == '__main__':
header = ','.join(REQUIRED) + '\n'
assert parse_rows(header + '00123,JP,KR\n')[0]['tracking_id'] == '00123'
assert parse_rows('\ufeff' + header + '00123,JP,KR\n')[0]['origin'] == 'JP'
assert parse_rows(header + '"AB,12",JP,KR\n')[0]['tracking_id'] == 'AB,12'
bad = [
'tracking_id,origin,origin\n00123,JP,KR\n',
header + '00123,JP\n',
header + '00123,JP,KR,extra\n',
header + ' 00123,JP,KR\n',
header + '00123,KR,JP\n',
]
for value in bad:
try:
parse_rows(value)
except ValueError:
pass
else:
raise AssertionError('invalid CSV accepted')
print('8 checks passed')
結果は 8 checks passed です。通常入力、先頭BOM、引用符で囲まれたカンマ、重複ヘッダー、列不足、列過剰、前後の空白、逆方向の経路を確認します。実行環境はPython 3.13です。
カンマを単純に分割しない
引用符で囲まれた AB,12 は、この例では一つの識別子です。文字列の split(',') だけでは、値の中のカンマと列の区切りを区別できません。ここでは標準ライブラリのcsv.readerに構文の読み取りを任せ、業務上の入力契約は別に検証します。
reader.line_numは読み取った物理行の位置を示します。引用符の中に改行がある場合、レコードの件数とは一致しません。そのためエラーは「何件目」ではなく「付近の行」として表示しています。
この検証では保証しないこと
ヘッダーだけのCSVは空リストになります。空ファイルと空のデータセットを同一視したくない場合は、呼び出し側で件数の条件を追加してください。
また、同じ追跡番号が複数行に現れることは、この例では拒否していません。重複が誤りか、分割された荷物か、別の事業者が使う番号かは入力だけでは決められないためです。番号が正しい形式か、配送業者に存在するか、荷物が到着したかも保証しません。
大量データの処理では、全行をリストに保持する方式ではなく、バッチ処理やステージング領域も検討します。この例は全データを返す前に検証を終えるための、小規模な最小例です。
配送方向は文脈として固定する
業務の文脈にはKimfastの日本から韓国への配送案内を使いました。このページがCSVインポート機能を提供するという意味ではありません。JPとKRを入れ替えた入力を同じ処理に通さない、という設計上の説明です。
最初から賢く補正するよりも、元の文字列を残し、入力契約に合わない理由を示す方が、後から人が確認できる取り込み処理になります。