【问题标题】:Clojure - process huge files with low memoryClojure - 处理内存不足的大文件
【发布时间】:2016-03-23 15:21:35
【问题描述】:

我正在处理 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 来将文件保存在内存中。

那么使用我的数据库更有效、更惯用的方法是什么?

一个想法可能是使用多方法来处理取决于 head? 谓词的值的标头和数据,但我认为这会对速度产生一些严重影响,特别是因为只有一次发生谓词结果从总是真到总是假。我还没有对它进行基准测试。

使用另一种方式来构建 line-seq 并使用iterate 解析它会更好吗?我猜这仍然让我需要使用 :while 和 :drop-while。

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

也许我对大体的想法还没有把握,应该如何处理文件?

与往常一样,非常感谢任何帮助、想法或对 tuts 的指导。

【问题讨论】:

    标签: clojure memory-efficient file-processing


    【解决方案1】:

    您应该使用标准库函数。

    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 完全相同。
    • 另一个附带信息,每行的 if-else 方法被证明比我采用的方法慢得多(因子 1.5)。重要的是因为这里的运行时间以小时为单位;-)
    • 我理解你关于lazy-file 的论点,但是处理打开和关闭文件会使这个函数更难进行单元测试。
    • 内存的问题是你在let绑定中持有惰性序列的头部。当您根据seq 文档处理这些行时,它们会保存在内存中。
    • 关于if,如果因为文件大小而成本高昂,那么你打开文件两次的方法绝对是有效的。
    【解决方案2】:

    这里有几件事需要考虑:

    1. 内存使用

      有报道称 leiningen 可能会添加导致保留对头部的引用的内容,尽管 doseq 特别不保留它正在处理的序列的头部,参见。 this SO question。尝试验证您的声明“使用尽可能多的 RAM 将文件保存在内存中”而不使用 lein repl

    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 的性能大致相当并使用 while/drop-while
    • 我喜欢你在阅读文件时的递归方式。我将尝试的下一个想法是,我将让标头解析器检查下一行是否是数据行(迭代器样式),如果是,则跳到数据解析器。每一行的if-else确实很慢,但是文件被很好地定义为几百个标题行和数亿个数据行,读取头部不到半秒。我只是还不确定,如何结合蹦床和迭代器......
    猜你喜欢
    • 2021-08-02
    • 2016-07-29
    • 2012-08-17
    • 2013-02-12
    • 2012-06-12
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多