M.Hiroi's Home Page

Common Lisp Programming

お気楽 Common Lisp プログラミング入門

[ PrevPage | Common Lisp | NextPage ]

ファイル入出力

今回は、ファイルからデータを読み込む、またはデータをファイルに書き込むなど、Common Lisp でデータの入出力を行う基本的な方法について説明します。最初に、最も簡単で基本的な「標準入出力」について説明します。

●標準入出力とは?

「標準入出力」は難しい話ではありません。実は、いままでに何回も使っているのです。たとえば、format や print を使ってデータを画面に表示しましたね。これはデータを「標準出力 (standard output)」へ出力していたのです。まだ説明していませんが、データの入力には read という関数があります。これは「標準入力 (standard input)」からデータを受け取ります。一般に、標準入力にはキーボードが割り当てられ、標準出力には画面が割り当てられています。

近代的なプログラミング言語の場合、ファイルをアクセスするためには「ストリーム (stream)」というデータを用います。辞書を引いてみると stream は「流れ」や「小川」という意味です。プログラミング言語の場合は、ファイルとプログラムの間でやりとりされるデータの流れ、という意味で使われているようです。

Common Lisp にはストリーム (stream) というデータ型が用意されています。ストリームはファイルと 1 対 1 に対応していて、ファイルからデータを入力する場合、ストリームを介してデータが渡されます。逆に、ファイルへデータを出力するときも、ストリームを介して行われます。

Common Lisp の場合、処理系を起動すると自動的に用意されるストリームがいくつかあります。その中には標準入出力に対応するストリームもあります。標準入力、標準出力、標準エラー出力に対応するストリームは次の変数に格納されています。

標準エラー出力の出力先は標準出力と同じく「画面」ですが、エラーメッセージを出力するときに使います。それでは、標準入出力ストリームの値を見てみましょう。

* *standard-input*

#<SYNONYM-STREAM :SYMBOL SB-SYS:*STDIN* {1000025193}>
* *standard-output*

#<SYNONYM-STREAM :SYMBOL SB-SYS:*STDOUT* {1000010A43}>
* *error-output*

#<SYNONYM-STREAM :SYMBOL SB-SYS:*STDERR* {1000002D03}>

Common Lisp の入出力関数はストリーム引数を持ちますが、それを省略した場合、多くの入力関数は規定値に *standard-input* を使い、出力関数は規定値に *standard-output* を使います。

●read と print

簡単な入出力は read と print を使って行うことができます。SBCL の REPL で次のプログラムを実行してください。

* (read)        <= Return を入力
foo             <= Return を入力

FOO             <= read の返り値 シンボルとして読み込む
* (read)
1234

1234            <= 数値として読み込む
* (read)
(a b c d)

(A B C D)       <= リストとして読み込む
* (read)
#(1 2 3 4)

#(1 2 3 4)      <= ベクタとして読み込む

read はデータを読み込んで S 式に変換して返します。read はデータを読み込むだけで、S 式の評価は行わないことに注意してください。S 式を評価するには関数 eval を使います。次の例を見てください。

* (read)          <= Return を入力
(* 4 4)

(* 4 4)           <= read の返り値
* (eval (read))   <= Return を入力
(* 4 4)

16                <= eval の返り値

まず、eval の引数 (read) が評価され、S 式 (* 4 4) が読み込まれます。次に、読み込んだ S 式が eval で評価されるので、(* 4 4) の評価値 16 が eval の返り値になります。

次は print を説明します。print が出力するデータは、エスケープコードを使って印字されます。たとえば、文字列は " で括られます。つまり、read で読み込める形で印字されます。print は改行してからデータを出力し、最後に空白文字をひとつ付けます。print の返り値は出力したデータです。

このほかに、関数 prin1 と princ があります。prin1 は print と同じ形式で出力しますが、改行と空白文字は付加しません。princ はエスケープコードを使わないで出力します。たとえば、文字列の場合は " で括られずにそのまま出力されます。簡単な例を示しましょう。

* (progn (print "hello, world") (format t "]"))

"hello, world" ]     ; 改行してから出力して空白が最後に付く
NIL
* (progn (prin1 "hello, world") (format t "]"))
"hello, world"]      ; 改行しないし最後に空白も付かない
NIL
* (progn (princ "hello, world") (format t "]"))
hello, world]        ; " で括られていない
NIL

