Excel の「CSV UTF-8」を読ませたら、10言語すべてが見出しの行を見失った ― その書き方、本当に通る?⑧

C#
B!

名前と点数の CSV を読んで、合計を出す。練習によくある問題です。1行目の見出しが「名前,点数」かを確かめてから読み進める、10言語のよくある書き方を用意しました。

ところが、Excel で「CSV UTF-8」として保存した形のファイルを渡すと、10言語中10言語が「見出しの行がありません」と答えました。中身はふつうの CSV と同じです。違いは、ファイルの先頭に付く目に見えない印、BOM(U+FEFF)だけです。

何をしたか

CODE Polyglot の入門 第12回 は、標準入力から CSV を読んで、1人ずつの点数と合計を出すプログラムです。正解表では、改行は LF でも CRLF でもよく、先頭の BOM は無視することにしてあります。

よくある書き方は、行に分けて、1行目を見出しと比べるものです。Python ならこうです。

lines = sys.stdin.read().split("\n")
if lines[0] != "名前,点数":
    print("見出しの行がありません")
    sys.exit()

毎朝の検証と同じ Linux の環境で動かした結果です(9件の正解表から、Excel に関係する2件と、カンマ・" を含む2件)。

入力Python・JavaScript・PHP・RubyGo・Java・Kotlin・C#・Rust・Swift
先頭に BOM(Excel の「CSV UTF-8」)見出しの行がありません見出しの行がありません
改行が CRLF(Windows・Excel)見出しの行がありません正しく合計した
名前にカンマ("山田, 太郎",80)間違えた間違えた
名前に "("自称""先生""",90)間違えた間違えた

BOM は、どの言語も取らなかった

10言語とも、行を読む機能は先頭の BOM を取り除きませんでした。1行目は「[BOM]名前,点数」になり、見た目は同じでも「名前,点数」とは一致しません。エラーにはならず、ただ比較が合わないだけなので、画面に出した見出しを目で見ても気づけません。

CRLF は、読み方で分かれた

Windows や Excel で作った CSV は、改行が CRLF(\r\n)のことがあります。

  • 落ちた(10言語中4言語): Python・JavaScript・PHP・Ruby。全部を読んでから \n で区切ったので、行の最後に \r が残り、見出しが「名前,点数\r」になった
  • 通った(10言語中6言語): Go の bufio.Scanner・Java の readLine・Kotlin の readLine・C# の ReadLine・Rust の lines()・Swift の readLine。1行ずつ読む機能が \r も取り除いた

「,」で区切るだけでは読めない行

名前にカンマが入ると、CSV では "山田, 太郎",80 のように " で囲みます。これを「,」で区切ると "山田 と 太郎" と 80 に分かれ、点数の位置に 太郎" が来ます。

  • エラーで止まった(10言語中6言語): Python・Java・Kotlin・C#・Rust・Swift
  • 黙って間違えた(10言語中4言語): Go・PHP・Ruby は "山田: 0 と出して合計を 72 に、JavaScript は "山田: NaN と出した

止まる言語は気づけますが、黙って間違える言語は、合計がずれたまま次の処理に進みます。"自称""先生"""(" を "" と2つ重ねる書き方)は、10言語とも " が残ったまま出ました。

どう直したか

BOM を取り除き、CSV を読む標準の機能があればそれを使います。Python なら、文字コードを utf-8-sig にすると BOM が取れ、csv が "…" と CRLF を読み分けます。

# utf-8-sig は先頭の BOM を取り除く。newline="" で改行の扱いを csv に任せる(CRLF も "…" の中の改行も読める)
sys.stdin.reconfigure(encoding="utf-8-sig", newline="")
# csv は "…" で囲んだ項目や "" を読み分ける。空の行は [] になるので飛ばす
rows = [row for row in csv.reader(sys.stdin) if row]

CSV を読む標準の機能が無い JavaScript では、BOM を自分で取り、1文字ずつ見て「今 "…" の中にいるか」を覚えながら区切りました。

// 先頭の BOM(U+FEFF)を取り除く。空の行は [""] になるので飛ばす
const text = require("fs").readFileSync(0, "utf8").replace(/^/, "");

直した版は、10言語とも9件中9件が正解になりました。

まとめ

  • Excel の「CSV UTF-8」の先頭の BOM は、10言語とも行を読む機能が取らなかった。見出しの比較は黙って失敗する
  • CRLF は、全部読んで \n で区切ると \r が残る(10言語中4言語)。1行ずつ読む機能は取ってくれた
  • 「,」で区切るだけでは、カンマや " を含む名前を読めない。止まる言語と、黙って間違える言語がある
  • CSV を受け付けるなら、Excel で保存したファイルをテストに入れる
  • 10言語の全プログラムは 入門 第12回 にあります
B!
← 一覧へ戻る