我想使用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所述。
答案 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
元素创建一个拉链。