Clojure - 解析小型 CSV 文件的内存使用情况

ale*_*159 5 memory csv garbage-collection clojure

我正在尝试解析一个 50MB 的 CSV 文件。~2500 行,~5500 列,一列是字符串(日期为 yyyy-mm-dd),其余是带有很多空点的浮点数。我需要能够访问所有数据,所以想要实现完整的文件,这在那个大小下应该是可能的。

我尝试了以下几个选项:

(with-open [rdr (io/reader path)] (doall (csv/read-csv rdr))))

手动使用line-seq字符串并将其解析为数字的稍微手动的方法。

我在单个 JVM 上的使用量slurp增加了 100MB,是文件大小的 2 倍。在解析数据时,我会根据它的完成方式增加 1-2GB。如果我多次打开文件并将其解析为同一个变量,内存使用量会不断增加,最终会出现内存错误并且程序失败。(我知道查看任务管理器并不是查看内存泄漏的最佳方法,但事实是程序失败了所以某处存在泄漏)

打开文件的正确方法是什么?我的最后一个用例是我每天都会获取一个新文件,我希望服务器应用程序每天打开文件并处理数据,而不会耗尽内存并需要重新启动服务器。

编辑:为了比较,使用 Python pandas 读取该文件将消耗大约 100MB 的内存,并且随后重新读取该文件不会继续增加内存使用量。

Edit2:这是一个使用本地原子尝试查看发生了什么的最小示例:

(defn parse-number [s] (if (= s "") nil (read-string s)))

(defn parse-line [line]
  (let [result (atom [])]
    (doseq [x (clojure.string/split line #",")]
      (swap! result conj (parse-number x)))
    @result))

(defn line-by-line-parser [file]
  (let [result (atom [])]
    (with-open [rdr (clojure.java.io/reader file)]
      (doseq [line (line-seq rdr)]
        (swap! result conj (parse-line line)))
      @result)))

;in the repl:
(def x (line-by-line-parser "C:\\temp\\history.csv")) ; memory goes up 1GB
(def x (line-by-line-parser "C:\\temp\\history.csv")) ; memory goes up an extra 1GB
; etc
Run Code Online (Sandbox Code Playgroud)

非常感谢!

Jos*_*hua 0

正如我在评论中提到的,看到你如何使用原子,有两件事让我困扰:

  1. 您使用它们的方式看起来非常命令式。Clojure 是一种函数式编程语言,使用它们的方式并不是很惯用。
  2. 我的猜测是,每次swap调用都会生成一堆垃圾对象,这些垃圾对象可以解释内存使用情况。

第一和第二可以使用into或来解决reduce。这样做的好处是,生成的代码会更短。

我更熟悉reduce,所以这里是一个使用它的例子:

(defn parse-number [s] (if (= s "") nil (read-string s)))

(defn parse-line [line]
  (reduce #(conj %1 (parse-number %2))
          []
          (clojure.string/split line #",")))

(defn line-by-line-parser [file]
  (with-open [rdr (clojure.java.io/reader file)]
    (reduce #(conj %1 (parse-line %2))
            []
            (line-seq rdr))))
Run Code Online (Sandbox Code Playgroud)

由于我没有你的测试数据,我只能猜测它可以解决你的问题。因此,如果您对其进行测试并报告是否有任何改进,我将很高兴。