使用Clojure Tranducers解析大文件:OutOfMemory错误

6
我希望能够解析一个大的json文件(3GB),并为文件中的每一行返回一个哈希映射。我的想法是使用转换器逐行处理文件,并构造一个包含某些选定字段的向量(文件中的> 5%字节)。然而,以下代码抛出了OutOfMemory异常:

file.json

{"experiments": {"results": ...}}
{"experiments": {"results": ...}}
{"experiments": {"results": ...}}

parser.clj

(defn load-with!
  "Load a file using a parser, a structure and a transducer."
  [parser structure xform path]
  (with-open [r (clojure.java.io/reader path)]
    (into structure xform (parser r))))

(def xf (map #(get-in % ["experiments" "results"])))
(def parser (comp (partial map cheshire.core/parse-string) line-seq))

(load-with! parser (vector) xf "file.json")

当我使用JVisualVM可视化工具观察这个过程时,堆内存会随着时间增长并在进程崩溃前超过25 GB。

在这种情况下,转换器(transducers)是否合适?有没有更好的替代方案?

我的一个要求是在函数结束时返回新结构。因此,我不能使用doseq来就地处理文件。

此外,我需要根据文件格式更改解析器和转换器。

谢谢!


我不完全理解你的代码。解析器的作用是什么?它似乎被传递了但未使用。另外,表达式(r)可能不是你想要的,它将读取器作为函数调用。 - Michiel Borkent
2
我不明白为什么转换器会有帮助。当您需要对数据执行一系列操作时,转换器非常有用;转换器允许您避免创建将被丢弃的中间数据结构。此代码只执行一个操作——映射 get-in。请注意,into 是非惰性的。您能否懒处理文件?使用 formapsequence 转换器函数,您能否创建一个映射条目的惰性序列?如果正确处理它们,您可以在不将所有文件内容保存在内存中的情况下处理每个条目。 - Mars
解析器/转换器的目标是根据文件格式(例如json、csv等)和文件中的供应商格式轻松适应工作。 - user5239066
你能否提供一些关于JSON文件中数据的具体信息,例如行数和每行的大小?或者更好的方法是,在某个地方上传一个代表性的文件版本,以便我们可以精确地重现问题?我在一个非常小的文件上尝试了你的代码,它运行得很好,但我本来期望它会崩溃,因为从一个3G的文件中获取25G的内存使用量似乎暗示着某种无限循环或其他问题。 - Robert Johnson
@Mars 是的,在这种特定情况下,xform 没有做太多事情。但是对于另一个文件,您可能希望应用一些过滤以及一些 get-in 操作,在这种情况下,使 load-with! 函数接受 xform 绝对是有用的。至于懒处理文件,据我所知,这应该是正确的,因为 line-seq 是懒处理的,map 也是如此,但是 OOM 错误显然表明某些地方出了问题。当然,into 不是懒处理的,但是 load-with! 必须返回一些非懒处理的东西,我认为重点是提取的数据预计适合内存。 - Robert Johnson
@RobertJohnson,感谢您的详细解释。您所写的一切都很有道理。Freaxmind,我同意传输器旨在为许多提供类似数据流的东西提供灵活的接口,但是许多方法也可以做到这一点。正如其他评论所指出的那样,我认为传输器更具体。 - Mars
1个回答

1
你已经很接近了。我不知道json/parse-string是什么,但如果它和这里json/read-str相同,那么这段代码应该是你想要实现的内容。
看起来你想要做类似于这样的事情:
(require '[clojure.data.json :as json])
(require '[clojure.java.io :as java])

(defn load-with!
  "Load a file using a parser, a structure and a transducer."
  [parser structure xform path]
  (with-open [r (java/reader path)]
    (into structure (xform (parser r)))))

(def xf (partial map #(get-in % ["experiments" "results"])))

(def parser (comp (partial map json/read-str) line-seq))


(load-with! parser [] xf "file.json")

我猜这些只是在将所有业务细节剪切到您的最小示例中时出现的错误。使用下面的代码,我能够处理一个大文件,而上面的代码给了我一个OOM错误:
(require '[clojure.data.json :as json])
(require '[clojure.java.io :as java])

(def structure (atom []))

(defn do-it! [xform path]
  (with-open [r (java/reader path)]
    (doseq [line (line-seq r)]
      (swap! structure conj (xform line)))))

(defn xf [line]
  (-> (json/read-str line)
      (get-in ["experiments" "results"])))

(do-it! xf "file.json")

(take 10 @structure)

感谢您的提议。在这里使用原子是必要的吗? - user5239066
感谢您的提议。是否有必要使用全局变量?与(into…)解决方案相比有什么区别? - user5239066
如果你有足够的内存,第一段代码将会运行。我认为在doseq中使用atom是必要的。我没有时间去研究这个问题,所以我的答案只是一个小的改进。 - Brandon Henry
希望有人评论一下为什么最初的代码不像预期的那样在常量内存中进行流处理(以及为什么建议使用的代码可以)。 - matanster

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