概要
ANSI Common Lispの第21章「ストリーム」を説明します。ストリームの概要
ストリームは、ファイルというデバイス上のデータとプログラムの橋渡し(入出力)を行うための抽象的なデータ構造です。ストリームは他のデータ構造と異なり、ストリーム自体にデータを格納することが目的ではなく、橋渡しが目的です。そのため、ストリーム自体もオブジェクトなのですが、基本的な目的はファイルから文字やバイトを読み取ったり、ファイルに文字やバイトを書きこんだりすることです。
ストリームはファイルからは一段抽象化されているため、入出力の相手はファイルに限られません。例えば、文字列を相手とするストリームもあります。ストリームには様々な種類がありますが、このページではファイルストリームと文字列ストリームを紹介します。
ストリームの開閉
ストリームは必ず開く処理と閉じる処理が存在します。一部のストリームは処理系の起動時に自動的に開かれ、終了時に自動的に閉じられますが、そのようなストリームも暗黙的に開閉されています。この節ではまず基本となるファイルストリームについて説明した後、文字列ストリームについても説明します。
ストリームの生成: open
ファイルストリームを生成し、開いた状態にしておくにはopen
関数を使います。ここでは、test.txt というファイルがある前提でサンプルを示します。
(defparameter *in* (open #p"test.txt"))
; => *IN*
*in*
; => #<INPUT BUFFERED FILE-STREAM CHARACTER #P"test.txt" @1>
open
関数で開かれたストリームは、標準の状態ではテキストデータ読み込み用のストリームです。ストリームが開かれているかを調べるにはopen-stream-p
述語関数を、ストリームが読み込み用か書き込み用かを調べるにはinput-stream-p
述語関数とoutput-stream-p
述語関数を使います。
(open-stream-p *in*)
; => T
(input-stream-p *in*)
; => T
(output-stream-p *in*)
; => NIL
open
関数には5つのキーワード引数があります。
- Argument:
:direction
- 入力または出力を指定します。出力の時は
:output
を指定します。標準は:input
です。 - Argument:
:element-type
- テキストデータかバイナリデータかを指定します。バイナリの場合は
'(unsigned-byte 8)
を指定します。標準は'character
です。 - Argument:
:if-exists
- 同名のファイルが存在していた場合の動作を指定します。追記する場合は
:append
を、新しいファイルで置き換える場合は:supersede
を指定します。多くの場合は、標準で:error
が指定されています。 - Argument:
:if-does-not-exist
- ファイルが存在していない場合の動作を指定します。エラーを通知する場合は
:error
を、新しく作る場合は:create
を指定します。標準は場合によって異なりますが、入力の時は一般に:error
が、出力の時は一般に:create
が使われます。 - Argument:
:external-format
- 入出力の際に用いる文字符号化方式を指定します。ANSIで定められているのは
:default
だけで、標準の文字コードが何かということも処理系に委ねられています。多くの処理系ではUTF-8が用いられていますが、WindowsでGNU CLISPを使う場合などはCP932が用いられます。また、このキーワード引数で指定するものは多くの処理系でキーワード(例えば:cp932
など)ですが、CLISPではCHARSET
パッケージの構造体です(例えばcharset:cp932
)。
(defparameter *out* (open #p"test2.txt" :direction :output
:element-type '(unsigned-byte 8)
:if-exists :supersede))
; => *OUT*
*out*
; => #<OUTPUT BUFFERED FILE-STREAM (UNSIGNED-BYTE 8) #P"test2.txt">
日本語を含むテキストデータを扱う場合は
:external-format
を指定します。一般にはUTF-8が使われているので、UTF-8の場合は指定しなくても普通に読み込めることが多いですが、処理系や環境の違いを吸収する必要があれば注意してください。
なお、ここでのUTF-8とはCommon Lisp内部での文字集合・文字符号化方式とは異なります。Common Lisp処理系の内部では、マルチバイトに対応した処理系ではUTF-32などの固定長バイトが使われていることが一般的です。ただ、内部の文字符号化方式は
character
クラスに抽象化されているため、意識することはほとんどありません。:external-format
は外部の文字と内部の文字の変換を手伝うためのものです。
open
関数はANSI Common Lispの関数の中でも最も「例外的状況( Exceptional Situations )」の記述が多い関数の一つです。安全なプログラムを作る場合はエラーハンドリングが不可欠ですので、第9章「コンディション」を参照してください。
ストリームの破棄: close
ストリームを使い終わった後は明示的に破棄する必要があります。ストリームの破棄はclose
関数で行います。
(close *in*)
; => T
(close *out*)
; => T
すでに閉じているストリームを再度閉じても構いませんが、返り値は処理系依存です。
(close *in*)
; => T
便利なマクロ: with-open-file
ストリームはopen
で始まり、close
で終わるのですが、close
を忘れることを防ぐために、暗黙的にclose
まで行うwith-open-file
マクロが用意されています。
(with-open-file (*in* "test.txt")
(read-line *in*))
; = "Hello," ;
; NIL
with-open-file
マクロの使い方は簡単で、ストリームを束縛したいシンボルを第1引数(リスト)の先頭の要素に置き、以降はopen
関数と同じものを並べるだけです。このマクロはレキシカル環境を持つので、上の例のようにスペシャル変数を使わず、let
スペシャルオペレータのように新しいシンボルを使うことができます。
基本的にファイルストリームを使う場合は
open
関数とclose
関数の組み合わせではなく、with-open-file
マクロを使用してください。
文字列ストリーム関数: make-string-input-stream
, make-string-output-stream
ストリームを使う場面で最も多いのはファイルの内容を扱う場合だと思いますが、文字列をファイルのように扱えると便利な時があります。文字列はベクトルなので、ファイルとデータ構造はよく似ています。
文字列から入力用ストリームを生成するには、
make-string-input-stream
関数を使います。
(setq *in* (make-string-input-stream "(1 2 3)"))
; => #<INPUT STRING-INPUT-STREAM>
(let ((lst (read *in*)))
(format t "~{~a~^,~}~%" lst))
; 1,2,3
; => NIL
(close *in*)
; => T
逆に、文字列を書き込むには以下のように、
make-string-output-stream
関数を使います。
(setq *out* (make-string-output-stream))
; => #<OUTPUT STRING-OUTPUT-STREAM>
(format *out* "(~{~a~^, ~})" (loop for i from 1 to 10 collect i))
; => NIL
(get-output-stream-string *out*)
; => "(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)"
(close *out*)
; => T
make-string-output-stream
関数で生成されるのは出力用のストリームなので、そこに文字列を書き込んでも、普通の読み取り関数では読み取れません。そこで、get-output-stream-string
関数で書き込まれた文字列を読み取ります。
もっとも、
format
関数は第1引数にnil
を指定すると文字列自体を返すことができるので、これらの関数を直接使うことは少ないかもしれません。
文字列ストリームマクロ: with-input-from-string
, with-output-to-string
open
関数とclose
関数に対してwith-open-file
マクロが定められていたように、文字列ストリームにもマクロが定められています。
前小節のサンプルは、それぞれ以下のように記述できます。
(with-input-from-string (in "(1 2 3)")
(format t "~{~a~^,~}~%" (read in)))
; 1,2,3
; => NIL
出力用のサンプルは以下のようになります。
(with-output-to-string (out)
(format out "(~{~a~^, ~})"
(loop for i from 1 to 10 collect i))
(get-output-stream-string *out*))
; => "(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)"
ただし、
with-output-to-string
マクロは上のように使うのではなく、別に用意した文字列用の変数に出力するような使い方がメインです。
(defparameter *str* (make-array 0 :element-type 'base-char
:adjustable t
:fill-pointer 0))
; => *STR*
(with-output-to-string (out *str*)
(format out "(~{~a~^, ~})"
(loop for i from 1 to 10 collect i)))
; => NIL
*str*
; => "(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)"
ベクトル(1次元配列)のこのような使い方は第15章「配列」の「可変長とフィルポインタの併用」に記述していますので、適宜参照してください。
with-output-to-string
マクロは文字列ストリームへの書き込みを暗黙的にvector-push-extend
関数で文字列変数に追加していく処理を行うことができます。
ストリームの読み書き
ストリームは開くだけでは何の意味もありません。必ず、ストリームから入力を得るか、出力を渡すかしなければなりません。この節では、ストリームに対する入力と出力のオペレータを紹介します。
文字: read-char
, write-char
:element-type
が'character
またはそのサブタイプであるテキストストリームから文字の入力を得るにはread-char
関数を、文字を出力するにはwrite-char
関数を使用します。
(with-open-file (in #p"test.txt")
(loop
repeat 13
for c = (read-char in)
do (print c)))
; #\H
; #\e
; #\l
; #\l
; #\o
; #\,
; #\Newline
; #\W
; #\o
; #\r
; #\l
; #\d
; #!
; => NIL
出力も使い方は同じです。
write-char
関数は第1引数に書き込む文字を、第2引数に出力用ストリームを指定します。
(with-open-file (out #p"test2.txt" :direction :output
:if-exists :supersede)
(loop
for c across "Hello, World."
do (write-char c out)))
; => NIL
write-char
関数は単純ですが、read-char
関数は実はもっと複雑です。その複雑さは、ファイルの終端をどうするかという点にあります。
read-char
関数は第1引数が入力用ストリームですが、以降はオプショナル引数です。第2引数は「ファイル終端を読み込もうとした時にエラーを通知するか」で、第3引数は「ファイル終端を読み込んだ時のオブジェクトは何にするか」です。第2引数は標準でt
になっており、終端を読み込んだ場合はend-of-file
コンディションが通知されます。ファイルから読み込む文字数が決まっておらず、終端まで読み込む場合は以下のようにコンディションをハンドリングすることで対処できます。
(with-open-file (in #p"test.txt")
(handler-case
(loop
for c = (read-char in)
do (princ c))
(end-of-file (condition)
(declare (ignore condition))
(format t "End-Of-File."))))
; Hello,
; World!
;
; Common Lispの世界へ
; ようこそ。
; End-Of-File.
; => NIL
しかし、一般にはもっと簡便な方法を使います。終端を読み込んだ際にエラーを通知せず、返り値が
nil
になった場合を捉える方法です。
(with-open-file (in #p"test.txt")
(loop
for c = (read-char in nil)
while c
do (princ c)))
; Hello,
; World!
;
; Common Lispの世界へ
; ようこそ。
; => NIL
文字を読み込む場合は返り値が
nil
になることはありませんので、nil
の時は終端とわかります。
文字列: read-line
, write-line
, write-string
テキストデータの場合は文字単位ではなく、行単位で読み込みたいことが多いと思います。そのような場合はread-line
関数を使います。以下は、読み込んだ行をそのまま出力するので、write-line
関数も合わせて使っています。
(with-open-file (in #p"test.txt")
(loop
for s = (read-line in nil)
while s
do (write-line s)))
; Hello,
; World!
;
; Common Lispの世界へ
; ようこそ。
; => NIL
テキストデータの行末文字は、使用している環境で一般的な組み合わせなら特に意識せず適切に取り除いて読み込んでくれます。一般的ではない組み合わせの場合、多くの処理系では行末処理を指定できます。自分の処理系のマニュアルを確認してください。
なお、
write-line
関数は自動で文字列の最後に適切な行末文字#\Newline
を追加しますが、行末文字を追加せずに文字列をストリームに出力する場合はwrite-string
関数を使います。
(with-open-file (in #p"test.txt")
(loop
for s = (read-line in nil)
while s
do (write-string s)))
; Hello,World!Common Lispの世界へようこそ。
; => NIL
バイト: read-byte
, write-byte
コンピュータ上ではテキストデータもビットの列であり、バイナリデータとして見ることもできます。また、そもそも画像や音声など、テキストではないデータも多く存在します。バイナリデータを読み込むにはread-byte
関数を、書き込むにはwrite-byte
関数を使用します。
現代のコンピュータでは、一般に8bitをまとめて「オクテット」として扱います。Common Lispは8bit単位(オクテット)ではなくても扱うことができます。
read-byte
関数やwrite-byte
関数が何bitを1バイトとして扱うかは使用では定められておらず、open
関数でストリームを開く時の:element-type
キーワード引数で指定した単位で扱うことになります。
前小節で読み込んだテキストデータをバイナリデータとして読み込んでみます。
(with-open-file (in #p"test.txt" :element-type '(unsigned-byte 8))
(loop
for b = (read-byte in nil)
while b
do (format t "~2,'0x " b)))
; 48 65 6C 6C 6F 2C 0A 57 6F 72 6C 64 21 0A 0A 43 6F 6D 6D 6F 6E
; 20 4C 69 73 70 E3 81 AE E4 B8 96 E7 95 8C E3 81 B8 0A E3 82 88
; E3 81 86 E3 81 93 E3 81 9D E3 80 82 0A
; => NIL
ベクトル: file-length
, read-sequence
, write-sequence
バイナリデータとして扱う場合は、データをまとめてベクトルの形で保持できると便利です。read-sequence
関数とwrite-sequence
関数はベクトルやリストなどのシーケンスの形でデータを扱うことができます。
read-sequence
関数はあらかじめ特定の要素数を持つシーケンスを用意しておき、そのシーケンスを第1引数として渡します。ファイルの大きさが巨大であれば、1024の倍数の要素数でバッファとなるベクトルを用意し、その要素数の単位で読み込みます(Alexandriaという定番ライブラリのcopy-file
関数では4096がバッファサイズとして用いられています)。
一方、ファイルの大きさがそれほど大きくない場合は、ファイルサイズを先に取得してから、全てを一括で読み込むのが一般的です。ファイルの大きさを取得するには
file-length
関数を使います。この関数が返すのはファイルサイズですが、その単位はストリームの:element-type
で指定した型となります。
ここでは、最も一般的な例として、
'(unsigned-byte 8)
を指定してファイルを一括で読み込んでみます。
(with-open-file (in #p"test.txt" :element-type '(unsigned-byte 8))
(let* ((size (file-length in))
(buf (make-array size :element-type '(unsigned-byte 8))))
(read-sequence buf in)
buf))
; => #(72 101 108 108 111 44 10 87 111 114 108 100 33 10 10 67 111
; 109 109 111 110 32 76 105 115 112 227 129 174 228 184 150
; 231 149 140 227 129 184 10 227 130 136 227 129 134 227 129
; 147 227 129 157 227 128 130 10)
10進法なので分かりづらいかもしれませんが、今まで読み込んだテキストファイルと同じデータが1つのベクトルに格納されています。
その他のオペレータ
基本的な入出力はこのページで紹介したことだけでたりますが、もう少し知っておいた方がいいオペレータがありますので、追加で紹介します。
標準入出力: *standard-input*
, *standard-output*
, *error-output*
Common Lispの処理系を立ち上げた時に自動的に開かれ、処理系を終了した時に自動的に閉じられるストリームがいくつか用意されています。その中でも標準入力・標準出力・標準エラー出力の3種類の出力はUnixとの関係でもよく利用されます。これらはスペシャル変数なのでオペレータではありませんが、稀に使うことがあります。
*standard-input*
; => #<IO SYNONYM-STREAM *TERMINAL-IO*>
*standard-output*
; => #<IO SYNONYM-STREAM *TERMINAL-IO*>
*error-output*
; => #<IO SYNONYM-STREAM *TERMINAL-IO*>
標準では全て端末に接続されているので、実体は
*terminal-io*
へ接続されているストリーム(エイリアス)になっています。
ストリームの属性: stream-element-type
, stream-external-format
open
関数やwith-open-file
マクロでストリームを開く時に、明示的にキーワード引数を指定すればどのようなストリームであるかは当然分かりますが、標準でどのようなストリームを生成するかを知りたければ、とりあえず開いてみて後から調べるのが早いでしょう。
ストリームはオブジェクトなので、ストリームの内容ではなくストリーム自体の情報も保持しています。どのような種類の情報を扱うストリームであるかは
stream-element-type
関数で、テキストストリームの場合の文字符号化形式はstream-external-format
関数で調べることができます。
(with-open-file (in #p"test.txt")
(format t "(~a, ~a)~%"
(stream-element-type in)
(stream-external-format in)))
; (CHARACTER, #<ENCODING UTF-8 UNIX>)
; => NIL
改行: terpri
, fresh-line
改行を出力したい場合、改行文字である#\Newline
を出力すれば構いません。
(princ #\Newline)
;
; => #\Newline
もう少し簡単な方法で、
terpri
関数というものを使うこともできます。
(terpri)
;
; => NIL
ファイルに出力する場合など、すでに改行が出力されている時には二重に出力することを避け、手前に改行がない場合にのみ改行したいという場合もあります。このような場合は
fresh-line
関数を使用してください。インタプリタでは違いが分からないと思います。
0 件のコメント :
コメントを投稿