TT Lab
はじめる
学ぶ 学習パス コース

顧客データを扱う

件数が合わない — 行ではなくレコードを数える

TT Labで続きを見る

目標

顧客のCSVを、行ではなくレコードとして数えるゲートキーパーcsvgate.pyを作ります。手で分割したパーサーと標準のパーサーの答えがどれだけ開くかを自分の資料で測り、区切り文字・行末・ヘッダーの有無を判別し、使えない行をリジェクトファイルに切り離します。

なぜ重要なのか

CSVでは、行とレコードは別のものです。注文メモに改行があると、レコード1つがファイルの中で2行を占め、社名にカンマがあると、その行をカンマで分割したときにカラムが増えます。どちらの場合も、パーサーはエラーを出さず、もっともらしい数字を出します。そのため、この間違いは、顧客が数字を見ておかしいと言うまで生き残ります。 RFC 4180は、引用符のルールと行末を定めていますが、規格をすべて守ったファイルだけが届くわけではありません。区切り文字がセミコロンやタブで、行末が1つのファイルの中で混ざっていて、ヘッダーがないファイルが届きます。ヘッダーがあるかどうかはファイルの中に書かれていないので、判別する必要があります。 そして、使えない行をどう扱うかが残ります。静かに飛ばすと、その件数の分だけ売上が消え、説明する根拠がありません。銀行の受信ファイルなら、1行だけずれてもファイルをまるごと送り返しますが、顧客が渡した分析用の資料は、送り返す先がありません。使える行は使い、使えない行だけを切り離します。 採点ツールは、提出された文言を信じません。一時ディレクトリに、採点ツールが作ったフィードを用意し、作成したゲートキーパーを実際に実行して、レコード数・金額の合計・リジェクトの理由を、採点ツールが自分で数えた値と照合します。社名と金額は、実行ごとに変わります。

ステップ

  1. /root/csv/gen_feed.pyを作成して実行し、feedディレクトリの下に7つのファイルを作成してください(保存先: /root/csv/feed)。同じ注文20件が、7つの形で出てきます。
  2. /root/csv/csvgate.pyにnaiveを作成し、行1つをレコード1つと信じるパーサーの答え(rows・amount_total・bad_field_count)を出力させてください。
  3. parseを追加して、csvモジュールで正しく数えた答え(records・amount_total)を出力させ、2つの答えの差を書いてください(保存先: /root/csv/gap.json)。
  4. sniffを追加して、行末をCRLF・LF・MIXEDで判別させてください。
  5. sniffが、区切り文字を候補(カンマ・セミコロン・タブ・パイプ)の中から判別するようにしてください。
  6. sniffが、ヘッダーの有無(header)とカラム数(fields)を判別するようにしてください。
  7. checkを追加して、使えない行を理由別に数え、リジェクトファイルに切り離させてください。リジェクトファイルは、入力ファイルがあるディレクトリの下のrejects/に、同じ名前で書きます。
  8. フィードの7つのファイルを一度に処理して、/root/csv/feed_report.jsonと、/root/csv/feed_report.mdを作成してください。

参考

7つの形の同じフィードを作る

/root/csv/gen_feed.pyを作成して実行し、feedディレクトリの下に7つのファイルを作成してください(保存先: /root/csv/feed)。同じ注文20件が、区切り文字・行末・ヘッダー・破損の有無だけを変えて出てきます。

Pythonのcsvモジュールのwriterで書けば、引用符は自動で付きます。lineterminatorを変えて行末を選び、delimiterを変えて区切り文字を選びます。わざと壊すファイル(カラム数が違う行、閉じられていない引用符)は、writerを使わず、文字列で直接書きます。

手で分割したパーサーの答えを出す

/root/csv/csvgate.pyにnaive <파일>を作成し(プレースホルダーはファイルです)、行1つをレコード1つと信じるパーサーの答えをJSONで出力させてください。rows・amount_total・bad_field_countの3つの値です。

