read-csv: シンプルなCSVリーダー

概要

このページでは、シンプルなCSVファイルのリーダーであるread-csvパッケージについて説明します。

read-csv: ストリームから1行読み取り、CSVを解析して返す

read-csvパッケージには外部シンボルが2つしかありません。一つは行単位の読み取りを行うread-csv関数で、もう一つはファイル単位の読み取りを行うparse-csvです。

read-csv関数は4つの引数を取りますが、最初の一つだけが必須で、残りはオプショナルです。引数はそれぞれ以下の意味を持ちます。
  1. 第1引数: 入力ストリーム
  2. 第2引数: セパレータ(標準は#\,
  3. 第3引数: ファイル終端時にエラーを通知するか否か(標準はt
  4. 第4引数: ファイル終端読み取り時に返すオブジェクト(標準はnil
個人的には、セパレータはカンマを使うことが多いので変更する可能性が低く、引数の並びはセパレータを最後に持って来る設計の方がいいのではないかという気もします。なぜなら、第3引数のエラー通知を行うとエラーハンドリングが必要になるため、通常はnilにしてエラーの通知を抑制するためです。

第4引数はnilのままでも構いませんが、read-csvパッケージの中では:eofキーワードを使って制御していますので、その流儀に従ってここでは:eofキーワードを使用します。

以下にサンプルを示します。まず、test.csvというファイル名で、次のデータを保存してください。国語と英語の試験の点数のデータとします。

Member,Japanese,English
A,66,78
B,72,62
C,85,75

今回は単純にCSVファイルを読み取るのではなく、指定された生徒の点数の合計点を返すような関数を考えてみます。今回はコンディションシステムを使ってファイル末尾まで読み込んだ場合の処理を記述しています。コンディションシステムについては標準仕様入門の第9章「コンディション」を参照してください。

(require "asdf")
(asdf:load-system :read-csv)


(defparameter *file* "test.csv")

(defun sum-from-string (list)
  (reduce #'+ (mapcar #'read-from-string list)))

(defun sum-score (name)
  (with-open-file (in *file*)
    (handler-case
        (loop for row = (read-csv:read-csv in)
              when (string= (first row) name)
              return (sum-from-string (rest row)))
      (end-of-file ()))))

read-csv関数はデータを全て文字列として処理するため、数に変換する場合はread-from-string関数を使います。ここでは国語と英語の点数ですが、試験の種類を増やしても対応できるようにmapcarreduceという高階関数を2つ組み合わせています。

以下のように呼び出します。

(sum-score "A")
; => 144
(sum-score "B")
; => 134
(sum-score "C")
; => 160
(sum-score "D")
; => NIL

データを扱う場合はリストではなくベクトルにしたい場合もありますし、今回のように文字列から数に変換してから使いたい場合もあります。データ処理は臨機応変に細かい調整を伴いながら行いますので、read-csv関数を使うことで1行単位の処理に対応できます。

parse-csv関数: 全データの一括読み込み

read-csvパッケージのもう一つの関数であるparse-csvは全データを一括で読み込み、リストのリストとして返します。使い方は単純です。

先ほどの続きで、以下のように実行してみてください。

(with-open-file (in *file*) (read-csv:parse-csv in))
; => (("Member" "Japanese" "English") ("A" "66" "78") ("B" "72" "62")
;     ("C" "85" "75"))

parse-csv関数の引数は2つだけで、read-csvの最初の2つと同じです。まとめてリストのリストとしてデータを取得したい場合は便利です。

0 件のコメント :

コメントを投稿