行数を数えると間違う — CSV は思ったより難しい
一言でいうと
CSVでは行(line)とレコード(record)は別のものであり、この2つを同じだと信じた瞬間、行数も金額も静かに間違います。
なぜ必要なのか
顧客が渡した売上ファイルを開いてwc -lを打ったところ、431と出ました。ところが、顧客は402件だと言います。29件の差の正体を探して、半日が過ぎます。
差は、たいてい2つのところから生まれます。1つは引用符の中の改行です。注文メモを改行して書くと、そのレコードはファイルの中で2行を占めます。もう1つは引用符の中の区切り文字です。社名が「ムーンショップ,株式会社」のようにカンマを含んでいれば、その行をカンマで分割したときに、カラムが1つ増えます。金額の列が1つずれるので、その行の金額は合計から抜けるか、見当違いの値が加算されます。
怖いのは、エラーが出ないという点です。パーサーは何も言わずに数字を出し、その数字はもっともらしく見えます。この種の間違いは、顧客が「うちの売上はこれより多いのですが」と言ってくれるまで生き残ります。
どう動くのか
RFC 4180が、CSVの共通ルールを書いています。核心は4つです。
- レコードはCRLFで区切ります。最後のレコードのCRLFは、あってもなくてもかまいません。
- フィールドに区切り文字・引用符・改行が入っている場合は、そのフィールドを二重引用符で囲みます。
- 囲まれたフィールドの中の二重引用符は、2つ(
"")で書きます。 - ヘッダー行があるかないかは、
text/csvのheaderパラメーターで知らせます。ファイルの中には、その表示がありません。
ここから、現場で出会う3つの問題がすぐに出てきます。
1つ目に、この規格は推奨であり、世の中のすべてがそれを守っているわけではありません。RFC 4180自身も冒頭で、CSVが長い間、公式の規格なしに使われてきており、この文書は広く使われている形を書き留めたものだと述べています。実際に届くファイルは、区切り文字がセミコロンやタブで、行末がLFだったり1つのファイルの中で混ざっていたりして、ヘッダーがありません。
2つ目に、ヘッダーの有無は、ファイルだけを見てもわかりません。そのため、推定する必要があります。最初の行には数字がなくて、後ろの行にはあるというのが、使える基準です。ただし、これは基準であって保証ではありません。最初のデータ行がすべて文字列の資料では間違います。
3つ目に、手で分割してはいけません。line.split(",")は、引用符を知りません。Pythonのcsvモジュールは、引用符の中の区切り文字と改行、2つで書かれた引用符をすべて処理します。そして、ファイルを開くときにnewline=""を指定する必要があります。ドキュメントが明記している要件で、これを抜かすと、引用符の中の改行がレコードを切ってしまいます。
원본 한 레코드
A-1002,"달빛상사","고객 요청:
오전 배송",1,12000
split(",") 의 눈에는 csv 모듈의 눈에는
줄 2개 · 필드 3개와 3개 레코드 1개 · 필드 5개
現場での姿
1つ目に、閉じられていない引用符です。あるレコードで引用符を1つ書き忘れると、その地点からファイルの終わりまでが1つのフィールドになります。パーサーはエラーを出さず、結果は「最後の1件」に見えます。これは、ファイルが引用の状態で終わっていないかを一度たどってみれば、確実にわかります。引用符を数えて、2つで書かれたもの("")は飛ばせば足ります。
2つ目に、Excelが書き換えます。人がExcelで開いて保存すると、区切り文字がロケール設定に従ってセミコロンになり、長い数字は指数表記になり、先頭の0が消えます。そのため、「原本のファイル」は、人が開く前のものでなければなりません。
3つ目に、壊れた行をそのまま捨ててしまいます。カラム数が合わない行を静かに飛ばすと、その件数の分だけ売上が消え、あとで説明する根拠がありません。使えない行は、リジェクトファイルに切り離して、元の行番号も一緒に書きます。そうすれば、顧客が自分のファイルでその行を開いて見られます。
4つ目に、ファイル全体を拒否するか、行だけを外すかです。銀行の受信ファイルのように、相手の合計と私たちの元帳が合っていなければならない場所では、1行だけずれてもファイル全体を送り返します。逆に、顧客が渡した分析用の資料は、送り返す先がありません。使える行は使い、使えない行だけを切り離します。どちらが正しいかは、資料ではなく業務が決めます。
実務で本当に大切なこと
- 行数とレコード数を一緒に報告します。2つが違うなら、その差そのものが情報です。
- 手で分割しません。標準のパーサーを使い、ファイルを開くときに
newline=""を指定します。 - 区切り文字と行末とヘッダーは、判別した結果を記録します。次の納品で変わったとき、その記録が根拠になります。
- 捨てずに切り離します。リジェクトファイルと理由、元の行番号を残します。
次のラボですること
同じ注文20件を7つの形で出力するフィードを自分で作ったあと、ゲートキーパーcsvgate.pyを1ステップずつ育てます。まず、手で分割したパーサーの答えを出し、csvモジュールの答えと並べて、差がどれだけ開くかを数字で見ます。そのあと、行末と区切り文字とヘッダーの有無を判別し、カラム数が食い違った行と空行と閉じられていない引用符を、リジェクトファイルに切り離します。採点ツールは、毎回違う社名と金額で自分のフィードを作り、作成したゲートキーパーを実際に実行して答えを照合します。