在Java中从XML生成/获取Xpath

43

我对建议/代码/解释感兴趣,而不是实际实现。

  • 我想遍历XML文档中的所有节点
  • 检查节点是否存在属性

情况1:如果节点没有属性,则获取/生成其xpath值的字符串
情况2:如果节点具有属性,则迭代属性列表并为每个属性创建包括节点在内的xpath。

编辑

我这样做的原因是:我正在使用JMeter编写自动化测试,因此对于每个请求,我需要验证请求实际完成了其工作,因此我通过使用Xpath获取节点值来断言结果。

当请求很小时,手动创建断言不是问题,但对于较大的请求来说,这真的很痛苦。

我正在寻找Java方法。

目标

我的目标是从此示例XML文件中实现以下内容:

<root>
    <elemA>one</elemA>
    <elemA attribute1='first' attribute2='second'>two</elemA>
    <elemB>three</elemB>
    <elemA>four</elemA>
    <elemC>
        <elemB>five</elemB>
    </elemC>
</root>
产生以下结果:
//root[1]/elemA[1]='one'
//root[1]/elemA[2]='two'
//root[1]/elemA[2][@attribute1='first']
//root[1]/elemA[2][@attribute2='second']
//root[1]/elemB[1]='three'
//root[1]/elemA[3]='four'
//root[1]/elemC[1]/elemB[1]='five'

解释:

  • 如果节点值/文本不为空/零,获取xpath,并添加“nodevalue”以进行断言
  • 如果节点具有属性,则为它们创建断言

更新

我找到了这个例子,它不能产生正确的结果,但我正在寻找像这样的东西:

http://www.coderanch.com/how-to/java/SAXCreateXPath


