【问题标题】:Huge file in Clojure and Java heap space errorClojure 中的巨大文件和 Java 堆空间错误
【发布时间】:2012-04-16 13:25:21
【问题描述】:

我之前在huge XML file 上发布过——它是一个 287GB 的 XML,带有我想放入 CSV 文件(修订作者和时间戳)的维基百科转储。我设法做到了这一点。在我得到 StackOverflow 错误之前,但现在在解决第一个问题之后我得到:java.lang.OutOfMemoryError: Java heap space error。

我的代码(部分取自 Justin Kramer 的回答)如下所示:

(defn process-pages
  [page]
  (let [title     (article-title page)
        revisions (filter #(= :revision (:tag %)) (:content page))]
    (for [revision revisions]
      (let [user (revision-user revision)
            time (revision-timestamp revision)]
        (spit "files/data.csv"
              (str "\"" time "\";\"" user "\";\"" title "\"\n" )
              :append true)))))

(defn open-file
[file-name]
(let [rdr (BufferedReader. (FileReader. file-name))]
  (->> (:content (data.xml/parse rdr :coalescing false))
       (filter #(= :page (:tag %)))
       (map process-pages))))

我不显示article-titlerevision-userrevision-title 函数,因为它们只是从页面或修订哈希中的特定位置获取数据。任何人都可以帮我解决这个问题——我是 Clojure 的新手,没有遇到问题。

【问题讨论】:

    标签: clojure heap-memory inputstream


    【解决方案1】:

    要明确一点,(:content (data.xml/parse rdr :coalescing false)) 是懒惰的。如果您不相信,请检查其类或拉出第一项(它会立即返回)。

    也就是说,在处理大型序列时需要注意几件事:抓住头部,以及未实现/嵌套的惰性。我认为您的代码受到后者的影响。

    以下是我的建议:

    1) 将(dorun) 添加到->> 调用链的末尾。这将迫使序列完全实现,而不用抓住头。

    2) 将 process-page 中的 for 更改为 doseq。您正在向文件吐口水,这是一个副作用,您不想在这里懒惰地这样做。

    正如 Arthur 建议的那样,您可能希望打开一个输出文件并继续写入,而不是为每个 Wikipedia 条目打开和写入(吐出)。

    更新

    这是一个尝试更清楚地分离关注点的重写:

    (defn filter-tag [tag xml]
      (filter #(= tag (:tag %)) xml))
    
    ;; lazy
    (defn revision-seq [xml]
      (for [page (filter-tag :page (:content xml))
            :let [title (article-title page)]
            revision (filter-tag :revision (:content page))
            :let [user (revision-user revision)
                  time (revision-timestamp revision)]]
        [time user title]))
    
    ;; eager
    (defn transform [in out]
      (with-open [r (io/input-stream in)
                  w (io/writer out)]
        (binding [*out* out]
          (let [xml (data.xml/parse r :coalescing false)]
            (doseq [[time user title] (revision-seq xml)]
              (println (str "\"" time "\";\"" user "\";\"" title "\"\n")))))))
    
    (transform "dump.xml" "data.csv")
    

    我在这里看不到任何会导致过度使用内存的东西。

    【讨论】:

    • 对于 Clojure 新手来说,关于 dorun 的观点可能会更清楚一点:问题中显示的打开文件函数返回对进程页面的调用结果序列,以及函数何时返回从 repl 调用,打印序列会导致所有结果同时保存在内存中。对结果调用 dorun 会导致对序列中的元素进行求值并返回 nil,因此无需将所有结果同时保存在内存中。
    • 感谢您的解释!我现在确实(希望)理解了这个代码 sn-p 中的懒惰是如何工作的,并改变了你提出的建议,但仍然是OutOfMemoryError: Java heap space。我正在处理最终文件的 1GB 样本,但它仍然会引发内存错误。非常感谢您的帮助。
    • 查看我的最新更新。如果您仍然收到 OutOfMemory 错误,我不知道为什么。我使用的代码与此非常相似,没有内存问题。
    • 故障排除思路:是否总是在同一项目上耗尽内存?该项目是否与众不同(例如,真的很大,有很多修订)?您是否尝试过为 JVM 提供更多内存?您确定您没有在任何地方保留任何子字符串(JVM 不会 GC 字符串仍在使用中的子字符串)?
    • 基本上 - 非常感谢您提供的所有帮助。 A 花了更多时间使用它,就 JVM 调整而言,这对我来说太复杂了,而且我尝试使用一些内存选项,我得到更多花哨的错误。在我能够正确处理这个问题之前,我可能会花更多时间在 Clojure 和 JVM 上。
    【解决方案2】:

    不幸的是data.xml/parse 并不懒惰,它会尝试将整个文件读入内存然后解析它。

    改为使用 this (lazy) xml library,它只保存它当前在ram 中处理的部分。然后,您需要重新构建代码以在读取输入时写入输出,而不是收集所有 xml,然后输出。

    你的线

    (:content (data.xml/parse rdr :coalescing false)
    

    会将所有 xml 加载到内存中,然后从中请求内容密钥。这会炸毁堆。

    懒惰答案的大致轮廓如下所示:

    (with-open [input (java.io.FileInputStream. "/tmp/foo.xml")
                output (java.io.FileInputStream. "/tmp/foo.csv"]
        (map #(write-to-file output %)
            (filter is-the-tag-i-want? (parse input))))
    

    要有耐心,与(> data ram) 合作总是需要时间:)

    【讨论】:

    • 他已经在使用来自 contrib 的 data.xml,正如你所指出的,它是懒惰的。
    【解决方案3】:

    我不了解 Clojure,但在纯 Java 中,可以使用基于 SAX 事件的解析器,例如 http://docs.oracle.com/javase/1.4.2/docs/api/org/xml/sax/XMLReader.html 不需要将 XML 加载到 RAM 中

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2013-06-28
      • 1970-01-01
      • 1970-01-01
      • 2023-04-07
      • 1970-01-01
      • 2021-07-18
      相关资源
      最近更新 更多