使用data.zip在Clojure中解析XML时的OutOfMemoryError

时间:2013-04-29 23:40:57

标签: xml clojure out-of-memory

我想使用Clojure从Wiktionary XML转储中提取标题。

我使用head -n10000 > out-10000.xml创建原始怪物文件的较小版本。然后我用文本编辑器修剪它以使其成为有效的XML。我根据(wc -l)中的行数重命名了文件:

(def data-9764 "data/wiktionary-en-9764.xml") ; 354K
(def data-99224 "data/wiktionary-en-99224.xml") ; 4.1M
(def data-995066 "data/wiktionary-en-995066.xml") ; 34M
(def data-7999931 "data/wiktionary-en-7999931.xml") ; 222M

以下是XML结构的概述:

<mediawiki>
  <page>
    <title>dictionary</title>
    <revision>
      <id>20100608</id>
      <parentid>20056528</parentid>
      <timestamp>2013-04-06T01:14:29Z</timestamp>
      <text xml:space="preserve">
        ...
      </text>
    </revision>
  </page>
</mediawiki>

以下是我尝试过的内容,基于this answer to 'Clojure XML Parsing'

(ns example.core
  (:use [clojure.data.zip.xml :only (attr text xml->)])
  (:require [clojure.xml :as xml]
            [clojure.zip :as zip]))

(defn titles
  "Extract titles from +filename+"
  [filename]
  (let [xml (xml/parse filename)
        zipped (zip/xml-zip xml)]
    (xml-> zipped :page :title text)))

(count (titles data-9764))
; 38

(count (titles data-99224))
; 779

(count (titles data-995066))
; 5172

(count (titles data-7999931))
; OutOfMemoryError Java heap space  java.util.Arrays.copyOfRange (Arrays.java:3209)

我在代码中做错了什么?或者这可能是我正在使用的库中的错误或限制?基于REPL实验,似乎我使用的代码是懒惰的。在下面,Clojure使用SAX XML解析器,因此不应该只是问题。

另见:

更新2013-04-30:

我想与clojure IRC频道分享一些讨论。我在下面贴了一个编辑过的版本。 (我删除了用户名,但是如果你想要信用,请告诉我;我会编辑并给你一个链接。)

  

整个标记在xml/parse中一次读入内存,   很久你甚至还会打电话。并且clojure.xml使用~lazy SAX   解析器生成一个急切的具体集合。懒惰地处理XML   需要比你想象的更多的工作 - 这将是的工作   做,不是一些魔法clojure.xml可以为你做的。随意反驳   致电(count (xml/parse data-whatever))

总结一下,即使在使用zip/xml-zip之前,此xml/parse会导致OutOfMemoryError文件足够大:

(count (xml/parse filename))

目前,我正在探索其他XML处理选项。我的列表顶部是clojure.data.xml,如https://stackoverflow.com/a/9946054/109618所述。

2 个答案:

答案 0 :(得分:4)

这是拉链数据结构的限制。拉链设计用于有效地导航各种树木,支持在树状层次结构中上/下/左/右移动,并在近乎恒定的时间内进行就地编辑。

从树中的任何位置,拉链都需要能够重新构建原始树(应用了编辑)。为此,它会跟踪当前节点,父节点以及树中当前节点左侧和右侧的所有兄弟节点,大量使用持久性数据结构。

您正在使用的过滤器函数从节点最左侧的子节点开始,一个接一个地向右移动,沿途测试谓词。最左边的孩子的拉链开始时左手兄弟的空矢量(请注意zip/down源中的:l []部分)。每次向右移动时,它都会将访问过的最后一个节点添加到左侧兄弟的向量中(zip/right中的:l (conj l node))。当你到达最右边的孩子时,你已经建立了树中该级别所有节点的内存向量,对于像你这样的宽树,可能会导致OOM错误。

作为一种解决方法,如果您知道顶级元素只是<page>元素列表的容器,我建议使用拉链在页面元素中导航并使用{{1处理页面:

map

所以,基本上,我们避免将zip抽象用于整个xml输入的顶层,从而避免将整个xml保存在内存中。这意味着对于甚至更大的xml,每个第一级子级都很大,我们可能不得不在XML结构的第二级再次跳过使用拉链,依此类推......

答案 1 :(得分:1)

查看source for xml-zip,它似乎完全懒惰:

(defn xml-zip
  "Returns a zipper for xml elements (as from xml/parse),
  given a root element"
  {:added "1.0"}
  [root]
    (zipper (complement string?) 
            (comp seq :content)
            (fn [node children]
              (assoc node :content (and children (apply vector children))))
            root))

注意(apply vector children),它实现了向量的children seq(尽管它没有实现整个后代树,所以它仍然是懒惰的)。如果一个节点有很多子节点(例如,<mediawiki>的子节点),那么即使这种懒惰程度也不够 - :content也需要是一个seq。

我对拉链的了解非常有限,所以我不确定为什么vector在这里被使用;看看用(assoc node :content (and children (apply vector children))))替换(assoc node :content children)是否有效,这应该将children保留为正常序列而不实现它。

(就此而言,我不确定为什么(apply vector children)代替(vec children) ...)

content-handler看起来它正在构建*contents*中的所有内容元素,因此OOM的源可能位于内容处理程序本身。

我不确定如何将拉链接口(树状)与您想要的流式协调。它适用于大型xml,但不适用于巨大的 xml。

在其他语言的类似方法中(例如Python的iterparse),树就像拉链一样迭代地构建。不同之处在于,在成功处理元素后,树将被修剪。

例如,在使用iterparse的Python中,您将在page上侦听endElement事件(即,当</page>出现在XML中时。)此时您知道您有一个完整的页面元素可以像树一样处理。完成后,删除刚刚处理的元素和控制内存使用的兄弟分支。

也许你也可以在这里采用这种方法。 xml zipper提供的节点是xml/element的var。内容处理程序可以返回一个函数,该函数在调用时对其*current* var进行清理。然后你可以调用它来修剪树。

或者,您可以在clojure中“手动”使用SAX作为根元素,并在遇到它时为每个page元素创建一个拉链。