●ファイルのアクセス

ファイルにアクセスする場合、次の 3 つの操作が基本になります。

  1. アクセスするファイルをオープンする
  2. 入出力関数を使ってファイルを読み書きする。
  3. ファイルをクローズする。

「ファイルをオープンする」とは、アクセスするファイルを指定して、それと一対一に対応するストリームを生成することです。入出力関数はそのストリームを経由してファイルにアクセスします。Common Lisp の場合、ファイルをオープンするには関数 open を使います。オープンしたファイルは必ずクローズしてください。この操作を行う関数が close です。

そして Common Lisp には、この 2 つの動作を行ってくれる便利なマクロ with-open-file が用意されています。基本的な使い方を示します。

(with-open-file (変数 ファイル名 :direction [:input or :output]) ... )

with-open-file は与えられたファイルをキーワード :direction で指定した方向でオープンし、生成したストリームを変数にセットします。変数は局所変数として扱われ、with-open-file が実行されている間だけ有効です。あとは、引数として与えられた S 式を順番に評価します。with-open-file の実行が終了すると、ファイルは自動的にクローズされます。次の図を見てください。

上図はカレントディレクトリでファイル test.dat をオープンした場合です。ファイル名は文字列、パス名、ストリームで指定します。パス名は Common Lisp のデータ型の一つで、型名は pathname です。パス名はあとで説明します。ここでは文字列で指定することにします。ファイル名のパス区切り記号にはスラッシュ / を使います。\ は文字列のエスケープコードに割り当てられているため、パス区切り記号には使えないことに注意してください。

test.dat へデータを出力する場合は、キーワード :direction に :output [*1] を指定します。変数 out には出力ストリームがセットされ、これを経由してデータを test.dat に書き込むことができます。逆に、test.dat からデータを読み込む場合は、:direction に :input [*2] を指定します。今度は 変数 in に入力ストリームがセットされ、これを経由して test.dat からデータを読み込むことができます。

ストリームのアクセス方法は、print や read などの入出力関数でアクセスするストリームを指定します。これを「ストリーム引数」といいます。指定を省略した場合は、標準入出力が使用されます。ストリームの指定方法は次のようになります。

print data 出力ストリーム
read 入力ストリーム eof-err eof-value
     eof-err   : ファイルの終了を検出した場合の処理指定
     eof-value : ファイルの終了を検出した場合の返り値

print の場合は出力するデータの後ろにストリームを指定します。入力ストリームを指定するとエラーになります。read の場合、入力ストリームを指定したあとで、オプションパラメータ eof-err と eof-value を設定することができます。

入力ストリームの場合、ファイルに格納されているデータには限りがあるので、ストリームからデータを取り出していくと、いつかはデータがなくなります。この状態を「ファイルの終了 (end of file : EOF)」といいます。

eof-err と eof-value はファイルが終了したときの動作を設定します。eof-err を省略したり NIL 以外の値を指定すると、ファイルが終了したときにエラーが発生します。eof-err に NIL を指定した場合、ファイルが終了すると eof-value に設定した値を返します。eof-value が省略された場合は NIL を返します。

簡単な例を示しましょう。

* (with-open-file (out "test.txt" :direction :output)
  (dotimes (x 10) (print x out)))

NIL
* (with-open-file (in "test.txt" :direction :input)
  (loop (let ((num (read in nil))) (if (not num) (return)) (print num))))

0
1
2
3
4
5
6
7
8
9
NIL

test.txt に 0 から 9 までの数値を書き込みます。それから、test.txt のデータを読み込みます。データの書き込みは print を、データの読み込みは read を使えば簡単です。print は余分な空白を付けますが、read は空白や改行などの空白文字は読み飛ばすので、数値データを読み込むことができます。

loop の中で read の返り値を変数 NUM に代入しています。read の eof-err には NIL を指定しているので、ファイルの終了を検出すると read は NIL を返します。したがって、if の条件部が成立して loop から return で脱出します。

-- note --------
[*1] 既に同じ名前のファイルが存在している場合はエラーになります。ファイルが存在している場合の動作はキーワード :if-exists で細かく指定することができます。
[*2] ファイルが存在しない場合はエラーになります。この動作はキーワード :if-does-node-exist で指定することができます。

●read-line と read-char

