【问题标题】:OutOfMemoryError when parsing XML in Clojure with data.zip使用 data.zip 在 Clojure 中解析 XML 时出现 OutOfMemoryError
【发布时间】:2013-04-29 23:40:57
【问题描述】:

我想使用 Clojure 从维基词典 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 年 4 月 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 所述。

【问题讨论】:

  • 啊,是的。早该发现的。你肯定想要clojure.data.xml 而不是clojure.xml - 过渡应该很容易。

标签: xml clojure out-of-memory


【解决方案1】:

这是 zipper 数据结构的限制。 Zippers 旨在有效地导航各种树,支持在树层次结构中向上/向下/向左/向右移动,并在近乎恒定的时间内进行就地编辑。

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

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

作为一种解决方法,如果您知道顶级元素只是 &lt;page&gt; 元素列表的容器,我建议使用拉链在页面元素中导航,然后使用 map 进行处理页面:

(defn titles
  "Extract titles from +filename+"
  [filename]
  (let [xml (xml/parse filename)]
    (map #(xml-> (zip/xml-zip %) :title text)
         (:content xml))))

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

【讨论】:

  • 这种方法可行,但总的来说,这使得处理大型 XML 文件的 clojure 代码在事后看来有些迟钝:我们需要来自 3 个命名空间(data.xmlzipdata.zip.xml)的函数并且需要跳过顶层的实际压缩抽象,以避免将整个 xml 保存在内存中。我想知道是否有任何其他库提供更流畅的方式。
【解决方案2】:

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 物化为一个向量(尽管它没有物化整个后代树,所以它仍然是惰性的)。如果一个节点的子节点数量非常多(例如,&lt;mediawiki&gt; 的子节点),那么即使是这种级别的惰性也不够——:content 也需要是一个 seq。

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

(就此而言,我不知道为什么是(apply vector children) 而不是(vec children)...)

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

我不确定我们如何才能使拉链界面(树状)与您想要的流媒体相协调。它适用于大型 xml,但不适用于 huge xml。

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

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

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

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

【讨论】:

  • 不确定向量是否在那里是绝对必要的,但我认为这不是 OOM 错误的原因。该向量用于 make-node 函数,该函数仅在以某种方式编辑拉链时调用。这似乎不是这里的情况。
猜你喜欢
  • 2014-05-24
  • 1970-01-01
  • 2012-10-21
  • 1970-01-01
  • 1970-01-01
  • 2014-03-01
  • 2010-11-14
  • 2011-06-22
  • 2019-09-16
相关资源
最近更新 更多