如何从大型XML文档中获取流式迭代器[Node]?

Dav*_*gel 9 xml scala xml-parsing

我需要处理由大量独立记录组成的XML文档,例如

<employees>
    <employee>
         <firstName>Kermit</firstName>
         <lastName>Frog</lastName>
         <role>Singer</role>
    </employee>
    <employee>
         <firstName>Oscar</firstName>
         <lastName>Grouch</lastName>
         <role>Garbageman</role>
    </employee>
    ...
</employees>
Run Code Online (Sandbox Code Playgroud)

在某些情况下,这些只是大文件,但在其他情况下,它们可能来自流媒体源.

我不能只是scala.xml.XmlLoader.load()它因为我不想把整个文件保存在内存中(或者等待输入流关闭),当我只需要处理一个记录时时间.我知道我可以使用XmlEventReader将输入作为一系列XmlEvents流式传输.然而,这些比scala.xml.Node更不方便使用.

所以我想以某种方式得到一个懒惰的Iterator [Node],以便使用方便的Scala语法对每个单独的记录进行操作,同时保持内存使用受控制.

为此,我可以从XmlEventReader开始,在每个匹配的开始和结束标记之间建立事件缓冲区,然后从中构建一个Node树.但是,有一种我更容易被忽视的方式吗?感谢您的任何见解!

huy*_*hjl 8

您可以使用XMLEventReaderthrough 使用的底层解析器,ConstructingParser并使用回调处理顶层下方的员工节点.您只需要小心处理后立即丢弃数据:

import scala.xml._

def processSource[T](input: Source)(f: NodeSeq => T) {
  new scala.xml.parsing.ConstructingParser(input, false) {
    nextch // initialize per documentation
    document // trigger parsing by requesting document

    var depth = 0 // track depth

    override def elemStart(pos: Int, pre: String, label: String,
        attrs: MetaData, scope: NamespaceBinding) {
      super.elemStart(pos, pre, label, attrs, scope)
      depth += 1
    }
    override def elemEnd(pos: Int, pre: String, label: String) {
      depth -= 1
      super.elemEnd(pos, pre, label)
    }
    override def elem(pos: Int, pre: String, label: String, attrs: MetaData,
        pscope: NamespaceBinding, nodes: NodeSeq): NodeSeq = {
      val node = super.elem(pos, pre, label, attrs, pscope, nodes)
      depth match {
        case 1 => <dummy/> // dummy final roll up
        case 2 => f(node); NodeSeq.Empty // process and discard employee nodes
        case _ => node // roll up other nodes
      }
    }
  }
}
Run Code Online (Sandbox Code Playgroud)

然后你像这样用来处理常量内存中第二级的每个节点(假设第二级的节点没有获得任意数量的子节点):

processSource(src){ node =>
  // process here
  println(node)
}
Run Code Online (Sandbox Code Playgroud)

与之相比的好处XMLEventReader是您不使用两个线程.此外,与建议的解决方案相比,您不必解析节点两次.缺点是这依赖于内部工作ConstructingParser.


Dav*_*gel 5

要从huynhjl的生成器解决方案中获取TraversableOnce[Node],请使用此技巧:

def generatorToTraversable[T](func: (T => Unit) => Unit) = 
  new Traversable[T] {
    def foreach[X](f: T => X) {
      func(f(_))
    }
  }

def firstLevelNodes(input: Source): TraversableOnce[Node] =
  generatorToTraversable(processSource(input))
Run Code Online (Sandbox Code Playgroud)

generatorToTraversable的结果不能遍历多次(即使在每个foreach调用上实例化一个新的ConstructingParser),因为输入流是一个Source,它是一个Iterator.但是,我们无法覆盖Traversable.isTraversableAgain,因为它是最终的.

真的,我们想通过返回Iterator来强制执行此操作.但是,Traversable.toIterator和Traversable.view.toIterator都会生成一个中间Stream,它将缓存所有条目(无法完成本练习的全部目的).那好吧; 如果它被访问两次,我会让流抛出异常.

还要注意整个事情不是线程安全的.

这段代码运行得很好,我相信整个解决方案都是懒惰而不是缓存(因此是常量内存),尽管我还没有在大输入上尝试过.