実践Common lispを読み始めた-第14章 ファイルとファイルI/O

ファイルのOpen

  • ファイルのopenはopen関数を使う。

-openの時に、指定したファイルが見つからなかった時のエラー時の挙動を変えることが出来る。:if-does-not-exist引数に

  • ファイルを閉じるときにはclose関数がある。

ファイルからデータを読む

  • openで開いたファイルの入力ストリームを読む方法としては

-read-char(一文字単位)
-read-line(行単位)
-read(S式を読んでlispのオブジェクトを返す。REPLの'R'に相当)

;;
;; "/Users/patterson/lisp/name.txt"
;;
(1 2 3)
456
"a string"
((a b)
    (c d))
;;

CL-USER> (defparameter *s* (open "/Users/patterson/lisp/name.txt"))
*S*
CL-USER> (read *s*)
(1 2 3)
CL-USER> (read *s*)
456
CL-USER> (read *s*)
"a string"
CL-USER> (read *s*)
((A B) (C D))

バイナリデータの読み込み

  • openの時に:element-type引数に'(unsigned-byte 8)を指定する。

バッファリング

  • read-sequenceを使う。バッファリングすることで性能差がでるのか確かめてみた。read.datには50bytes×1000000行のデータが記録されている。
;;
;; readtest.lisp
;;
(defun read-line-test () 
  (time 
   (let ((in (open "/Users/patterson/lisp/read.dat")))
     (loop for line = (read-line in nil in) ;;一行ずつ読む
	until (eq line in)))))

(defun read-with-buffer-test ()
  (time 
   (let ((in (open "/Users/patterson/lisp/read.dat"))
	 (buffer (make-array 4096))) ;;バッファには配列を使用
     (loop for bytes = (read-sequence buffer in) ;;4096bytesのバッファを使用
	until (= bytes 0)))))

(defun read-with-buffer-test2 ()
  (time 
   (let ((in (open "/Users/patterson/lisp/read.dat"))
	 (buffer (make-string 4096))) ;;バッファにstringを使用
     (loop for bytes = (read-sequence buffer in) ;;4096bytesのバッファを使用
	until (= bytes 0)))))

実行結果は

CL-USER> (read-line-test)
; cpu time (non-gc) 70 msec user, 0 msec system
; cpu time (gc)     30 msec user, 10 msec system
; cpu time (total)  100 msec user, 10 msec system
; real time  115 msec
; space allocation:
;  37 cons cells, 12,005,672 other bytes, 0 static bytes
NIL
CL-USER> (read-with-buffer-test)
; cpu time (non-gc) 210 msec user, 20 msec system
; cpu time (gc)     0 msec user, 0 msec system
; cpu time (total)  210 msec user, 20 msec system
; real time  229 msec
; space allocation:
;  38,694 cons cells, 22,072 other bytes, 0 static bytes
NIL
CL-USER> (read-with-buffer-test2)
; cpu time (non-gc) 50 msec user, 10 msec system
; cpu time (gc)     0 msec user, 0 msec system
; cpu time (total)  50 msec user, 10 msec system
; real time  67 msec
; space allocation:
;  37 cons cells, 13,888 other bytes, 0 static bytes
NIL
  • 予想に反して一行ずつ読んだ場合の処理は速い。
  • 一方で、一行ずつ読んだ場合はメモリの使用量が圧倒的に多い。その影響でGCも動いている。
  • bufferに文字列を指定した方が、arrayよりも時間的、空間的にも有利。

ファイル出力

  • ファイルを書き込みモードでオープンするには、openのオプション:directionを:outputにする。
  • openのオプション:if-existsキーワードで指定したファイルが存在したときに既存のファイルの置き換え(:supersede)、追加(:append)、上書き(:overwrite)、nil(openの戻り値にnilを返す)を指定できる。:supersedeと:overwriteの違いは、:supersedeは既存のファイルの内容が全部消えるのに対して、:overwriteは上書きなので、元のファイルよりも書き込むデータが少ない場合元のファイルのデータの一部が残る。
CL-USER> (let ((out (open "/Users/patterson/lisp/foo" :direction :output :if-exists :supersede)))
	   (write-string "test." out) ;;fooの中身は"test."
	   (close out))
T
CL-USER> (let ((out (open "/Users/patterson/lisp/foo" :direction :output :if-exists :supersede)))
	   (write-string "test2." out) ;;fooの中身は"test2."
	   (close out))
T
CL-USER> (let ((out (open "/Users/patterson/lisp/foo" :direction :output :if-exists :overwrite)))
	   (write-string "test3." out) ;;fooの中身は"test3."
	   (close out))
T
CL-USER> (let ((out (open "/Users/patterson/lisp/foo" :direction :output :if-exists nil)))
	   (write-string "test4." out) ;;openでnilが返るのでfooの中身は"test3."のまま
	   (close out))
test4.
; Evaluation aborted
CL-USER> (let ((out (open "/Users/patterson/lisp/foo" :direction :output :if-exists :overwrite)))
	   (write-string "TE" out) ;;fooの中身は"TEst3."
	   (close out))
T
CL-USER> (let ((out (open "/Users/patterson/lisp/foo" :direction :output :if-exists :supersede)))
	   (write-string "TE" out) ;;fooの中身は"TE"
	   (close out))
T

ファイルのClose

  • openとcloseでファイルのOpen/Closeが出来るけど、closeの呼び忘れを考えるとあんまりよろしくない。そこでwith-open-fileマクロを使う。
CL-USER> (with-open-file (out "/Users/patterson/lisp/foo" :direction :output :if-exists :supersede)
	   (format out "Hello world.")) ;;fooの中身は"Hello world."

上の例ではoutはwith-open-fileの中のブロックでのみ有効となり、ブロックから外れると(つまり(format out "Hello world.")を実行したら)closeされる。

pathname

  • プラットフォームに依存しない抽象的なファイル名の表現のこと。理想的だけど複雑らしい。
  • ホスト、デバイスディレクトリ、ネーム、タイプ、バージョンという6つの要素を使ってファイル名を表現する。
  • 文字列のファイル名からパスネームへの変換にはpathname関数を使う。
  • パスネームの個々の要素を調べるにはpathname-directory, pathname-name, pathname-host, pathname-type, pathname-device, pathname-versionを使う。
  • パスネームから文字列のファイル名に変換するにはnamestring, directory-namestring, file-namestringを使う。
CL-USER> (defparameter *foo* (pathname "/Users/patterson/lisp/foo"))
*FOO*
CL-USER> (pathname-directory *foo*)
(:ABSOLUTE "Users" "patterson" "lisp")
CL-USER> (pathname-name *foo*)
"foo"
CL-USER> (pathname-host *foo*)
NIL
CL-USER> (pathname-device *foo*)
:UNSPECIFIC
CL-USER> (pathname-type *foo*)
NIL
CL-USER> (pathname-version *foo*)
:UNSPECIFIC
CL-USER> (namestring *foo*)
"/Users/patterson/lisp/foo"
CL-USER> (directory-namestring *foo*)
"/Users/patterson/lisp/"
CL-USER> (file-namestring *foo*)
"foo"
 ||<

**新しいパスネームの構築
-新規にパスネームを作るにはmake-pathnameを使う。