Clojure和Java中的巨大文件和堆空间错误

6
我之前在stackoverflow上发布了一篇关于大型XML文件的帖子——这是一个287GB的Wikipedia转储XML文件,我想将其转换为CSV文件(包括修订版本、作者和时间戳)。我已经成功做到了某种程度。之前我遇到了StackOverflow错误,但现在我解决了第一个问题后,出现了Java堆空间错误:java.lang.OutOfMemoryError。

我的代码(部分参考了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方面真的很新,不太明白这个问题。

3个回答

4
只是为了明确起见,(:content (data.xml/parse rdr :coalescing false))是懒惰的。如果你还不确定,检查它的类或者拉取第一个项目(它会立即返回)。
话虽如此,在处理大量序列时要注意两件事情:保留头部和未实现/嵌套的懒惰性。我认为你的代码遭受了后者。
这是我的建议:
1) 在->>调用链的末尾添加(dorun)。这将强制序列完全实现,而不保留头部。
2) 在process-page中将for更改为doseq。你正在将内容输出到文件中,这是一种副作用,你不应该在这里懒惰地完成。
正如Arthur所建议的那样,您可能希望打开一个输出文件并继续写入,而不是针对每个维基百科条目都进行打开和写入(spit)。 更新: 以下是重写的内容,试图更清晰地分别考虑问题:
(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")

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

1
对于刚接触Clojure的人来说,关于dorun的要点可能需要更清晰一些:如问题中所示的open-file函数返回调用process-pages的结果序列,当从repl调用该函数时,打印序列会导致所有结果同时保存在内存中。在结果上调用dorun会导致序列的元素被评估并返回nil,因此永远不需要同时拥有所有结果。 - Jouni K. Seppänen
谢谢解释!我现在(希望)理解了这段代码中延迟加载的工作原理,并按照你的建议进行了改变,但仍然出现了 OutOfMemoryError: Java heap space 的错误。我正在处理一个1GB大小的最终文件样本,但仍然出现内存错误。非常感谢任何帮助。 - trzewiczek
请看我的最新更新。如果仍然出现OutOfMemory错误,我不确定原因。我以前使用过非常类似的代码,没有内存问题。 - Justin Kramer
故障排除的想法:它是否总是在同一项上耗尽内存?那个项目是否不寻常(例如,非常大,有很多修订)?您是否尝试过给JVM更多的内存?您确定您没有在任何地方保留任何子字符串(JVM不会GC仍在使用的子字符串)? - Justin Kramer
基本上 - 非常感谢你的所有帮助。我花了更多时间去尝试,但是在JVM调整方面它对我来说太复杂了,而且我尝试使用一些内存选项时会得到更多的错误。可能需要再花一些时间学习Clojure和JVM,才能正确地解决这个问题。 - trzewiczek
我有一个类似的问题,涉及一个大文件(150MB),但这种技术对我不起作用。我在想这是否是数据结构深度与广度的问题?对于我的文件,存在嵌套层次(约6层),我需要从每个层次获取信息。 - bnbeckwith

1

很遗憾,data.xml/parse不是惰性的,它试图将整个文件读入内存,然后解析它。

相反,使用这个(惰性)xml库,它只在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,正如你所指出的那样,这是一种懒惰的方式。 - Justin Kramer

0

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