0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

配送CSVを辞書にする前に検証する:列ずれと先頭ゼロを守るPython例

0
Posted at

筆者は김빠른(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を入れ替えた入力を同じ処理に通さない、という設計上の説明です。

最初から賢く補正するよりも、元の文字列を残し、入力契約に合わない理由を示す方が、後から人が確認できる取り込み処理になります。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?