你想要什么样的XPath表达式呢?你可以简单地获取每个元素在其父节点的getChildren()节点列表中的索引,并创建一个类似于/*[5]/*[2]/*[8]/@yourattr的XPath。但是,如果你想要断言结果,难道不应该反过来做吗?编写一个XPath表达式,如果你的XML正确则返回true,否则返回false,然后进行评估? - biziclop
@c0mrade:你更新的问题中有漏洞。如果一个元素有多个文本节点,比如<x>text 1<y/>text 2</x>,那么想要的解决方案应该如何处理这样的元素?我会更新我的答案,提供XSLT和C#两种解决方案(我的Java有点生疏)--这对你有用吗? - Dimitre Novatchev
@Dimitre Novatchev 是的,非常欢迎。谢谢。 - ant
@c0mrade:我已经编写了一个完整且非常简短(30行)的XSLT解决方案,易于理解并且可以完美解决您的问题。 - Dimitre Novatchev
可能是如何检索相应的XPath的重复问题。 - james.garriss
显示剩余7条评论
8个回答

49

更新:

@c0mrade已经更新了他的问题。以下是解决方案:

这个XSLT转换:

<xsl:stylesheet version="1.0"  xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
    <xsl:output method="text"/>
    <xsl:strip-space elements="*"/>
    
    <xsl:variable name="vApos">'</xsl:variable>
    
    <xsl:template match="*[@* or not(*)] ">
      <xsl:if test="not(*)">
         <xsl:apply-templates select="ancestor-or-self::*" mode="path"/>
         <xsl:value-of select="concat('=',$vApos,.,$vApos)"/>
         <xsl:text>&#xA;</xsl:text>
        </xsl:if>
        <xsl:apply-templates select="@*|*"/>
    </xsl:template>
    
    <xsl:template match="*" mode="path">
        <xsl:value-of select="concat('/',name())"/>
        <xsl:variable name="vnumPrecSiblings" select=
         "count(preceding-sibling::*[name()=name(current())])"/>
        <xsl:if test="$vnumPrecSiblings">
            <xsl:value-of select="concat('[', $vnumPrecSiblings +1, ']')"/>
        </xsl:if>
    </xsl:template>
    
    <xsl:template match="@*">
        <xsl:apply-templates select="../ancestor-or-self::*" mode="path"/>
        <xsl:value-of select="concat('[@',name(), '=',$vApos,.,$vApos,']')"/>
        <xsl:text>&#xA;</xsl:text>
    </xsl:template>
</xsl:stylesheet>

当应用于提供的XML文档时:

<root>
    <elemA>one</elemA>
    <elemA attribute1='first' attribute2='second'>two</elemA>
    <elemB>three</elemB>
    <elemA>four</elemA>
    <elemC>
        <elemB>five</elemB>
    </elemC>
</root>

能够完全生成所需的、正确的结果:

/root/elemA='one'
/root/elemA[2]='two'
/root/elemA[2][@attribute1='first']
/root/elemA[2][@attribute2='second']
/root/elemB='three'
/root/elemA[3]='four'
/root/elemC/elemB='five'

应用于 @c0mrade 新提供的文档时:

<root>
    <elemX serial="kefw90234kf2esda9231">
        <id>89734</id>
    </elemX>
</root>

再次生成了正确的结果:

/root/elemX[@serial='kefw90234kf2esda9231']
/root/elemX/id='89734'

说明:

  • 只有没有子元素或者有属性的元素才会被匹配并处理。

  • 对于任何这样的元素,如果它没有子元素,则将处理它的所有祖先或自身元素,处理模式为'path'。然后输出"='theValue'"部分,然后输出一个 NL 字符。

  • 接着处理所有匹配元素的属性

  • 最后,将模板应用于所有子元素

  • 'path'模式下处理元素很简单:输出一个 / 字符和元素的名称。然后,如果有相同名称的前置同级元素,则输出一个 "[numPrecSiblings+1]" 部分。

  • 处理属性很简单:首先处理其父元素的所有 ancestor-or-self:: 元素,处理模式为 'path',然后输出 [attrName=attrValue] 部分,随后是一个 NL 字符。

请注意:

  • 属于命名空间的名称可以以初始可读形式显示,没有问题。

  • 为了增加可读性,永远不会显示索引为 [1]


以下是我的初始回答(可以忽略)

这里提供了一个纯 XSLT 1.0 解决方案,接受一个节点集参数并为每个成员节点生成一个有效的 XPath 表达式。

样式表(buildPath.xsl):


<xsl:stylesheet version='1.0'
xmlns:xsl='http://www.w3.org/1999/XSL/Transform'
xmlns:msxsl="urn:schemas-microsoft-com:xslt" 
>

<xsl:output method="text"/>
<xsl:variable name="theParmNodes" select="//namespace::*[local-name() =
'myNamespace']"/>
<xsl:template match="/">
  <xsl:variable name="theResult">
    <xsl:for-each select="$theParmNodes">
    <xsl:variable name="theNode" select="."/>
    <xsl:for-each select="$theNode |
$theNode/ancestor-or-self::node()[..]">
      <xsl:element name="slash">/</xsl:element>
      <xsl:choose>
        <xsl:when test="self::*">           
          <xsl:element name="nodeName">
            <xsl:value-of select="name()"/>
            <xsl:variable name="thisPosition" 
                select="count(preceding-sibling::*[name(current()) = 
                        name()])"/>
            <xsl:variable name="numFollowing" 
                select="count(following-sibling::*[name(current()) = 
                        name()])"/>
            <xsl:if test="$thisPosition + $numFollowing > 0">
              <xsl:value-of select="concat('[', $thisPosition +
                                                           1, ']')"/>
            </xsl:if>
          </xsl:element>
        </xsl:when>
        <xsl:otherwise> <!-- This node is not an element -->
          <xsl:choose>
            <xsl:when test="count(. | ../@*) = count(../@*)">   
            <!-- Attribute -->
              <xsl:element name="nodeName">
                <xsl:value-of select="concat('@',name())"/>
              </xsl:element>
            </xsl:when>     
            <xsl:when test="self::text()">  <!-- Text -->
              <xsl:element name="nodeName">
                <xsl:value-of select="'text()'"/>
                <xsl:variable name="thisPosition" 
                          select="count(preceding-sibling::text())"/>
                <xsl:variable name="numFollowing" 
                          select="count(following-sibling::text())"/>
                <xsl:if test="$thisPosition + $numFollowing > 0">
                  <xsl:value-of select="concat('[', $thisPosition + 
                                                           1, ']')"/>
                </xsl:if>
              </xsl:element>
            </xsl:when>     
            <xsl:when test="self::processing-instruction()">
            <!-- Processing Instruction -->
              <xsl:element name="nodeName">
                <xsl:value-of select="'processing-instruction()'"/>
                <xsl:variable name="thisPosition" 
                   select="count(preceding-sibling::processing-instruction())"/>
                <xsl:variable name="numFollowing" 
                    select="count(following-sibling::processing-instruction())"/>
                <xsl:if test="$thisPosition + $numFollowing > 0">
                  <xsl:value-of select="concat('[', $thisPosition + 
                                                            1, ']')"/>
                </xsl:if>
              </xsl:element>
            </xsl:when>     
            <xsl:when test="self::comment()">   <!-- Comment -->
              <xsl:element name="nodeName">
                <xsl:value-of select="'comment()'"/>
                <xsl:variable name="thisPosition" 
                         select="count(preceding-sibling::comment())"/>
                <xsl:variable name="numFollowing" 
                         select="count(following-sibling::comment())"/>
                <xsl:if test="$thisPosition + $numFollowing > 0">
                  <xsl:value-of select="concat('[', $thisPosition + 
                                                            1, ']')"/>
                </xsl:if>
              </xsl:element>
            </xsl:when>     
            <!-- Namespace: -->
            <xsl:when test="count(. | ../namespace::*) = 
                                               count(../namespace::*)">

              <xsl:variable name="apos">'</xsl:variable>
              <xsl:element name="nodeName">
                <xsl:value-of select="concat('namespace::*', 
                '[local-name() = ', $apos, local-name(), $apos, ']')"/>

              </xsl:element>
            </xsl:when>     
          </xsl:choose>
        </xsl:otherwise>            
      </xsl:choose>
    </xsl:for-each>
    <xsl:text>&#xA;</xsl:text>
  </xsl:for-each>
 </xsl:variable>
 <xsl:value-of select="msxsl:node-set($theResult)"/>
</xsl:template>
</xsl:stylesheet>

XML源代码(buildPath.xml):


<!-- top level Comment -->
<root>
    <nodeA>textA</nodeA>
 <nodeA id="nodeA-2">
  <?myProc ?>
        xxxxxxxx
  <nodeB/>
        <nodeB xmlns:myNamespace="myTestNamespace">
  <!-- Comment within /root/nodeA[2]/nodeB[2] -->
   <nodeC/>
  <!-- 2nd Comment within /root/nodeA[2]/nodeB[2] -->
        </nodeB>
        yyyyyyy
  <nodeB/>
  <?myProc2 ?>
    </nodeA>
</root>
<!-- top level Comment -->

结果:

/root/nodeA[2]/nodeB[2]/namespace::*[local-name() = 'myNamespace']
/root/nodeA[2]/nodeB[2]/nodeC/namespace::*[local-name() =
'myNamespace']

3
让Java运行XSLT并收集其结果? - BalusC
1
@BalusC 我可以这样做,但这并不完全是我所问的问题,而且既然我不知道这段代码,我更喜欢我可以更新/编辑的代码,我已更新我的问题。谢谢。 - ant
1
@Dimitre Novatchev 太好了,它完全按照我的要求工作。我对代码的小巧和功能印象深刻。看起来你很懂得xsl/xml,我一定要探索一下xsl。你能推荐一些有用的网站/书籍资源供我学习吗?我已经收藏了你的博客,在那里看到了很多我不太理解的代码,我需要从基础开始逐步提高。再次感谢你的帮助,我将在21小时后接受赏金。谢谢! - ant
2
@c0mrade:不用谢。是的,XSLT 是一种非常强大的语言。如果需要更多资源,请查看我回答另一个 SO 问题的链接:http://stackoverflow.com/questions/339930/any-good-xslt-tutorial-book-blog-site-online/341589#341589 - Dimitre Novatchev
1
@Dimitre Novatchev 真是太棒了,非常感谢。它完全按照我的计划工作。我一定要仔细阅读你建议的链接。谢谢。 - ant
显示剩余2条评论

20

以下是如何使用SAX实现该功能:

import java.util.HashMap;
import java.util.Map;

import org.xml.sax.Attributes;
import org.xml.sax.SAXException;
import org.xml.sax.XMLReader;
import org.xml.sax.helpers.DefaultHandler;

public class FragmentContentHandler extends DefaultHandler {

    private String xPath = "/";
    private XMLReader xmlReader;
    private FragmentContentHandler parent;
    private StringBuilder characters = new StringBuilder();
    private Map<String, Integer> elementNameCount = new HashMap<String, Integer>();

    public FragmentContentHandler(XMLReader xmlReader) {
        this.xmlReader = xmlReader;
    }

    private FragmentContentHandler(String xPath, XMLReader xmlReader, FragmentContentHandler parent) {
        this(xmlReader);
        this.xPath = xPath;
        this.parent = parent;
    }

    @Override
    public void startElement(String uri, String localName, String qName, Attributes atts) throws SAXException {
        Integer count = elementNameCount.get(qName);
        if(null == count) {
            count = 1;
        } else {
            count++;
        }
        elementNameCount.put(qName, count);
        String childXPath = xPath + "/" + qName + "[" + count + "]";

        int attsLength = atts.getLength();
        for(int x=0; x<attsLength; x++) {
            System.out.println(childXPath + "[@" + atts.getQName(x) + "='" + atts.getValue(x) + ']');
        }

        FragmentContentHandler child = new FragmentContentHandler(childXPath, xmlReader, this);
        xmlReader.setContentHandler(child);
    }

    @Override
    public void endElement(String uri, String localName, String qName) throws SAXException {
        String value = characters.toString().trim();
        if(value.length() > 0) {
            System.out.println(xPath + "='" + characters.toString() + "'");
        }
        xmlReader.setContentHandler(parent);
    }

    @Override
    public void characters(char[] ch, int start, int length) throws SAXException {
        characters.append(ch, start, length);
    }

}

可以通过以下方式进行测试:

import java.io.FileInputStream;

import javax.xml.parsers.SAXParser;
import javax.xml.parsers.SAXParserFactory;

import org.xml.sax.InputSource;
import org.xml.sax.XMLReader;

public class Demo {

    public static void main(String[] args) throws Exception {
        SAXParserFactory spf = SAXParserFactory.newInstance();
        SAXParser sp = spf.newSAXParser();
        XMLReader xr = sp.getXMLReader();

        xr.setContentHandler(new FragmentContentHandler(xr));
        xr.parse(new InputSource(new FileInputStream("input.xml")));
    }
}
这将生成所需的输出:
//root[1]/elemA[1]='one'
//root[1]/elemA[2][@attribute1='first]
//root[1]/elemA[2][@attribute2='second]
//root[1]/elemA[2]='two'
//root[1]/elemB[1]='three'
//root[1]/elemA[3]='four'
//root[1]/elemC[1]/elemB[1]='five'

3
好极了 :) 现在我们所需要的就是一个StAX实现,这样我们就拥有了全部的工具集。 - biziclop
+1 鼓励你的努力,我赞同 biziclop 的评论,将来可能会有人发现它很有用。 - ant
1
等一下... elementNameCount 计算文档全局特定元素类型(名称)的出现次数,无论它们是兄弟姐妹、表亲(同级但不同父母)还是在不同层级。但是你输出 XPath "[" + count + "]" 就好像我们正在计算兄弟姐妹中的位置一样。这对于非平凡的文档显然会失败。对吧?例如 <a><a>foo</a></a> 将输出 //a[1]/a[2]='foo',而 [2] 是不正确的。 - LarsH
@BlaiseDoughan,您能否请看一下这个问题 - http://stackoverflow.com/questions/10698287/xpath-transformation-not-working-in-java 。我正在使用Java中的XML签名,因此必须使用xpath提取要签名的部分。但是它就是不起作用。 - Ashwin
1
@LarsH 不是这样的,因为每次 startElement 转换都会创建一个新的 FragmentContentHandler,并带有自己的 elementNameCount 注册表。这应该可以正常工作,但我必须自己尝试一下。 - NagyI
@Nagyl:你可能是对的。我已经有3.5年没看过这个了。:-) 如果你测试过,请告诉我们。 - LarsH

13

使用jOOX(一个Java的jquery API端口,免责声明-我在这个库背后的公司工作),你可以通过一个语句实现你所想要的几乎所有内容:

// I'm assuming this:
import static org.joox.JOOX.$;

// And then...
List<String> coolList = $(document).xpath("//*[not(*)]").map(
    context -> $(context).xpath() + "='" + $(context).text() + "'"
);

如果 document 是你的样本文档:
<root>
    <elemA>one</elemA>
    <elemA attribute1='first' attribute2='second'>two</elemA>
    <elemB>three</elemB>
    <elemA>four</elemA>
    <elemC>
        <elemB>five</elemB>
    </elemC>
</root>

这将产生

/root[1]/elemA[1]='one'
/root[1]/elemA[2]='two'
/root[1]/elemB[1]='three'
/root[1]/elemA[3]='four'
/root[1]/elemC[1]/elemB[1]='five'

“几乎”意味着 jOOX(尚)不支持匹配/映射属性。因此,您的属性将不会产生任何输出。不过这个功能将在不久的将来实现。

你能请看一下这个问题吗 - http://stackoverflow.com/questions/10698287/xpath-transformation-not-working-in-java 。我正在使用Java中的XML签名,为此我必须使用xpath提取要签名的部分。但是它就是不起作用。 - Ashwin
@Ashwin:很抱歉,我没有使用“XPath转换”的经验。我不认识你正在使用的那个库。 - Lukas Eder
带着美元符号$是什么意思?这合法吗,Java? - Jason S
@JasonS 是一个合法的标识符,是从 JOOX.$ 静态导入的。我会更新答案。 - Lukas Eder
这个很好用,但是对于大型XML文件不太适用。有什么建议吗? - Brian T Hannan
@BrianTHannan:你可以实现一个SAX处理器。 - Lukas Eder

4
private static void buildEntryList( List<String> entries, String parentXPath, Element parent ) {
    NamedNodeMap attrs = parent.getAttributes();
    for( int i = 0; i < attrs.getLength(); i++ ) {
        Attr attr = (Attr)attrs.item( i );
        //TODO: escape attr value
        entries.add( parentXPath+"[@"+attr.getName()+"='"+attr.getValue()+"']"); 
    }
    HashMap<String, Integer> nameMap = new HashMap<String, Integer>();
    NodeList children = parent.getChildNodes();
    for( int i = 0; i < children.getLength(); i++ ) {
        Node child = children.item( i );
        if( child instanceof Text ) {
            //TODO: escape child value
            entries.add( parentXPath+"='"+((Text)child).getData()+"'" );
        } else if( child instanceof Element ) {
            String childName = child.getNodeName();
            Integer nameCount = nameMap.get( childName );
            nameCount = nameCount == null ? 1 : nameCount + 1;
            nameMap.put( child.getNodeName(), nameCount );
            buildEntryList( entries, parentXPath+"/"+childName+"["+nameCount+"]", (Element)child);
        }
    }
}

public static List<String> getEntryList( Document doc ) {
    ArrayList<String> entries = new ArrayList<String>();
    Element root = doc.getDocumentElement();
    buildEntryList(entries, "/"+root.getNodeName()+"[1]", root );
    return entries;
}

这段代码有两个前提条件:您没有使用命名空间,并且没有混合内容元素。命名空间限制不是很严格,但会使XPath表达式更难读,因为每个元素都会像*:<name>[namespace-uri()='<nsuri>'][<index>]这样,但除此之外很容易实现。另一方面,混合内容将使xpath的使用非常繁琐,因为您必须能够单独地访问元素内的第二、第三等文本节点。


2
  1. 使用w3c.dom
  2. 递归向下遍历
  3. 对于每个节点,有一种简单的方法可以获取它的xpath:要么在#2时将其存储为数组/列表,要么通过函数进行递归,直到父级为空,然后反转遇到的节点的数组/列表。

类似这样。

更新: 并连接最终列表以获取最终的xpath。 不认为属性会成为问题。


1

我曾经完成过类似的任务。主要思路是可以使用xpath中元素的索引。例如,在以下xml中:

<root>
    <el />
    <something />
    <el />
</root>

与第二个<el/>相关的xpath将是/root[1]/el[2](xpath索引从1开始)。这意味着“取第一个根元素,然后从所有名称为el的元素中取第二个”。因此,元素something不会影响元素el的索引。因此,您可以理论上为xml中的每个特定元素创建一个xpath。在实践中,我通过递归遍历树并记住有关元素及其索引的信息来完成了这一点。
然后,创建引用元素特定属性的xpath只需将'/@attrName'添加到元素的xpath即可。


1

我已经编写了一个方法来返回Practical XML库中元素的绝对路径。为了让你了解它是如何工作的,这里有一个单元测试的摘录:

assertEquals("/root/wargle[2]/zargle",
             DomUtil.getAbsolutePath(child3a)); 

因此,您可以通过文档进行递归,应用测试,并使用此方法返回XPath。或者更好的方法是,您可以使用同一库中的基于XPath的断言


谢谢您的回答,这个库有一些文档/示例吗? - ant

1

上周我也做了同样的事情,将我的XML处理成Solr兼容格式。

既然你想要伪代码,这是我完成它的方法:

// 你可以跳过对父节点和子节点的引用。

1_ 初始化一个自定义节点对象:NodeObjectVO {String nodeName, String path, List attr, NodeObjectVO parent, List child}

2_ 创建一个空列表

3_ 创建XML的DOM表示,并遍历节点。对于每个节点,获取相应的信息。所有信息,如节点名称、属性名称和值,都应该从DOM对象中轻松获得。(您需要检查DOM节点类型,代码应忽略处理指令和纯文本节点。)

// 代码膨胀警告。 4_ 唯一棘手的部分是获取路径。我创建了一个迭代实用程序方法,从NodeElement获取xpath字符串。(While(node.Parent != null ) { path+=node.parent.nodeName}.

(您还可以通过维护全局路径变量来实现此目的,该变量跟踪每次迭代的父路径。)

5_ 在setAttributes(List)的setter方法中,我将使用所有可用属性附加对象路径。(一个具有所有可用属性的路径。而不是每个可能属性组合的路径列表。您可能希望以其他方式完成。)

6_ 将NodeObjectVO添加到列表中。

7_ 现在我们有了一个扁平(非层次结构)的自定义节点对象列表,其中包含我需要的所有信息。

(注意:正如我提到的那样,我维护父子关系,您可能应该跳过该部分。存在代码膨胀的可能性,特别是在getparentpath时。对于小型xml,这不是问题,但对于大型xml来说,这是一个问题。)


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接