在Clojure中将一个非常大的文本文件读入列表中

29
什么是将非常大的文件(比如一行一个有100 000个名称的文本文件)懒加载成列表(按需加载)的最佳方法,使用Clojure语言? 基本上,我需要对这些项目进行各种字符串搜索(现在我使用shell脚本中的grep和reg ex来实现)。 我尝试在开头添加“'”,在结尾添加“)”但显然这种方法(加载静态/常量列表)由于某些原因具有大小限制。
5个回答

32

根据你的具体需求,有多种方法可以做到这一点。

如果你想对文件中的每一行应用一个 function,你可以使用类似于 Abhinav 回答中的代码:

(with-open [rdr ...]
  (doall (map function (line-seq rdr))))

这种方法的优点是文件被尽快打开、处理和关闭,但会强制整个文件一次性读取。

如果你想延迟处理文件,你可能会尝试返回每一行,但这样做不会起作用

(map function ; broken!!!
    (with-open [rdr ...]
        (line-seq rdr)))

因为with-open返回时文件已关闭,在惰性处理文件之前关闭。

一个解决方法是使用slurp将整个文件读入内存:

(map function (slurp filename))

这种方式显然存在一个缺点——内存使用,但可以保证不会遗漏打开的文件。

另一种选择是在读取到结尾之前保持文件处于打开状态,同时生成一个惰性序列:

(ns ...
  (:use clojure.test))

(defn stream-consumer [stream]
  (println "read" (count stream) "lines"))

(defn broken-open [file]
  (with-open [rdr (clojure.java.io/reader file)]
    (line-seq rdr)))

(defn lazy-open [file]
  (defn helper [rdr]
    (lazy-seq
      (if-let [line (.readLine rdr)]
        (cons line (helper rdr))
        (do (.close rdr) (println "closed") nil))))
  (lazy-seq
    (do (println "opening")
      (helper (clojure.java.io/reader file)))))

(deftest test-open
  (try
    (stream-consumer (broken-open "/etc/passwd"))
    (catch RuntimeException e
      (println "caught " e)))
  (let [stream (lazy-open "/etc/passwd")]
    (println "have stream")
    (stream-consumer stream)))

(run-tests)

输出:

caught  #<RuntimeException java.lang.RuntimeException: java.io.IOException: Stream closed>
have stream
opening
closed
read 29 lines
展示在需要之前文件并没有被打开。
这种方法的优点是你可以在不将所有内容存储在内存中的情况下在"其他地方"处理数据流,但它也有一个重要的缺点 - 直到流的末尾才关闭文件。 如果你不小心,可能会并行打开许多文件,甚至忘记关闭它们(未完全读取流)。
最佳选择取决于具体情况 - 这是一种惰性求值和有限系统资源之间的权衡。
附注:是否在库中定义了“lazy-open”?我试图找到这样一个函数而进入了这个问题,并最终像上面那样编写了自己的函数。

24

安德鲁的解决方案对我很有效,但嵌套defn不太符合习惯用法,而且你不需要两次使用lazy-seq:这里是更新版本,没有额外的打印,并使用letfn

(defn lazy-file-lines [file]
  (letfn [(helper [rdr]
                  (lazy-seq
                    (if-let [line (.readLine rdr)]
                      (cons line (helper rdr))
                      (do (.close rdr) nil))))]
         (helper (clojure.java.io/reader file))))

(count (lazy-file-lines "/tmp/massive-file.txt"))
;=> <a large integer>

这段代码使用looprecur会更好。 - Nelo Mitranim
@NeloMitranim loop / recur 不是惰性的。 - JohnJ
抱歉,我的错。我没有完全听懂。 - Nelo Mitranim

21

你需要使用line-seq。以下是来自ClojureDocs的示例:

;; Count lines of a file (loses head):
user=> (with-open [rdr (clojure.java.io/reader "/etc/passwd")]
         (count (line-seq rdr)))

使用懒惰的字符串列表时,您无法有效地执行需要整个列表存在的操作,例如排序。如果您可以将操作实现为 filtermap ,则可以延迟消耗列表。否则最好使用嵌入式数据库。

此外,请注意不要保留列表头,否则整个列表将被加载到内存中。

另外,如果需要执行多个操作,则需要一遍又一遍地读取文件。请注意,在某些情况下,懒惰可能会让事情变得更加困难。


非常感谢,但是如果我想把整个列表都保存在内存中(不偷懒),那么最好的方法是什么?正如你所说,对于某些操作,我需要一遍又一遍地遍历整个列表(假设我有足够的内存来保存整个列表)。 - Ali
4
在这种情况下,只需保留对惰性列表头部的引用即可。第一次惰性加载后,它将保持已加载状态。类似这样:(def names (with-open [rdr (clojure.java.io/reader "/path/to/names/file")] (line-seq rdr))) - Abhinav Sarkar
7
我不这样认为。因为你将"line-seq"用"with-open"包围起来,当其返回时底层流会自动关闭。所以在你的"names"变量之后没有留下任何东西。因此,基本上你需要1:(def rdr (clojure.java.io/reader "/path/to/names/file")) 然后 2:(def names (line-seq rdr)) 然后 3:(. rdr close)。最后,你现在可以像这样操作你的"names":(count names) - Rollo Tomazzi
2
@RolloTomazzi,如果你在关闭rdr之前没有意识到names,它也不会起作用(问题与@AbhinavSarkar的建议指出的完全相同:line-seq只读取第一个元素,其余是惰性的,因此关闭rdr将不允许您读取names的第一个元素之外的内容,所以(count names)可能会引发异常)。你需要在2和3之间添加一个新步骤,例如(dorun names)来实现集合。但是,这等效于(def names (with-open [rdr ...] (doall (line-seq rdr)))),就像@andrew的答案一样,这更好。 - Bruno Reis

1

看看我的答案在这里

(ns user
  (:require [clojure.core.async :as async :refer :all 
:exclude [map into reduce merge partition partition-by take]]))

(defn read-dir [dir]
  (let [directory (clojure.java.io/file dir)
        files (filter #(.isFile %) (file-seq directory))
        ch (chan)]
    (go
      (doseq [file files]
        (with-open [rdr (clojure.java.io/reader file)]
          (doseq [line (line-seq rdr)]
            (>! ch line))))
      (close! ch))
    ch))

所以:
(def aa "D:\\Users\\input")
(let [ch (read-dir aa)]
  (loop []
    (when-let [line (<!! ch )]
      (println line)
      (recur))))

1

如果你使用Clojure处理大型文件,你可能会发现iota库非常有用。当我对大量输入应用reducers时,我经常使用iota sequences,iota/vec通过索引提供了对大于内存的文件的随机访问。


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