【问题标题】:Using Clojure Tranducers to parse big files: OutOfMemory Error使用 Clojure 转换器解析大文件:内存不足错误
【发布时间】:2016-10-22 14:11:51
【问题描述】:

我想解析一个大的 json 文件 (3GB) 并为该文件中的每一行返回一个哈希映射。我的直觉是使用传感器逐行处理文件并构造一个带有一些选定字段的向量(> 文件中的 5% 字节)。

但是,以下代码会引发 OutOfMemory 异常:

文件.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。

在这种情况下传感器是否合适?有更好的选择吗?

我的要求之一是在函数末尾返回新结构。因此,我无法使用 doseq 就地处理文件。

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

谢谢!

【问题讨论】:

  • 我不完全理解你的代码。解析器的作用是什么?它似乎已通过但未使用。此外,(r) 表达式可能不是您想要的,它将 reader 称为函数。
  • 我不明白为什么传感器会有所帮助。当您要对数据执行一系列操作时,转换器很有用;转换器允许您避免创建将被丢弃的中间数据结构。这段代码只做一件事——它映射get-in。注意into 是非懒惰的。你能懒惰地处理文件吗?使用formapsequence 转换器函数,您能创建一个惰性映射条目序列吗?如果处理得当,您可以处理每一个,而无需将所有文件内容保存在内存中。
  • 解析器/转换器的目标是根据文件格式(例如 json、csv ...)和文件中的供应商格式轻松调整工作。
  • 您能否详细说明 JSON 文件中的数据,例如行数和每行的大小?或者,更好的是,将文件的代表性版本上传到某个地方,以便我们可以准确地重现问题?我在一个非常小的文件上尝试了您的代码并且效果很好,但我希望它会中断,因为从 3G 文件中获取 25G 的内存使用表明某种无限循环或其他东西。
  • @Mars 是的,在这种特殊情况下,xform 并没有做太多。但是,对于不同的文件,您可能希望应用一些过滤以及一些获取操作,在这种情况下加载! fn 接受 xform 绝对有用。至于懒惰地处理文件,据我所知 should 是因为 line-seq 是懒惰的,map 也是如此,但 OOM 错误显然表明某处出了问题。当然into 是非懒惰的,但load-with! 必须返回一些非懒惰的东西,我认为关键是提取的数据应该适合内存。

标签: clojure transducer


【解决方案1】:

你已经很接近了。我不知道json/parse-string 做了什么,但如果它与here 中的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)

【讨论】:

  • 谢谢你的提议,这个里面有必要用原子吗
  • 感谢您的建议。有必要有一个全局变量吗?与 (into ...) 的解决方案相比有什么区别?
  • 如果你有足够的内存,第一段代码就可以工作。我认为原子是doseq所必需的。我没时间研究这个,所以我的回答只是一个小小的改进。
  • 如果有人评论为什么初始代码没有像预期的那样在常量内存中真正进行流处理(以及为什么建议的代码是),那就太好了。
猜你喜欢
  • 2016-03-23
  • 2011-12-25
  • 2016-08-19
  • 1970-01-01
  • 2021-04-23
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多