このステップは、わざと間違ったパーサーを作るものです。ファイルをまるごと読んで改行で分割し、最初の行と空行を飛ばし、各行をカンマで分割します。カラムが5つでなければ、金額を足さず、bad_field_countとしてだけ数えてください。あとで正しく数えた答えと並べるので、ルールを正確に守ります。

引用符の中のカンマと改行を正しく読む

parse <파일>を追加して(プレースホルダーはファイルです)、csvモジュールで数えた答え(records・amount_total)を出力させ、orders_rfc.csvに対する2つの答えの差を、file・naive_rows・csv_records・naive_amount・csv_amountとして書いてください(保存先: /root/csv/gap.json)。

ファイルを開くときにnewline=""を指定することが核心です。csv.readerは、引用符の中の区切り文字と改行、2つで書かれた引用符をすべて処理します。ヘッダー行を除いた残りがレコードで、金額は、カラム数が合っているレコードでだけ足します。

行末が混ざったファイルを見分ける

sniff <파일>を追加して(プレースホルダーはファイルです)、行末を判別させてください。CRLFだけならCRLF、LFだけならLF、2つが混ざっていればMIXEDです。応答には、path・delimiter・newlineが必要です。

行末はバイトで数えます。ファイルをバイナリで読んでCRLFの個数を数え、全体のLFの個数からその分を引けば、単独のLFの個数が出ます。両方が0でなければ、混ざっています。このステップでは、区切り文字はカンマにしておいてもかまいません。

区切り文字を候補の中から選ぶ

sniffが、区切り文字をカンマ・セミコロン・タブ・パイプの4つの候補の中から判別するようにしてください。候補ごとにファイル全体をパースしてみて、カラム数が最も均一に広いものを選びます。

最初の行だけを見て数えると、引用符の中の区切り文字にだまされます。候補ごとにcsv.readerでまるごとパースして、カラム数の最頻値と、その最頻値を持つ行の割合を求め、カラムが1つだけの候補は点数を0にしてください。カラム数が行ごとに違うファイルでも、この方法は耐えられます。

ヘッダーがあるかないかを判別する

sniffの応答にfields(カラム数)とheader(ヘッダーの有無)を加えてください。最初の行には数字として読めるカラムが1つもなく、続く3行にはあれば、ヘッダーとみなします。

ヘッダーがあるかどうかは、ファイルの中に書かれていません。RFC 4180も、それをメディアタイプのパラメーターで知らせるよう言っているだけです。そのため、判別ではなく推定であり、推定には基準が必要です。基準をコードに書いておけば、次の納品で間違ったときに、何を直せばよいかが見えます。

使えない行だけを切り離す

check <파일>を追加して(プレースホルダーはファイルです)レコードごとに検査し、空行・カラム数の不一致・閉じられていない引用符を理由別に数え、リジェクトファイルに切り離させてください。リジェクトファイルは、入力ファイルがあるディレクトリのrejects/に、同じ名前で書きます。sniffの応答にはunterminatedを加えます。

閉じられていない引用符は、エラーを出しません。その地点からファイルの終わりまでが1つのフィールドになり、「最後の1件」のように見えるだけです。ファイルを一度たどって、引用の状態で終わっていないかを見れば、確実にわかります。2つで書かれた引用符は飛ばす必要があります。リジェクトした行には、元が何番目のレコードだったかも一緒に書いてください。

フィード1枚で報告する

フィードの7つのファイルを一度に処理して、files・accepted・rejected・amount_totalを書き(保存先: /root/csv/feed_report.json)、/root/csv/feed_report.mdに、## 무엇을 받았나、## 줄과 레코드는 다르다、## 떼어 낸 줄、## 보내는 쪽에 요청할 것の4つの節で書いてください(韓国語の見出しは順に「受け取ったもの」「行とレコードは別のもの」「切り離した行」「送る側に依頼すること」という意味です)。

filesは、ファイル名をキーにして、delimiter・newline・header・records・accepted・rejected・amount_totalを入れたオブジェクトです。レポートには、切り離した行の数を数字で書いてください。顧客が自分のファイルでその行を開いて見られれば、会話が速くなります。前のステップで作った関数を、そのまま呼べば足ります。