Clojure - 用低内存处理大文件

6

我正在处理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的本意完全相反。
  • 监视内存使用情况会发现,尽管看起来是懒惰的,程序会建立使用与保持文件在内存中所需的RAM一样多的RAM。
那么,有没有更有效和习惯的方法来处理我的数据库呢?
一个想法可能是使用多方法来处理头部和数据,具体取决于head?谓词的值,但我认为这将有一些严重的速度影响,特别是当谓词结果从始终为真变为始终为假时只有一个出现的情况。我还没有对此进行基准测试。
使用另一种方式构建line-seq并使用iterate解析它是否更好?我猜这仍然需要我使用:while:drop-while
在我的研究中,多次提到了使用NIO文件访问,这应该可以提高内存使用率。我还没有找到如何在Clojure中以习惯的方式使用它的方法。
也许我对文件应该如何处理的总体概念仍然不太清楚?
像往常一样,任何帮助、想法或指向教程的指针都将不胜感激。
2个回答

2

您应该使用标准库函数。

line-seq、with-open和doseq可以轻松完成工作。

可以采用以下方式:

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

感谢您的建议。我使用的lazy-file方法是在我开始学习Clojure时实现的,存储在一个io模块中并从那里使用。它的净效果与仅使用line-seq完全相同。 - waechtertroll
另外一个需要注意的信息是,按行使用if-else方法的速度明显较慢(因素为1.5),比我采用的方式要慢得多。这里的运行时间以小时计算,所以这种差异是显著的。;-) - waechtertroll
我理解你关于“lazy-file”的论点,但是处理文件的打开和关闭会使得这个函数更难进行单元测试。 - kawas44
1
内存问题在于你将惰性序列的头部保存在了let绑定中。根据seq文档,当你处理这些行时,它们会被保存在内存中。 - kawas44
关于 if,如果由于文件大小而导致成本过高,您打开文件两次的方法绝对是有效的。 - kawas44

0

这里有几个需要考虑的事情:

  1. 内存使用

    有报告称,Leiningen可能会添加一些东西,导致保留对头的引用,尽管doseq明确不会保留正在处理的序列的头部,参见this SO question。请尝试在不使用lein repl的情况下验证您的声明“使用与将文件保存在内存中所需的RAM一样多的RAM”。

  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方法基本上只是将单个读取的行与单个处理调用对齐。您当前解决方案中需要惰性的原因是因为您将读取(整个文件,逐行)与处理分开。如果您将一行的处理移动到读取中,则不需要懒惰。


谢谢你的回答。昨天我编写了一些测试用例来进行基准测试。结果表明:A) 并不是读取本身消耗了那么多内存,似乎是数据库(顺便说一下,我的内存消耗声明来自于运行编译后的应用程序)B) lazy-fileline-seq 在速度和内存使用方面表现大致相等C) 令人惊讶的是,多方法和循环递归方法需要约150%的时间才能打开文件两次并使用while/drop-while。 - waechtertroll
我喜欢你在读取文件时使用递归的方式。下一个想法是,我将让头部解析器检查下一行是否为数据行(迭代器风格),如果是,则跳转到数据解析器。每行都进行if-else判断会非常慢,但是文件已经定义好了几百个头部行和数亿个数据行,并且读取头部不到半秒钟。我还不确定如何结合跳板和迭代器... - waechtertroll

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接