有没有办法在Scala中获得“反向”XPath?

时间:2011-09-07 13:06:32

标签: xml scala

如果我有一个DOM,是否可以获得元素的反向XPath?例如,如果我有:

<start>
  <nodes>
    <node>
      <name>Whatever</name>
    </node>
    <node>
      <name>Whatever 2</name>
    </node>
  </nodes>
</start>

例如,如果我引用了名为Whatever 2的节点,是否可以返回/start/nodes/node/name[. = "Whatever 2"]

3 个答案:

答案 0 :(得分:3)

这是使用Scala REPL中的Java DOM API向上走树的一种非常简单的方法:

首先,我们导入相关的包并设置我们的文档构建器和源代码:

scala> import org.w3c.dom._
import org.w3c.dom._

scala> import javax.xml.parsers._
import javax.xml.parsers._

scala> val factory = DocumentBuilderFactory.newInstance()
factory: javax.xml.parsers.DocumentBuilderFactory = ...

scala> val builder = factory.newDocumentBuilder()
builder: javax.xml.parsers.DocumentBuilder = ...

scala> val source = new org.xml.sax.InputSource()
source: org.xml.sax.InputSource = org.xml.sax.InputSource@7ecec7c6

现在解析示例文档:

scala> val content = """<start>
             <nodes>
               <node><name>Whatever</name></node>
               <node><name>Whatever 2</name></node>
             </nodes>
           </start>"""
content: java.lang.String = ...

scala> source.setCharacterStream(new java.io.StringReader(content))

scala> val document = builder.parse(source)
document: org.w3c.dom.Document = [#document: null]

这是一个非常简单的函数,以递归方式将DOM传递到文档根目录:

scala> def path: Node => String = {
     |   case document: Document => ""
     |   case node => path(node.getParentNode) + "/" + node.getNodeName
     | }
path: org.w3c.dom.Node => String

我们选择第二个<name>节点进行测试:

scala> val node = document.getElementsByTagName("name").item(1)
node: org.w3c.dom.Node = [name: null]

我们得到了我们的期望:

scala> path(node)
res1: String = /start/nodes/node/name

调整path函数以避免显式递归或在向上走树时收集更多信息(例如在必要时指示位置以避免歧义)并不难:

scala> def path(element: Element) = {
     |   def sameName(f: Node => Node)(n: Node) =
     |     Stream.iterate(n)(f).tail.takeWhile(_ != null).filter(
     |       _.getNodeName == n.getNodeName
     |     ).toList
     |   val preceding = sameName(_.getPreviousSibling) _
     |   val following = sameName(_.getNextSibling) _
     |   "/" + Stream.iterate[Node](element)(_.getParentNode).map {
     |     case _: Document => None
     |     case e: Element => Some { (preceding(e), following(e)) match {
     |       case (Nil, Nil) => e.getTagName
     |       case (els, _)   => e.getTagName + "[" + (els.size + 1) + "]"
     |     }}
     |   }.takeWhile(_.isDefined).map(_.get).reverse.mkString("/")
     | }
path: (element: org.w3c.dom.Element)java.lang.String

请注意,我稍微更改了类型,以清楚地表明这只会为元素提供有效的XPath路径。我们可以测试:

scala> path(node.asInstanceOf[Element])
res13: java.lang.String = /start/nodes/node[2]/name

这也是我们的期望。

答案 1 :(得分:1)

正如其他人所指出的那样,如果你拥有的只是scala.xml.Node,那么如果不花费大量的时间和空间,你就无法实现目标。

然而,如果你愿意让你的呼叫者跳过几个圈,并且你发现让你厌恶Java的想法,你可能会比尝试zipper更糟糕。

另请参阅Daniel Spiewak在Anti-XML中的implementation(可能有一天会取代Scala的内置XML支持)

答案 2 :(得分:0)

听起来你正在寻找像路径(Node)这样的函数:XPath?不幸的是,由于节点没有父引用,因此无法有效地使用scala.xml。选项包括: 1)在找到正确的节点后搜索树并正确识别。 2)使用支持父引用的另一个XML库(scala或java)... anti-xml等