関数 read-line はストリームから文字を読み込み、改行文字までのデータを文字列として返します。改行文字は文字列に含まれません。関数 read-char はストリームより 1 文字読み込み、それを文字型データとして返します。read-line と read-char は、read と同じようにストリームとファイル終了時の動作を指定することができます。

簡単な例を示しましょう。3 行のテキストが書いてあるファイル test1.txt を、read-line と read-char で読み込んで表示します。

test1.txt の内容
---------------
abcd
efgh
ijkl
* (with-open-file (in "test1.txt" :direction :input)
  (loop (let ((buff (read-line in nil))) (if (not buff) (return)) (print buff))))

"abcd" 
"efgh" 
"ijkl" 
NIL
* (with-open-file (in "test1.txt" :direction :input)
  (loop (let ((c (read-char in nil))) (if (not c) (return)) (print c))))

#\a
#\b
#\c
#\d
#\Newline
#\e
#\f
#\g
#\h
#\Newline
#\i
#\j
#\k
#\l
#\Newline
NIL

#\Newline は改行を表す文字で、整数値で表すと 10 (#x0a) になります。ちなみに、関数 char-code で文字を整数値に変換することができます。逆に、整数値を文字に変換するには関数 code-char を使います。次の例を見てください。

* (char-code #\Newline)

10
* (code-char 10)

#\Newline

●write-line と write-char

関数 write-line は文字列をストリームに書き込みます。このとき、改行文字が付加されます。改行文字を付加しない関数 write-string もあります。関数 write-char は文字をストリームに書き込みます。なお、write-line と write-string は :start と :end で指定した部分文字列をストリームに書き込むこともできます。

簡単な例を示しましょう。

* (dotimes (x 5) (write-line "hey! "))
hey!
hey!
hey!
hey!
hey!
NIL
* (dotimes (x 5) (write-string "hey! "))
hey! hey! hey! hey! hey!
NIL
* (dotimes (x 5) (write-char #\A))
AAAAA
NIL
* (dotimes (x 5) (write-char #\HIRAGANA_LETTER_A))
あああああ
NIL

●read-sequence と write-sequence

列 (sequence) にデータを読み込むときは read-sequence を、列の要素を書き込むときは write-sequence を使います。

read-sequence sequence stream &key :start :end => position
write-sequence sequence stream &key :start :end => sequence

read-sequence の返り値は :start + 読み込んだ要素数 になります。列を満たすまでデータを読み込むと、返り値は :end と同じ値になりますが、途中でストリームが終了した場合は :end よりも小さな値になります。write-sequence は引数 sequence をそのまま返します。

列に格納される要素のデータ型はストリームの要素のデータ型に依存します。ストリームの要素のデータ型は関数 stream-element-type で調べることができます。

* (stream-element-type *standard-input*)

CHARACTER
* (stream-element-type *standard-output*)

CHARACTER

このデータ型はファイルをオープンするときにキーワード引数 :element-type で指定することができます。規定値は character です。character は文字型データを表す型シンボルです。

簡単な使用例を示します。

* (setq buff #(0 0 0 0 0))

#(0 0 0 0 0)
* (read-sequence buff *standard-input*)12345

5
* buff

#(#\1 #\2 #\3 #\4 #\5)
* (write-sequence buff *standard-output*)
12345
#(#\1 #\2 #\3 #\4 #\5)
* (read-sequence buff *standard-input* :start 1 :end 4)あいう

4
* buff

#(#\1 #\HIRAGANA_LETTER_A #\HIRAGANA_LETTER_I #\HIRAGANA_LETTER_U #\5)
* (write-sequence buff *standard-output* :start 1 :end 4)
あいう
#(#\1 #\HIRAGANA_LETTER_A #\HIRAGANA_LETTER_I #\HIRAGANA_LETTER_U #\5)

read-sequence は、引数で指定した列の内容を破壊することに注意してください。

●バイナリファイルの操作

ファイルは大きく分けると、「テキスト」と「バイナリ」の 2 種類があります。テキストファイルは特定の文字コード (アスキーコードや UTF-8 など) でエンコードされたデータが格納されています。コマンド cat などで画面に表示したり、エディタで編集することができます。

これに対し、バイナリファイルはテキストファイル以外のものを指します。バイナリファイルはテキストファイルと違い、特定の文字コードの範囲には収まらないデータが含まれているため、cat で画面に表示すると、わけのわからない文字を画面に巻き散らすことになります。

Common Lisp でファイルをオープンする場合、:element-type でストリームの要素のデータ型を決めます。規定値は character なので、オープンしたファイルはテキストファイルとして扱われることになります。バイナリファイルとして扱う場合、:element-type に適切なデータ型を指定します。主なデータ型を以下に示します。

Common Lisp ではバイト (byte) を「任意幅のビット列」という意味で使っていて、私たちがよく使っている「バイト = 8 ビット」という意味ではありません。たとえば、:element-type '(unsigned-byte 8) と指定すれば、ストリームの要素は 0 - 255 の整数値になります。

バイナリファイルのアクセスには次の関数を使います。

read-byte input-stream => integer
write-byte integer output-stream => integer

read-byte は input-stream から要素をひとつ読み込み、それを整数にして返します。write-byte は引数 integer を output-stream に書き込み、integer をそのまま返します。どちらの関数も integer は :element-type で指定したデータ型の範囲内になります。符号無し 8 ビット整数 (0 - 255) に限定されているわけではありません。ご注意くださいませ。

read-byte は read と同様にオプションパラメータ eof-err と eof-value を設定することができます。バイナリファイルのアクセスには read-sequence や write-sequence も使用することができます。

簡単な使用例を示しましょう。

* (with-open-file (out "test.dat" :direction :output :element-type '(unsigned-byte 8))
(dotimes (x 16) (write-byte x out)))

NIL
* (with-open-file (in "test.dat" :direction :input :element-type '(unsigned-byte 8))
(loop (let ((num (read-byte in nil))) (if (not num) (return)) (print num))))

0
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
NIL
* (with-open-file (in "test.dat" :direction :input :element-type '(unsigned-byte 8))
(let ((buff (make-array 16))) (read-sequence buff in) buff))

#(0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15)

ファイル test.dat を (unsigned-byte 8) でライトオープンして、0 から 15 までの数値を write-byte で書き込みます。次に、test.dat を (unsigned-byte 8) でリードオープンし、read-byte でデータを読み込むと、0 から 15 までの数値が得られます。もちろん、read-sequence で test.dat の数値を読み込むこともできます。

●パス名

「パス名 (pathname)」はファイル名を表すデータ型です。異なる OS やファイルシステムでも同じような動作ができるように、ファイルを操作する関数がいろいろ用意されています。

関数 pathname は引数をパス名に変換します。

pathname filename
* (pathname "test.txt")

#P"test.txt"
* (pathname "test2.txt")

#P"test2.txt"
* (pathname "foo/bar.txt")

#P"foo/bar.txt"
* #p"test.txt"

#P"test.txt"

引数は文字列、パス名、ストリームのいずれかです。pathname に文字列を渡した場合、ファイルの有無はチェックされません。また、パス名の表示と同じ構文 #p"test.txt" を入力しても生成することができます。

実在するファイルのパス名を得るには関数 truename か probe-file を使います。

truename filename
probe-file filename

truename と probe-file はそのファイルシステムの中で filename の「本当の名前」をパス名として返します。Unix 系 OS などでシンボリックリンクがある場合は、それを解決した名前が返されます。ファイルが存在しない場合、truename はエラーになりますが、probe-file は NIL を返します。

* (truename "test1.txt")

#P"/home/ ... /test1.txt"
* (probe-file "test1.txt")

#P"/home/ ... /test1.txt"
* (truename "test2.txt")

=> エラー "Failed to find the TRUENAME of /home/ ... /test2.txt:"
* (probe-file "test2.txt")

NIL

パス名を文字列に変換するために次の関数が用意されています。

namestring pathname
file-namestring pathname
directory-namestring pathname

namestring はパス全体を、file-namestring はファイル名を、directory-namestring はファイル名を除いたパスを返します。

* (setq p (truename "test1.txt"))

#P"/home/ ... /test1.txt"
* (namestring p)

"/home/ ... /test1.txt"
* (file-namestring p)

"test1.txt"
* (directory-namestring p)

"/home/ ... /"

●パス名の要素

パス名は 6 つの要素を持っています。

要素の値は関数 pathname-要素名 で取得することができます。

* p

#P"/home/ ... /test1.txt"
* (pathname-host p)

#<SB-IMPL::UNIX-HOST {1000102973}>
* (pathname-device p)

NIL
* (pathname-directory p)

(:ABSOLUTE "home" ...)
* (pathname-name p)

"test1"
* (pathname-type p)

"txt"
* (pathname-version p)

NIL

Windows の場合は次のようになります。

* (setq p (truename "test1.txt"))

#P"C:/Users/ ... /test1.txt"
* (pathname-host p)

#<SB-IMPL::WIN32-HOST {1000304783}>
* (pathname-device p)

"C"
* (pathname-directory p)

(:ABSOLUTE "Users" ...)
* (pathname-name p)

"test1"
* (pathname-type p)

"txt"
* (pathname-version p)

NIL

pathname-dirctory の返り値がリストの場合、先頭要素は :absolute (絶対パス) か :relative (相対パス) になります。たとえば、(:absolute) はルートディレクトリを表します。(:absolute "home" "foo" "bar") は Unix 系 OS であれば "/home/foo/bar/" を表します。

Common Lisp の仕様は、ファイルのバージョン管理を行うシステムも考慮に入れているようです。Ubuntu と Windows の場合、ファイルのバージョンは管理していないので、pathname-version はどちらも NIL になります。

●make-pathname と directory

関数 make-pathname はキーワードで与えられた要素でパス名を作ります。

make-pathname &key :host :device :directory :name :type :version :defaults :case
* (make-pathname :name "test" :type "txt")

#P"test.txt"
* (make-pathname :name "test" :type "txt" :directory '(:relative :up))

#P"../test.txt"
* (make-pathname :name "test" :type "txt" :directory '(:relative "foo" :wild))

#P"foo/*/test.txt"
* (make-pathname :name "test" :type "txt" :directory '(:relative "foo" :wild-inferiors))

#P"foo/**/test.txt"

ディレクトリの指定でキーワードシンボルを使うことができます。:up はディレクトリを一つ上に行くことを表します。:wild は 1 レベルのディレクトリに適合するワイルドカードです。:wild-inferiors は任意のレベルのディレクトリに適合するワイルドカードです。

たとえば、foo/*/test.txt は foo/bar/test.txt, foo/baz/test.txt, foo/oops/test.txt などとマッチングし、foo/**/test.txt は foo/test.txt, foo/bar/test.txt, foo/bar/baz/test.txt, foo/bar/baz/oops/test.txt などにマッチングします。

関数 directory は pathname と一致するファイルやディレクトリを探索し、見つけたファイルまたはディレクトリのパス名をリストに格納して返します。

directory pathname

引数 pahname は文字列、パス名、ストリームのいずれかを渡します。SBCL の場合、ファイル名の指定にグロブ (glob) と同様のメタ文字 *, ?, [] を使用することができます。ただし、[] の中で ^ や - を使うことはできないようです。ワイルドカード :wild-inferiors (**) を使って、ディレクトリを再帰的にたどることも可能です。

簡単な使用例を示します。


* (directory "~/work/foo/test?.txt")

(#P"/home/mhiroi/work/foo/test1.txt")
* (directory "~/work/foo/*/test?.txt")

(#P"/home/mhiroi/work/foo/bar/test2.txt")
* (directory "~/work/foo/*/*/test?.txt")

(#P"/home/mhiroi/work/foo/bar/baz/test3.txt")
* (directory "~/work/foo/**/test?.txt")

(#P"/home/mhiroi/work/foo/bar/baz/test3.txt"
 #P"/home/mhiroi/work/foo/bar/test2.txt"
 #P"/home/mhiroi/work/foo/test1.txt")
* (directory "~/work/foo/**/")

(#P"/home/mhiroi/work/foo/"
 #P"/home/mhiroi/work/foo/bar/"
 #P"/home/mhiroi/work/foo/bar/baz/")

●:if-exists と :if-does-not-exist

ファイルをオープンするとき、既に同じ名前のファイルが存在している場合があります。この場合、キーワード :if-exists を使って動作を細かく指定することができます。:if-exists は :direction が :output または :io (input/output, 入出力両用) の場合に有効です。

Common Lisp では、次のキーワードが用意されています。

:error
:new-version
:rename
:rename-and-delete
:overwrite
:append
:supersede
nil

Common Lisp の場合、:if-exists を省略したときの既定値は :error か :new-version になります。CLtL2 (参考文献 2) 574 ページから引用します。

Ubuntu と Windows の場合、ファイルのバージョン管理はしていないので、SBCL でのバージョン要素は NIL になっています。ファイルが存在するとき、SBCL がエラーを発するのは Common Lisp の仕様に適合した動作になります。

:if-exists に :supersede を指定すると、ファイルが存在するときはファイルを長さ 0 に切り詰めてからデータを書き込みます。:overwrite を指定すると、ファイルの長さを 0 に切り詰めずに、先頭からデータを上書きします。この場合、ファイルが存在していないとエラーになります。

:append を指定した場合、既存のファイルの最後尾にデータが追加されます。:append もファイルが存在していないとエラーになります。NIL を指定した場合はファイルをオープンしません。この場合、関数 open は NIL を返します。

:rename を指定した場合、現在あるファイルの名前を変更して、指定した名前で新しいファイルを生成します。:rename-and-delete を指定した場合、:rename と同じように新しいファイルを生成して、ストリームを閉じるときに名前を変更したファイルを削除します。SBCL の場合、たとえば test.txt は test.txt.bak という名前に変更されます。

:if-does-not-exist はファイルが存在していないときの動作を指定します。Common Lisp では、次のキーワードが用意されています。

:error
:create
nil

:if-does-not-exist に :error を指定すると、ファイルが存在していない場合はエラーになります。:direction に :input を指定する場合や、:if-exists に :overwrite や :append を指定する場合の既定値になります。

:create を指定すると、新しいファイルを生成します。:direction が :output または :io で、:if-exists が :overwrite でも :append でもない場合の既定値になります。NIL を指定すると、ファイルをオープンしません。この場合、関数 open は NIL を返します。

簡単な使用例を示しましょう。ファイル test.dat にデータを書き込みます。test.txt がなければ新しくファイルを作成し、既に test.txt がある場合は最後尾にデータを追加します。プログラムは次のようになります。

(with-open-file (out "test.txt" :direction :output
                                :if-exists :append :if-does-not-exist :create)
  (dotimes (x 10) (print x out)))

:if-exists に :append を指定した場合、デフォルトでは test.dat が存在しないとエラーになります。そこで、:if-does-not-exists に :create を指定して、test.dat が存在しないときは新しいファイルを生成するようにします。

●問題

次に示す関数を定義してください。

  1. 複数のテキストファイルを表示する cat-file filename ...
  2. テキストファイルの先頭 10 行を表示する head-file filename
  3. テキストファイルの最後 10 行を表示する tail-file filename
  4. 2 つのテキストファイルを行単位で連結する paste-file file1 file2
  5. ファイルのエントロピーを計算する entoropy filename
    各記号 ai の出現確率 P(ai) がわかると、次の式でエントロピー H を求めることができます。
    H = - Σ P(ai) * log2 P(ai)  (ビット)
           i
    

エントロピーについては拙作のページ Algorithms with Python シャノン・ファノ符号とハフマン符号 をお読みください。













●解答1

リスト : 複数のファイルを表示する

(defun cat-file (&rest args)
  (dolist (file args)
    (with-open-file
     (in file :direction :input)
     (let (buff)
       (loop
        (if (not (setq buff (read-line in nil)))
            (return))
        (write-line buff))))))

dolist で引数 ARGS からファイル名を順番に取り出して、with-open-file でリードオープンします。あとはファイルの終了を検出するまで read-line で 1 行ずつ読み込み、それを write-line で標準出力に書き込むだけです。

●解答2

リスト : ファイルの先頭 10 行を表示する

(defun head-file (file)
  (with-open-file
   (in file :direction :input)
   (let (buff)
     (dotimes (n 10)
       (if (not (setq buff (read-line in nil)))
           (return))
       (write-line buff)))))

with-open-file で引数 FILE をリードオープンします。あとは dotimes で 10 行読み込んで、write-line で出力します。途中でファイルの終了を検出した場合は return でループを脱出します。

●解答3

リスト : ファイルの末尾 10 行を表示する

(defun tail-file (file)
  (let (buff queue)
    (with-open-file
     (in file :direction :input)
     (loop
      (if (not (setq buff (read-line in nil)))
          (return))
      (setq queue
            (append (if (< (length queue) 10) queue (cdr queue))
                    (list buff)))))
    (dolist (xs queue) (write-line xs))))

読み込んだ直近の 10 行を変数 QUEUE のリストに保持します。QUEUE の長さが 10 に満たない場合、QUEUE の末尾に読み込んだ行 (list buff) を連結します。QUEUE の長さが 10 の場合は、cdr で先頭要素を取り除いてから (list buff) を連結します。最後に dolist で QUEUE に格納されている行を write-line で出力します。

なお、このプログラムはリストの連結に append を使っているので、効率はよくありません。興味のある方はプログラムを改良してみてください。

●解答4

リスト : ファイルを行単位で連結する

(defun flush-file (in buff)
  (write-line buff)
  (loop
   (if (not (setq buff (read-line in nil)))
       (return))
   (write-line buff)))

(defun paste-file (file1 file2)
  (let (buff1 buff2)
    (with-open-file
     (in1 file1 :direction :input)
     (with-open-file
      (in2 file2 :direction :input)
      (loop
       (setq buff1 (read-line in1 nil)
             buff2 (read-line in2 nil))
       (cond ((and buff1 buff2)
              (format t "~A ~A~%" buff1 buff2))
             (buff1
              (flush-file in1 buff1)
              (return))
             (buff2
              (flush-file in2 buff2)
              (return))
             (t (return))))))))

with-open-file を二重に使って引数 FILE1 と FILE2 をリードオープンします。read-line で 1 行読み込み、変数 BUFF1 と BUFF2 にセットします。両方とも真であれば、format で BUFF1 と BUFF2 を出力します。これで行を連結して表示することができます。

BUFF1 だけが真、または BUFF2 だけが真であれば、関数 flush-file でストリームの内容を最後まで出力して、return でループを脱出します。両方とも偽であれば、どちらのストリームにも行は残っていないので、return でループを脱出するだけです。

●解答5

リスト : ファイルのエントロピーを求める

(defun make-frequency (filename)
  (with-open-file
   (in filename :direction :input :element-type '(unsigned-byte 8))
   (let ((freq (make-array 256 :initial-element 0))
         (c 0))
     (loop
      (if (not (setq c (read-byte in nil)))
          (return freq))
      (incf (aref freq c))))))

(defun entoropy (filename)
  (let* ((freq (make-frequency filename))
         (s (reduce #'+ freq))
         (e 0d0))
    (dotimes (c (length freq) (list e (/ (* e s) 8) s))
      (let ((p (/ (aref freq c) s)))
        (when (plusp p)
          (incf e (- (* p (log p 2)))))))))

関数 make-frequency で記号 (0 - 255) の出現頻度表を作成します。ファイルをオープンするときは :element-type に (unsigned-byte 8) を指定し、read-byte で 1 バイトずつ読み込みます。あとは記号 c に対して incf で (aref frea c) の値を +1 するだけです。関数 entoropy は各記号の出現確率 p を求め、- p * math.log(p, 2) を計算して変数 e に加算します。これでエントロピー e を求めることができます。

それでは、実際に Canterbury Corpus で配布されているテストデータ The Canterbury Corpus のエントロピーを求めてみましょう。

リスト : entoropy のテスト

(defun test ()
  (dolist (file '("alice29.txt" "asyoulik.txt" "cp.html" "fields.c" "grammar.lsp"
                  "kennedy.xls" "lcet10.txt" "plrabn12.txt" "ptt5" "sum" "xargs.1"))
    (let ((result (entoropy file)))
      (format t "~12a  ~8d  ~,6f  ~8,0f~%" file (third result) (first result) (second result)))))
* (test)
alice29.txt     152089  4.567680    86837.
asyoulik.txt    125179  4.808116    75234.
cp.html          24603  5.229137    16082.
fields.c         11150  5.007698     6979.
grammar.lsp       3721  4.632268     2155.
kennedy.xls    1029744  3.573471   459970.
lcet10.txt      426754  4.669118   249071.
plrabn12.txt    481861  4.531363   272936.
ptt5            513216  1.210176    77635.
sum              38240  5.328990    25473.
xargs.1           4227  4.898431     2588.
NIL

各列の項目はファイル名、ファイルサイズ、エントロピー、下限値です。ファイルサイズ * エントロピー で圧縮の下限値を計算することができます。ただし、この結果は無記憶情報源モデルの場合であり、モデル化によってエントロピーの値は異なることに注意してください。


Copyright (C) 2020 Makoto Hiroi
All rights reserved.

[ PrevPage | Common Lisp | NextPage ]