Clojure-使用低内存处理大型文件

Clojure-使用低内存处理大型文件,clojure,memory-efficient,file-processing,Clojure,Memory Efficient,File Processing,我正在处理60GB或更大的文本文件。这些文件分为可变长度的标题部分和数据部分。我有三个职能: head?区分头行和数据行的谓词 处理标题处理一个标题行字符串 处理数据处理一个数据行字符串 处理函数异步访问和修改内存中的数据库 我改进了从另一个SO线程读取文件的方法,它应该构建一个延迟的行序列。其思想是使用一个函数处理一些行,然后切换一次函数,并使用下一个函数继续处理 (defn lazy-file [file-name] (letfn [(helper [rdr]

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

  • head?
    区分头行和数据行的谓词
  • 处理标题
    处理一个标题行字符串
  • 处理数据
    处理一个数据行字符串
  • 处理函数异步访问和修改内存中的数据库
我改进了从另一个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)))
尽管这样做有效,但由于以下几个原因,它的效率相当低:

  • 我必须过滤标题行并对其进行处理,然后重新解析整个文件并删除所有标题行以处理数据,而不是简单地调用
    processhead
    ,直到到达数据,然后继续执行
    processdata
    。这与
    惰性文件
    的意图正好相反
  • 通过观察内存消耗情况,我发现这个程序虽然看起来很懒,但它可以使用尽可能多的RAM来将文件保存在内存中
那么,使用我的数据库有什么更有效、更惯用的方法呢

一种想法可能是根据
head?
谓词的值使用multi方法来处理标题和数据,但我认为这会对速度产生严重影响,特别是在只有一种情况下,谓词结果总是从true变为false。我还没有做基准测试

使用另一种方法构建seq行并用
迭代
解析它是否更好?我想这仍然会让我需要使用:while和:drop-while

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

也许我还不太了解一般的想法,该如何处理该文件


一如既往,任何对TUTS的帮助、想法或指针都是非常值得赞赏的。

这里有几件事要考虑:

  • 内存使用

    有报道称,leiningen可能会添加一些东西,从而保留对头部的引用,尽管它不会保留正在处理的序列的头部,参见。在不使用
    lein repl
    的情况下,尝试验证您的声明“使用尽可能多的RAM将文件保存在内存中”

  • 解析行

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

    这里还有另一个选项,就是将处理函数合并到文件读取函数中。因此,您可以立即处理它,而不只是
    cons
    ing一个新行并返回它,通常您可以将处理函数作为参数而不是硬编码它

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


  • 您应该使用标准库函数

    行seq,带有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模块中并从那里开始使用。它的净效果确实与仅使用
    行序列
    相同。另一方面,每行的if-else方法被证明比我所采用的方法慢得多(系数1.5)。重要的是,这里的运行时间以小时为单位;-)我理解你关于
    惰性文件的论点,但是打开和关闭文件会使这个函数更难进行单元测试。内存的问题是你在
    let
    绑定中持有惰性seq的头。当您根据
    seq
    文档处理这些行时,它们会保存在内存中。关于
    if
    ,如果由于文件大小而导致成本高昂,那么您打开文件两次的方法肯定是有效的。谢谢您的回答。昨天我写了一些测试用例来做基准测试。结果表明,A)消耗这么多内存的不是读取本身,而是数据库(顺便说一句,我的内存消耗声称源于运行编译后的应用程序)B)
    惰性文件
    行序列
    的性能大致相同,考虑到速度和内存使用情况,令人惊讶的是,多方法和循环递归方法将需要大约150%的时间来打开文件两次并使用while/drop,而您在读取文件时喜欢使用递归方式。我将尝试的下一个想法是,我将让头解析器检查下一行是否是数据行(迭代器样式),如果是,则跳到数据解析器。If-else在每一行上都非常慢,但是文件被很好地定义为几百个标题行和数亿个数据行,读取标题只需要不到半秒钟的时间。我只是还不确定,如何结合蹦床和迭代器。。。
    (with-open [rdr (clojure.java.io/reader file-path)]
      (doseq [line (line-seq rdr)]
        (if (head? line)
          (process-header line)
          (process-data line))))