3

我想使用 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>

这是我尝试过的,基于对“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整个标签在你调用 count 之前就被一次性读入内存。并clojure.xml使用 ~lazy SAX 解析器生成急切的具体集合。懒惰地处理 XML 需要的工作比你想象的要多得多——这将是 做的工作,而不是什么魔法clojure.xml可以为你做的。随意打电话反驳(count (xml/parse data-whatever))

总而言之,即使在使用之前zip/xml-zip,这也会xml/parse导致OutOfMemoryError文件足够大:

(count (xml/parse filename))

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

4

2 回答 2

4

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

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

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

作为一种解决方法,如果您知道顶级元素只是<page>元素列表的容器,我建议使用 zipper 在页面元素中导航并仅用于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 结构的第二级中再次跳过使用拉链,依此类推......

于 2013-04-30T15:00:00.440 回答
1

查看 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),它将childrenseq 物化为向量(尽管它没有物化整个后代树,所以它仍然是惰性的)。如果您有一个节点的大量子节点(例如, 的子节点<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)中,树是像 zipper 一样迭代构建的。不同之处在于,在元素处理成功后,树将被修剪。

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

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

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

于 2013-04-30T01:14:29.227 回答