Clojure-用低内存处理巨大的文件



我正在处理60GB或更大的文本文件。文件分为可变长度的标题部分和数据部分。我有三个功能:

  • head?区分标题行和数据行的谓词
  • process-header处理一个标题行字符串
  • process-data处理一个数据线串
  • 处理函数异步访问和修改内存中的数据库

我从另一个SO线程中改进了一个文件读取方法,该方法应该构建一个延迟的行序列。这个想法是用一个函数处理一些行,然后切换一次函数,然后用下一个函数继续处理。

(defn lazy-file
  [file-name]
  (letfn [(helper [rdr]
            (lazy-seq
             (if-let [line (.readLine rdr)]
               (cons line (helper rdr))
               (do (.close rdr) nil))))]
    (try
      (helper (clojure.java.io/reader file-name))
      (catch Exception e
        (println "Exception while trying to open file" file-name)))))

我把它和一起使用

(let [lfile (lazy-file "my-file.txt")]
  (doseq [line lfile :while head?]
    (process-header line))
  (doseq [line (drop-while head? lfile)]
    (process-data line)))

尽管这是有效的,但由于以下几个原因,它的效率相当低:

  • 我不需要简单地调用process-head,直到我到达数据,然后继续使用process-data,而是必须过滤标题行并处理它们,然后重新开始解析整个文件,并删除所有标题行来处理数据。这与lazy-file的意图完全相反
  • 观察内存消耗告诉我,这个程序虽然看起来很懒,但它的内存消耗量与将文件保存在内存中所需的内存一样多

那么,使用数据库的更高效、更惯用的方法是什么呢?

一种想法可能是使用多方法来处理依赖于head?谓词值的标头和数据,但我认为这会对速度产生严重影响,尤其是在只有一次谓词结果从始终为true变为始终为false的情况下。我还没有对此进行基准测试。

使用另一种方法构建行seq并用iterate解析它会更好吗?我想,这仍然会让我需要使用:while和:drop while。

在我的研究中,曾多次提到使用NIO文件访问,这将提高内存使用率。我还不知道如何在clojure中以惯用的方式使用它。

也许我还是有一个不好的总体思路,应该如何对待档案?

一如既往,我们非常感谢任何帮助、想法或指导。

您应该使用标准库函数。

行seq,与开放和doseq将很容易做到这一点。

(with-open [rdr (clojure.java.io/reader file-path)]
  (doseq [line (line-seq rdr)]
    (if (head? line)
      (process-header line)
      (process-data line))))

这里需要考虑以下几点:

  1. 内存使用

    有报道称,leiningen可能会添加一些内容,从而保留对头部的引用,尽管doseq特别没有保留它正在处理的序列的头部,参见这个SO问题。尝试验证您的索赔";使用与将文件保持在存储器中所需的RAM一样多的RAM";而不使用CCD_ 9。

  2. 分析线路

    您也可以使用loop/recur方法,而不是使用doseq的两个循环。您期望解析的是第二个参数,如下所示(未经测试):

        (loop [lfile (lazy-file "my-file.txt")
               parse-header true]
           (let [line (first lfile)]
                (if [and parse-header (head? line)]
                    (do (process-header line)
                        (recur (rest lfile) true))
                    (do (process-data line)
                        (recur (rest lfile) false)))))
    

    这里还有另一个选项,就是将处理功能合并到文件读取功能中。因此,与其只是cons生成一个新行并返回它,您还可以立即处理它——通常情况下,您可以将处理函数作为参数,而不是对其进行硬编码

    您当前的代码看起来处理是一种副作用。如果是这样的话,那么如果你结合了处理,你可能会消除懒惰。无论如何,您都需要处理整个文件(或者看起来是这样),并且是按行处理的。lazy-seq方法基本上只是将单行读取与单个处理调用对齐。在当前的解决方案中,您需要懒惰,因为您将读取(整个文件,逐行)与处理分开。如果你把一行的处理转移到阅读中,你就不需要懒散地这样做。

最新更新