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

时间:2010-11-07 14:23:58

标签: file text clojure

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

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

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

5 个答案:

答案 0 :(得分:29)

根据您的需要,有多种方法可以做到这一点。

如果您要将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是否在库的某处定义了?我找到了这个问题,试图找到这样一个函数,最后编写了我自己的函数,如上所述。

答案 1 :(得分:21)

安德鲁的解决方案对我来说效果很好,但是嵌套的defn不是那么惯用,你不需要做lazy-seq两次:这是一个没有额外打印的更新版本并使用{ {1}}:

letfn

答案 2 :(得分: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)))

但是对于一个懒惰的字符串列表,你不能有效地执行那些需要整个列表存在的操作,比如排序。如果您可以将您的操作实施为filtermap,那么您可以懒惰地使用该列表。否则,最好使用嵌入式数据库。

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

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

答案 3 :(得分:1)

see my answer here

(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))))

答案 4 :(得分:0)

您可能会发现iota库对于在Clojure中处理非常大的文件很有用。在将reducer应用于大量输入时,我一直使用iota序列,并且iota / vec通过对它们进行索引来提供对大于内存的文件的随机访问。