【发布时间】:2011-05-06 07:27:15
【问题描述】:
在 clojure 中将一个非常大的文件(例如每行有 100 000 个名称的文本文件)读入列表(懒惰地 - 根据需要加载)的最佳方法是什么?
基本上我需要对这些项目进行各种字符串搜索(我现在在 shell 脚本中使用 grep 和 reg ex 进行搜索)。
我尝试在开头添加 '( 并在末尾添加 ) 但显然这种方法(加载静态?/常量列表,由于某种原因有大小限制。
【问题讨论】:
在 clojure 中将一个非常大的文件(例如每行有 100 000 个名称的文本文件)读入列表(懒惰地 - 根据需要加载)的最佳方法是什么?
基本上我需要对这些项目进行各种字符串搜索(我现在在 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
显示文件在需要时才打开。
最后一种方法的优点是您可以在“其他地方”处理数据流,而无需将所有内容都保存在内存中,但它也有一个重要的缺点 - 直到读取流的末尾才关闭文件。如果您不小心,您可能会并行打开许多文件,甚至忘记关闭它们(通过不完全读取流)。
最佳选择取决于具体情况 - 这是惰性评估和有限系统资源之间的权衡。
PS:lazy-open 是否在库中的某处定义?我遇到了这个问题,试图找到这样一个函数,最后写了我自己的,如上所述。
【讨论】:
Andrew 的解决方案对我来说效果很好,但嵌套的 defns 不是那么惯用的,你不需要做两次 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>
【讨论】:
loop 和recur 会更好。
loop / recur 并不懒惰。
您需要使用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)))
(def rdr (clojure.java.io/reader "/path/to/names/file")) 然后 2:(def names (line-seq rdr)) 然后 3:(. rdr close)。最后,您现在可以使用您的“名称”,例如:(count names)
rdr 之前没有意识到names,它也不会起作用(问题与您在@AbhinavSarkar 的建议中指出的完全相同:line-seq 读取只有第一个元素,其余的都是惰性的,因此关闭rdr 将不允许您读取names 的第一个元素,因此(count names) 可能会抛出异常)。您必须在 2 和 3 之间添加一个新步骤,以实现集合,例如 (dorun names)。但是,这相当于(def names (with-open [rdr ...] (doall (line-seq rdr)))),就像@andrew 的答案一样,这要好得多。
(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))))
【讨论】:
您可能会发现 iota 库对于在 Clojure 中处理非常大的文件很有用。当我将 reducer 应用于大量输入时,我一直使用 iota 序列,并且 iota/vec 通过对大于内存的文件进行索引来提供对大于内存的文件的随机访问。
【讨论】: