什么是将非常大的文件(比如一行一个有100 000个名称的文本文件)懒加载成列表(按需加载)的最佳方法,使用Clojure语言?
基本上,我需要对这些项目进行各种字符串搜索(现在我使用shell脚本中的grep和reg ex来实现)。
我尝试在开头添加“'”,在结尾添加“)”但显然这种方法(加载静态/常量列表)由于某些原因具有大小限制。
根据你的具体需求,有多种方法可以做到这一点。
如果你想对文件中的每一行应用一个 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
展示在需要之前文件并没有被打开。安德鲁的解决方案对我很有效,但嵌套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>
你需要使用line-seq
。以下是来自ClojureDocs的示例:
;; Count lines of a file (loses head):
user=> (with-open [rdr (clojure.java.io/reader "/etc/passwd")]
(count (line-seq rdr)))
使用懒惰的字符串列表时,您无法有效地执行需要整个列表存在的操作,例如排序。如果您可以将操作实现为 filter
或 map
,则可以延迟消耗列表。否则最好使用嵌入式数据库。
此外,请注意不要保留列表头,否则整个列表将被加载到内存中。
另外,如果需要执行多个操作,则需要一遍又一遍地读取文件。请注意,在某些情况下,懒惰可能会让事情变得更加困难。
(def names (with-open [rdr (clojure.java.io/reader "/path/to/names/file")] (line-seq rdr)))
- Abhinav Sarkar(def rdr (clojure.java.io/reader "/path/to/names/file"))
然后 2:(def names (line-seq rdr))
然后 3:(. rdr close)
。最后,你现在可以像这样操作你的"names":(count names)
- Rollo Tomazzirdr
之前没有意识到names
,它也不会起作用(问题与@AbhinavSarkar的建议指出的完全相同:line-seq
只读取第一个元素,其余是惰性的,因此关闭rdr
将不允许您读取names
的第一个元素之外的内容,所以(count names)
可能会引发异常)。你需要在2和3之间添加一个新步骤,例如(dorun names)
来实现集合。但是,这等效于(def names (with-open [rdr ...] (doall (line-seq rdr))))
,就像@andrew的答案一样,这更好。 - Bruno Reis(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))))
如果你使用Clojure处理大型文件,你可能会发现iota库非常有用。当我对大量输入应用reducers时,我经常使用iota sequences,iota/vec通过索引提供了对大于内存的文件的随机访问。
loop
和recur
会更好。 - Nelo Mitranimloop
/recur
不是惰性的。 - JohnJ