在Clojure 1.3中,如何读写文件?

175

我想知道在Clojure 1.3中读写文件的“推荐”方式。

  1. 如何读取整个文件
  2. 如何逐行读取文件
  3. 如何写入一个新文件
  4. 如何向现有文件添加一行

3
从谷歌获取的第一个结果:http://lethain.com/reading-file-in-clojure/该文章介绍了如何在Clojure中读取文件。它演示了如何使用Java IO类来打开,读取和关闭文件,并提供了一些有用的方法和注意事项。此外,它还展示了如何使用Clojure的一些核心库来更方便地读取文件内容。 - jcubic
8
这个结果是2009年的,最近有些事情发生了变化。 - Sergey
12
的确。这个StackOverflow的问题现在是谷歌搜索的第一个结果。 - mydoghasworms
6个回答

291

假设我们只处理文本文件,而不是一些疯狂的二进制文件。

第一步:如何将整个文件读入内存。

(slurp "/tmp/test.txt")

当文件非常大时,不建议使用此方法。

第二种方法:逐行读取文件。

(use 'clojure.java.io)
(with-open [rdr (reader "/tmp/test.txt")]
  (doseq [line (line-seq rdr)]
    (println line)))
with-open宏会确保在主体结束时关闭reader。reader函数将字符串(也可以是URL等)强制转换为BufferedReaderline-seq返回一个惰性seq,要求惰性seq的下一个元素将导致从reader中读取一行。 请注意,从Clojure 1.7开始,您还可以使用transducers读取文本文件。 第三点:如何写入新文件。
(use 'clojure.java.io)
(with-open [wrtr (writer "/tmp/test.txt")]
  (.write wrtr "Line to be written"))

使用with-open时,它会在代码块结束时关闭BufferedWriter。 Writer将字符串转换为BufferedWriter,您可以通过Java互操作使用它:(.write wrtr "something")。

您还可以使用spit,它是slurp的相反操作:

(spit "/tmp/test.txt" "Line to be written")

第四步:向现有文件追加一行。

(use 'clojure.java.io)
(with-open [wrtr (writer "/tmp/test.txt" :append true)]
  (.write wrtr "Line to be appended"))

与上面相同,但现在具有附加选项。

或者再次使用spit,它是slurp的相反操作:

(spit "/tmp/test.txt" "Line to be written" :append true)

提示:为了更明确地表示你正在读写文件,而不是其他某些内容,你可以先创建一个文件对象,然后将其强制转换为BufferedReaderWriter

(reader (file "/tmp/test.txt"))
;; or
(writer (file "tmp/test.txt"))

文件函数也在clojure.java.io中。

PS2:有时候能够查看当前目录(即“。”)是很方便的。你可以通过两种方式获取绝对路径:

(System/getProperty "user.dir") 
或者
(-> (java.io.File. ".") .getAbsolutePath)

1
非常感谢您详细的回答。 我很高兴了解到1.3中推荐的文件IO(文本文件)方式。似乎有一些关于文件IO的库 (clojure.contrb.io,clojure.contrib.duck-streams 以及一些直接使用Java BufferedReader FileInputStream InputStreamReader的示例) 这让我更加困惑。此外,关于Clojure 1.3的信息很少, 特别是在日语(我的母语)中。谢谢。 - jolly-san
嗨,jolly-san,感谢您接受我的答案!供您参考,clojure.contrib.duck-streams现已过时。这可能会增加混淆。 - Michiel Borkent
也就是说,(with-open [rdr (reader "/tmp/test.txt")] (line-seq rdr)) 返回的是 IOException Stream closed 而不是一系列的行。该怎么办?不过,我已经从 @satyagraha 的答案中得到了很好的结果。 - 0dB
4
这与懒惰有关。当您在with-open之外使用line-seq的结果时,例如将其结果打印到REPL中,那么读取器已经关闭了。解决方法是将line-seq包装在doall中,以立即强制进行评估。 (with-open [rdr (reader "/tmp/test.txt")] (doall (line-seq rdr))) - Michiel Borkent
对于像我这样的真正初学者,请注意 doseq 返回 nil,这可能会导致没有返回值的悲伤时刻。 - Oct

37

如果文件可以放入内存中,你可以使用 slurp 和 spit 来读写它:

(def s (slurp "filename.txt"))

(s现在包含一个文件的内容作为字符串)

(spit "newfile.txt" s)
这将创建newfile.txt文件,如果不存在则写入文件内容。如果你想追加到文件中,可以这样做。
(spit "filename.txt" s :append true)

要逐行读写文件,您需要使用Java的reader和writer。它们被包装在命名空间clojure.java.io中:

(ns file.test
  (:require [clojure.java.io :as io]))

(let [wrtr (io/writer "test.txt")]
  (.write wrtr "hello, world!\n")
  (.close wrtr))

(let [wrtr (io/writer "test.txt" :append true)]
  (.write wrtr "hello again!")
  (.close wrtr))

(let [rdr (io/reader "test.txt")]
  (println (.readLine rdr))
  (println (.readLine rdr)))
; "hello, world!"
; "hello again!"
请注意 slurp/spit 和 reader/writer 示例之间的区别在于后者中文件保持打开状态(在 let 语句中),而且读取和写入是缓存的,因此在反复从/向文件中读取/写入时更加高效。有关更多信息,请参见以下链接:slurpspitclojure.java.ioJava 的 BufferedReaderJava 的 Writer

1
谢谢Paul。通过您清晰明了的代码和评论,我能够更多地学习并专注于回答我的问题。非常感谢。 - jolly-san
感谢您添加了一些Michiel Borkent(优秀)所述的典型情况下最佳方法中未提到的略低级别的方法。 - Mars
@Mars 谢谢。实际上我先回答了这个问题,但是 Michiel 的回答更有结构性,似乎更受欢迎。 - Paul
他在处理常规情况时做得很好,但你提供了其他信息。这就是为什么SE允许多个答案的好处。 - Mars

6

关于问题2,有时候我们希望将行的流作为一等对象返回。为了得到这样的惰性序列,同时在EOF时自动关闭文件,我使用了以下函数:

(use 'clojure.java.io)

(defn read-lines [filename]
  (let [rdr (reader filename)]
    (defn read-next-line []
      (if-let [line (.readLine rdr)]
       (cons line (lazy-seq (read-next-line)))
       (.close rdr)))
    (lazy-seq (read-next-line)))
)

(defn echo-file []
  (doseq [line (read-lines "myfile.txt")]
    (println line)))

7
我认为将 defn 嵌套并不符合 Clojure 的惯用法。据我理解,你的 read-next-line 函数在 read-lines 函数外也可以被访问到。你或许可以使用 (let [read-next-line (fn [] ...)) 代替。 - kristianlm
如果返回创建的函数(闭合打开的读取器),而不是全局绑定,我认为您的答案会更好一些。 - Ward

1

现在,您不再需要使用interop来逐行读取文件:

(->> "data.csv"
      io/resource
      io/reader
      line-seq
      (drop 1))

这里假设你的数据文件保存在资源目录中,并且第一行是可以丢弃的标题信息。

1
这是如何读取整个文件。
如果文件在资源目录中,您可以这样做:
(let [file-content-str (slurp (clojure.java.io/resource "public/myfile.txt")])

记得要求/使用clojure.java.io

0
(require '[clojure.java.io :as io])
(io/copy (io/file "/etc/passwd") \*out*\)

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接