Clojure でのファイルの入出力

今回は Clojure でファイルの読み書きを行う方法についての記事です。

ファイルの読み込み

Clojure でファイルの読み込みを行う場合、 reader を使います。
この reader などの IO 系の関数は Ver. 1.3 まで contrib.duck-streams などに分かれていましたが、 今はほとんど clojure.java.io に統合されています。
(use 'clojure.java.io)
(reader "test.txt")   ; #<BufferedReader>
ファイルなので使い終わったら閉じる必要があります。 リソースの解放を忘れずにしないといけない場合には、 Ruby のブロックや C# の using のように、ブロックを抜けると必ず解放してくれる仕組みが欲しいところです。
Clojure にも with-open というマクロが用意されています。
(with-open [fin (reader "test.txt")]
  ;; 読み取り処理
  )
このブロックの中で reader が返すオブジェクト(fin)を使って読み取り処理を行います。 これは Java の BufferedReader オブジェクトなので、 .read や .readLine といった関数が使えます。
それをそのまま使ってもいいのですが、もっと Clojure っぽい書き方もできます。 line-seq を使うとシーケンスとして扱えるようになります。

readsamp.clj :
(use 'clojure.java.io)

(with-open [fin (reader "test.txt")]
  (doseq [str (line-seq fin)]
    (println str))
  )
~/clojure/fileio $ cat test.txt 
foo
bar
~/clojure/fileio $ clojure.bat readsamp.clj 
foo
bar
サンプルコードの実行方法などに関しては以前の記事をご覧下さい。

ファイルの書き込み

書き込みの場合は writer を使います。 使い方は読み込みとほとんど同じですが、 オプションで :append を使うと追加書き込みができます。

(use 'clojure.java.io)

(with-open [fout (writer  "hello.txt" :append true)]
  (.write fout (str "hello" " world"))
  )
writer の戻り値は BufferdWriter のオブジェクトであり、 書き込みの場合はそのメソッドをそのまま使っています。

日本語の文字コード

日本で使う以上、日本語のファイルを読み書きできなければなりません。 日本語を扱うためには reader, writer のオプション :encoding で文字コードを指定します。
(writer "java_io.txt" :encoding "UTF-8")
ここで指定する文字コードは Java でサポートされているコードです。 よく使われている文字コードのエンコード名は次表のようになります。
エンコード名 文字コード
ISO-2022-JP JIS コード
EUC-JP 日本語 EUC
SJIS シフト JIS
MS932 Windows-31J
(シフト JIS とほとんど同じだけど、Windows の文字コードは正確にはこちら)
UTF-8 8 bit Unicode
JISAutoDetect 文字コードの自動判定 (reader のみ)

出力の時はいいのですが、読み込み時には文字コードがわかっていないことの方が普通です。 JISAutoDetect を指定していると自動で文字コードを判別してくれます。

jcode.clj :
(use 'clojure.java.io)

(with-open [fin (reader "japanese.txt" :encoding "JISAutoDetect")]
  (doseq [str (line-seq fin)]
    (println str))
  )
~/clojure/fileio $ cat japanese.txt 
こんにちは、日本
~/clojure/fileio $ clojure.bat jcode.clj 
こんにちは、日本

標準入力、標準出力

標準出力等を使う場合には次の変数を使います。
対象 変数 特殊変数
標準入力 System/in *in*
標準出力 System/out *out*
標準エラー出力 System/err *err*

*in*, *out* などの変数は read-line や println で使われる出力先です。

interact.clj :
(print "Input name : ")
(flush)
(let [name (read-line)]
  (println "Hello" name))
*in*, *out* はデフォルトでは System/in、System/out になっているのですが、これはスクリプトや REPL など状況でクラスは違ってきます。

標準入出力と Java のファイル IO クラス

標準入出力や *in*, *out* で使われているクラスを見てみます。

ioclass.clj :
(println "*in* " (class *in*))
(println "*out*" (class *out*))
(println "*err*" (class *err*))
(println "System/in " (class System/in))
(println "System/out" (class System/out))
(println "System/err" (class System/err))
~/clojure/fileio $ clojure.bat ioclass.clj 
*in*  clojure.lang.LineNumberingPushbackReader
*out* java.io.OutputStreamWriter
*err* java.io.PrintWriter
System/in  java.io.BufferedInputStream
System/out java.io.PrintStream
System/err java.io.PrintStream
C, C++ など多くの言語では、標準入出力とファイルの IO クラスは同じように扱えるのが普通です。
これに対して Java の IO は複雑で使いづらいと悪名を馳せています。

Clojure のファイル IO も呼び出しを軽くラップしているだけで、 実質は Java のクラスです。 このため、文字コードとあわせて突き詰めていくと非常に複雑になります。
ここではファイル IO と標準入出力を同じように扱うための方法を少しだけ書いてみたいと思います。

入力

読み込みで挙げた line-seq 関数は System/in のクラスである BufferedInputStream に対しては使えません。
BufferedInputStream などから Reader を作る必要があります。
(doseq [str (line-seq (clojure.java.io/reader *in*))]
  (println str))

出力

標準出力では println などメソッドに持つ PrintStream クラスです。 BufferdWriter で print 系のメソッドを使うには PrintWriter のオブジェクトにする必要があります。
(use 'clojure.java.io)
(import (java.io PrintWriter))

(with-open [fout (PrintWriter. (writer  "hello.txt"))]
  (.println fout (str "hello" " world"))
  )
~/clojure/fileio $ clojure.bat printsamp.clj 
~/clojure/fileio $ cat hello.txt 
hello world

ファイルの一括読み込み、書き出し

ファイルを一行づつ読み書きするのではなく、一度に読み取ったり、出力したりすることもできます。
  • 読み込み :
  • 書き出し :
    • (spit f contents & options)
spitslurp.clj :
(spit "version.txt" "Ver. 1.2.3")
(println (slurp "version.txt"))
~/clojure/fileio $ clojure.bat spitslurp.clj 
Ver. 1.2.3
指定するオプションは reader, writer で使用したものと同じものが使用できます。
(spit "event.log" "foo\n" :append true)
また、 slurp の入力元はローカルファイルだけでなく、ネット上のファイルも可能ですし、 Reader, InputStream のような入力用のオブジェクトも利用できます。
(slurp "http://yohshiy.blog.fc2.com/blog-category-29.html")
(slurp *in*)

サンプルコード

最後にもう少し長いサンプルとして簡単な cat プログラムを作ってみました。 cat は引数として受け取ったファイルを標準出力に出力するプログラムです。
対象 リンク
ブラウズ simpcat
圧縮ファイル simpcat.zip

以前、標準入出力にも対応するべきという記事を書きました。 ここでもなるべく頑張って対応するようにしています。
  • 引数を全て省略すると標準入力
  • -o(--ouput) オプションでファイルに出力
~/clojure/fileio/simpcat $ lein run -- -h
Usage: simpcat [Options] [FILE ...]

FILE: Input file path.  (Default: standard input)

Options:
  -h, --help             Show help.
  -v, --version          Show program version.
  -o, --output OUT_FILE  Output file path (Default: standard output)
オプション引数の解析には前回紹介した tools.cli を使用しています。
;; オプション仕様定義
(def option-spec
  [["-h" "--help" "Show help."]
   ["-v" "--version" "Show program version."]
   ["-o" "--output OUT_FILE" "Output file path (Default: standard output)"]
   ])

(defn -main [& args]
  (let [{:keys [options arguments errors summary]} (parse-opts args option-spec)]
    :
--output オプションがあれば、出力先はオプション引数で指定されたファイルになります。
    (if (:output options)
      (with-open [fout (PrintWriter. (writer (:output options)))]
        (cat-files arguments fout))
      (cat-files arguments System/out))
入力先には、引数が空であれば標準入力を使い、そうでなければ入力ファイルとして順に使います。
(defn cat-files [fpaths fout]
  (if-not (empty? fpaths)
    (doseq [fpath fpaths]
      (with-open [fin (reader fpath)]
        (print-file fin fout)
        ))
    (print-file (reader System/in) fout)
    ))
入出力用のクラスを調整して渡しているので、ファイルの読み込み、 書き出し処理では分岐することなく、同じように処理できます。
(defn print-file [fin fout]
  (doseq [str (line-seq fin)]
    (.println fout str)))  
関連記事
スポンサーサイト
Prev.    Category    Next 

Facebook コメント


コメント

コメントの投稿

Font & Icon
非公開コメント

このページをシェア
アクセスカウンター
アクセスランキング
[ジャンルランキング]
コンピュータ
43位
アクセスランキングを見る>>

[サブジャンルランキング]
プログラミング
3位
アクセスランキングを見る>>
カレンダー(アーカイブ)
プルダウン 降順 昇順 年別

05月 | 2017年06月 | 07月
- - - - 1 2 3
4 5 6 7 8 9 10
11 12 13 14 15 16 17
18 19 20 21 22 23 24
25 26 27 28 29 30 -


はてな新着記事
はてな人気記事
ブロとも申請フォーム
プロフィール

yohshiy

Author:yohshiy
職業プログラマー。
仕事は主に C++ ですが、軽い言語マニアなので、色々使っています。

はてブ:yohshiy のブックマーク
Twitter:@yohshiy

サイト紹介
プログラミング好きのブログです。プログラミング関連の話題や公開ソフトの開発記などを雑多に書いてます。ただ、たまに英語やネット系の話になることも。