CODE Polyglot / 入門 / 第12回

CSV を読む — 名前と点数の表を合計する

標準入力から CSV(カンマ区切りの表)を読んで、1人ずつの点数と合計を出します。カンマを「,」で区切るだけでは読めない CSV が、実際にはよくあります。

画面にこう出れば正解

どの言語で書いても、同じ入力なら画面に出る文字は1文字も違わない、というのが正解の決め方です(例は Python で動かしたときのコマンドです)。

基本

input.txt の中身

名前,点数
山田,80
佐藤,72
$ python3 scores.py < input.txt
山田: 80
佐藤: 72
合計: 152(2人)

名前にカンマ("…" で囲む)

input.txt の中身

名前,点数
"山田, 太郎",80
佐藤,72
$ python3 scores.py < input.txt
山田, 太郎: 80
佐藤: 72
合計: 152(2人)

名前に " ("" と2つ重ねる)

input.txt の中身

名前,点数
"自称""先生""",90
山田,80
$ python3 scores.py < input.txt
自称"先生": 90
山田: 80
合計: 170(2人)

改行が CRLF(Windows・Excel)

input.txt の中身

名前,点数[CR]
山田,80[CR]
佐藤,72[CR]
$ python3 scores.py < input.txt
山田: 80
佐藤: 72
合計: 152(2人)

先頭に BOM(Excel の「CSV UTF-8」)

input.txt の中身

[BOM]名前,点数
山田,80
佐藤,72
$ python3 scores.py < input.txt
山田: 80
佐藤: 72
合計: 152(2人)

最後に改行が無い

input.txt の中身

名前,点数
山田,80
佐藤,72
$ python3 scores.py < input.txt
山田: 80
佐藤: 72
合計: 152(2人)

空の行がある

input.txt の中身

名前,点数
山田,80

佐藤,72
$ python3 scores.py < input.txt
山田: 80
佐藤: 72
合計: 152(2人)

見出しだけ

input.txt の中身

名前,点数
$ python3 scores.py < input.txt
データがありません

見出しが無い

input.txt の中身

山田,80
佐藤,72
$ python3 scores.py < input.txt
見出しの行がありません

10言語のプログラム

どれもそのまま保存して動かせます。毎朝、上の正解の例をすべて実際に動かして確かめています。

scores.php8.4.26 で 9 / 9 件の出力が正解と一致

<?php
// 標準入力の CSV(名前,点数)を読んで、1人ずつの点数と合計を出す
$text = stream_get_contents(STDIN);
// 先頭の BOM(EF BB BF の3バイト)を取り除く
if (str_starts_with($text, "\u{FEFF}")) {
    $text = substr($text, 3);
}

$rows = [];
// 改行は LF でも CRLF でもよいので、\r\n と \n のどちらでも区切る。空の行は飛ばす
foreach (preg_split('/\r?\n/', $text) as $line) {
    if ($line === '') {
        continue;
    }
    // str_getcsv は "…" で囲んだ項目や "" を読み分ける。PHP 8.4 から escape(4つ目)を書かないと警告が出るので "" と書く
    $rows[] = str_getcsv($line, ',', '"', '');
}

if (count($rows) === 0 || $rows[0] !== ['名前', '点数']) {
    echo "見出しの行がありません\n";
    exit;
}
if (count($rows) === 1) {
    echo "データがありません\n";
    exit;
}

$total = 0;
foreach (array_slice($rows, 1) as [$name, $score]) {
    echo $name . ": " . (int) $score . "\n";
    $total += (int) $score;
}
echo "合計: " . $total . "(" . (count($rows) - 1) . "人)\n";

動かし方(ファイルを保存したフォルダで) ・ PHP を入れる

$ php scores.php < input.txt

言語でつまずきやすいところ

初心者がよく書く形(各言語で最初に習う書き方をそのまま使ったもの)を、同じ正解表で動かした結果です。言語を出した答えごとにまとめ、赤い字はその言語が実際に出した間違った行です。

入力初心者がよく書く形の答え(10言語)
基本

✓10言語すべて正解

名前にカンマ("…" で囲む)
  • PHP・Go・Ruby"山田: 0
    合計: 72(2人)
  • Python・Java・C#・Rust・Kotlin・Swiftエラーで止まった
  • JavaScript"山田: NaN
    合計: NaN(2人)
名前に " ("" と2つ重ねる)
  • PHP・Python・Go・JavaScript・Java・C#・Rust・Ruby・Kotlin・Swift"自称""先生""": 90
改行が CRLF(Windows・Excel)
  • PHP・Python・JavaScript・Ruby見出しの行がありません
  • Go・Java・C#・Rust・Kotlin・Swift✓ 正解
先頭に BOM(Excel の「CSV UTF-8」)
  • PHP・Python・Go・JavaScript・Java・C#・Rust・Ruby・Kotlin・Swift見出しの行がありません
最後に改行が無い

✓10言語すべて正解

空の行がある

✓10言語すべて正解

見出しだけ

✓10言語すべて正解

見出しが無い

✓10言語すべて正解

この回でやること

  • 標準入力から、1行ずつ読む
  • CSV の1行を、項目に分ける
  • 数に直して、合計する

この回の決まり

  • 1行目は見出しの「名前,点数」。無ければ「見出しの行がありません」、見出しだけなら「データがありません」
  • 項目にカンマや " を含むときは、"…" で囲む。中の " は "" と2つ重ねる(CSV の決まり。RFC 4180)
  • 改行は LF でも CRLF でもよい。ファイルの先頭の BOM(Excel の「CSV UTF-8」で保存すると付く印)は無視する。空の行は飛ばす

解説

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

山田,80 は「,」で区切れば読めます。ところが、名前にカンマが入ると "山田, 太郎",80 のように " で囲んで書くのが CSV の決まりです。これを「,」で区切ると、"山田 と 太郎" と 80 の3つに分かれてしまいます。

「,」で区切るだけのプログラムに "山田, 太郎",80 を渡すと、項目が3つに分かれます。Python・Java・Kotlin・C#・Rust・Swift はエラーで止まりました。Go・PHP・Ruby は "山田: 0、JavaScript は "山田: NaN と、間違った答えを黙って出しました。"自称""先生""" は、10言語とも " が残ったまま出ました。

標準で CSV を読む機能を持つ言語(Python の csv・Go の encoding/csv・Ruby の CSV・PHP の str_getcsv など)は、それを使います。持たない言語では、1文字ずつ見て「今 " の中にいるか」を覚えながら区切ります。

Windows・Excel で作った CSV

Excel で保存した CSV は、改行が CRLF(\r\n)で、先頭に BOM(目に見えない印 U+FEFF)が付くことがあります。

  • CRLF: 全部を読んでから \n で区切ると、行の最後に \r が残ります。Python・JavaScript・PHP・Ruby をこの書き方にしたところ、見出しの行が「名前,点数\r」になって合わず、「見出しの行がありません」と出ました。Go(bufio.Scanner)・Java・Kotlin・C#(ReadLine)・Rust(lines())・Swift(readLine)の、1行ずつ読む機能は \r も取るので通りました
  • BOM: 10言語とも、行を読む機能は BOM を取りませんでした。見出しが「[BOM]名前,点数」になり、全言語で「見出しの行がありません」と出ました。自分で取り除きます

例の表示について

下の例では、目に見えない BOM を [BOM]、CRLF の CR を [CR] と書いています。

練習

  1. 平均点も出してみましょう(小数第1位まで)
  2. 点数が数でない行があったら、その行番号を出して止めるようにしてみましょう