我正在尝试解析一个 50MB 的 CSV 文件。 ~2500 行,~5500 列,一列是字符串(日期为 yyyy-mm-dd),其余是带有很多空点的浮点数。我需要能够访问所有数据,所以想要实现完整的文件,这在那个大小下应该是可能的。
我尝试了以下几个选项:(with-open [rdr (io/reader path)] (doall (csv/read-csv rdr))))
使用 line-seq
稍微手动一点的方法并手动将字符串解析为数字。
我在单个 slurp
上的 JVM 使用情况增加 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
非常感谢!
最佳答案
只要您不将解析的数据保留在任何 GC 根目录下(如 def
或 memoize
函数),上面显示的代码就不会泄漏。您可以通过将代码循环 100 次并查看是否出现 OOM(我不指望任何)来轻松证明这一点。话虽如此,您可以按照其他人的建议采取一些措施来缓解内存压力。
如果您想确切地知道内存在哪里,请选择类似 this 的分析器。深入了解它。
我对你的情况的预感只是 GC 压力(不是泄漏)。具体使用read-string
,远不止conj
/atom
.尝试更换 read-string
使用更低级别的内容(例如 Integer/parse
),您应该会看到很大的不同。 conj
另一方面,从持久数据结构的角度来看(Python 不使用)是非常高效的,但当然它永远不会击败原始数组(Python 使用)。 atom
通常用于并发。在您的情况下,它可以替换为 transient
(和 persistent!
)但我不认为它会产生很大的不同。
更新 - 添加分配火焰图
read-string
运行时使用了 70% 的内存分配 关于csv - Clojure - 解析小型 CSV 文件的内存使用情况,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/59250104/