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

Ali*_*Ali 26 text file clojure

在clojure中将一个非常大的文件(比如每行一个有10万个名字的文本文件)读入一个列表(懒惰地 - 根据需要加载它)的最佳方法是什么?

基本上我需要对这些项进行各种字符串搜索(我现在用shell脚本中的grep和reg ex做).

我尝试在结尾添加'(在开头和结尾),但显然这个方法(加载一个静态?/常量列表,由于某种原因有一个大小限制.

and*_*oke 29

有多种方法可以做到这一点,具体取决于你想要什么.

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

(with-open [rdr ...]
  (doall (map function (line-seq rdr))))
Run Code Online (Sandbox Code Playgroud)

这样做的好处是尽可能快地打开,处理和关闭文件,但强制立即使用整个文件.

如果你想延迟处理文件,你可能会想要返回行,但这不起作用:

(map function ; broken!!!
    (with-open [rdr ...]
        (line-seq rdr)))
Run Code Online (Sandbox Code Playgroud)

因为文件在with-open返回时关闭,这是您懒惰地处理文件之前.

解决此问题的一种方法是将整个文件拉入内存slurp:

(map function (slurp filename))
Run Code Online (Sandbox Code Playgroud)

这有一个明显的缺点 - 内存使用 - 但保证你不要让文件保持打开状态.

另一种方法是让文件保持打开状态,直到读到结束,同时生成一个惰性序列:

(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)
Run Code Online (Sandbox Code Playgroud)

哪个印刷品:

caught  #<RuntimeException java.lang.RuntimeException: java.io.IOException: Stream closed>
have stream
opening
closed
read 29 lines
Run Code Online (Sandbox Code Playgroud)

显示文件在需要之前甚至没有打开.

最后一种方法的优点是,您可以"在其他地方"处理数据流,而无需将所有内容保存在内存中,但它也有一个重要的缺点 - 文件在读取流结束之前不会关闭.如果你不小心,你可以并行打开许多文件,甚至忘记关闭它们(通过不完全读取流).

最佳选择取决于具体情况 - 这是懒惰评估与有限系统资源之间的权衡.

PS:是lazy-open在库中的某个地方定义的?我到达这个问题试图找到这样一个功能,并最终编写我自己的,如上所述.


Joh*_*hnJ 21

安德鲁的解决方案对我来说效果很好,但嵌套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>
Run Code Online (Sandbox Code Playgroud)


Abh*_*kar 20

你需要使用line-seq.clojuredocs的一个例子:

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

但是对于一个懒惰的字符串列表,你不能有效地进行那些需要整个列表存在的操作,比如排序.如果你能实现你的操作为filtermap那么你可以懒洋洋地消耗名单.否则,最好使用嵌入式数据库.

另请注意,您不应该抓住列表的头部,否则整个列表将被加载到内存中.

此外,如果您需要执行多个操作,则需要一次又一次地读取该文件.请注意,懒惰有时会让事情变得困难.

  • 好吧,我不这么认为.因为您已将"line-seq"包围为"with-open",所以底层流将在返回时自动关闭.所以你的"名字"变量没有留下任何东西.所以基本上你必须要1:`(def rdr(clojure.java.io/reader"/ path/to/names/file"))`然后2:`(def name(line-seq rdr))``3 :`(.rdr close)`.最后,你现在可以玩你的"名字",如:`(count names)` (7认同)
  • 在这种情况下,只需保持对惰性列表的头部的引用.它会在第一次懒洋洋地加载然后保持加载状态.类似于:`(def name(with-open [rdr(clojure.java.io/reader"/ path/to/names/file")](line-seq rdr)))` (4认同)
  • @RolloTomazzi,如果你在关闭`rdr`之前没有意识到`names`,它也不会工作(问题与你指出@ AbhinavSarkar的建议完全相同:`line-seq`只读取第一个元素, rest是懒惰的,所以关闭`rdr`将不允许你读取`names`的第一个元素,所以`(count names)`可能会抛出异常).你必须在2到3之间添加一个新步骤,以实现集合,如`(dorun names)`.但是,这相当于`(def name(with-open [rdr ...](doall(line-seq rdr))))`,就像@ andrew的回答一样,这样做更好. (2认同)