从 XML 节点 java 生成/获取 xpath

Posted

技术标签:

【中文标题】从 XML 节点 java 生成/获取 xpath【英文标题】:Generate/get xpath from XML node java 【发布时间】:2011-06-12 09:22:13 【问题描述】:

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

我想查看 xml 文档及其所有节点 检查节点是否存在属性

如果节点没有属性,get/generate String with value of its xpath 如果节点确实具有属性,则迭代槽属性列表并为包括节点在内的每个属性创建 xpath。

忠告?希望你能提供一些有用的信息

编辑:

这样做的原因是 .. 我正在 jmeter 中编写自动化测试,因此对于每个请求,我都需要验证该请求确实完成了它的工作,因此我通过使用 xpath 获取节点值来断言结果。(额外信息 -无关)

当请求很小时,手动创建断言不是问题,但对于较大的请求,它真的很痛苦..(额外信息 - 不相关)

赏金:

我正在寻找java方法

目标

我的目标是从这个 ex 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

【问题讨论】:

好问题,+1。请参阅我的答案以获得完整的 XSLT 1.0 解决方案,该解决方案采用包含节点集的参数并为此节点集中的每个节点生成 XPath 表达式。节点可以是任何类型:文档节点、元素、文本节点、属性、注释、PI、命名空间。 您想要什么样的 XPath 表达式?您可以简单地在其父节点的getChildren() 节点列表中获取每个元素的索引,并创建一个类似/*[5]/*[2]/*[8]/@yourattr 的xpath。但是如果你想断言结果,你不应该反过来做吗?编写一个 xpath 表达式,如果您的 XML 正确则返回 true,否则返回 false,然后计算它? @biziclop 我想从我发送的请求中创建 xpath(所以我可以用它来验证结果),而不是相反。我更新了我的问题 @c0mrade:您更新的问题中存在漏洞。如果一个元素有多个文本节点,如:&lt;x&gt;text 1&lt;y/&gt;text 2&lt;/x&gt; 所需的解决方案应如何处理任何此类元素?我将使用 XSLT 解决方案和 C# 解决方案更新我的答案(我的 Java 有点生疏)——这对你有用吗? @Dimitre Novatchev 感谢您的评论,据我所知,我的 xml 文件中从未发生过这种情况,而且我认为不会发生。正如 BalusC 建议的那样,我可以让 java 运行 XSLT,如果它产生正确的输出,如我上面发布的示例。 tnx 【参考方案1】:

更新

@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='89734'
/root/elemX[@serial='kefw90234kf2esda9231']

说明

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

对于任何此类元素,如果它没有子元素,则其所有祖先或自身元素都将在特定模式下处理,命名为 'path'。然后输出"='theValue'"部分,然后是一个NL字符。

然后处理匹配元素的所有属性

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

'path' 模式下处理元素很简单:输出/ 字符和元素名称。然后,如果前面有同名的兄弟姐妹,则输出“[numPrecSiblings+1]”部分。

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

请注意

名称空间中的名称以初始可读形式显示,没有任何问题。

为了提高可读性,从不显示[1] 的索引。


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

这是一个纯 XSLT 1.0 解决方案

下面是一个示例 xml 文档和一个样式表,它采用节点集参数并为每个成员节点生成一个有效的 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']

【讨论】:

让 Java 运行 XSLT 并收集其结果? @BalusC 我可以这样做,但这并不是我所要求的,而且由于我不知道这段代码,所以我更喜欢我可以更新/编辑的代码,所以我更新了我的题。 tnx @Dimitre Novatchev 太好了,它完全按照我的意愿工作。代码的小尺寸和它的作用给我留下了深刻的印象。看起来你知道你的方式 arround xsl/xml 我必须肯定地探索 xsl。你能推荐一些有用的网络/书籍资源供我学习吗?我已经为你的博客添加了书签,在那里看到了大量的代码,但我并没有真正得到我需要从基础开始工作,直到达到顶峰。再次伟大的 tnx,我可以在 21 小时内接受赏金,我会在那个时间到期时接受。感谢您的帮助 @c0mrade:不客气。是的,XSLT 是一种非常强大的语言。如需更多资源,请查看我对另一个 SO 问题的回答:***.com/questions/339930/… @Dimitre Novatchev 太棒了,谢谢一百万。它完全按照我的计划工作。我一定要通过你建议的链接。谢谢【参考方案2】:

以下是使用 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'

【讨论】:

不错 :) 我们现在只需要一个 StAX 实现,我们将拥有完整的一套。 +1 为你的努力,我第二次 biziclop 的评论,有人会发现它在未来有用 等一下...elementNameCount 统计整个文档中特定元素类型(名称)的出现次数,无论它们是兄弟姐妹、堂兄弟姐妹(相同级别但不同父级)还是在不同的层次。但是您输出 XPath "[" + count + "]" 就好像我们在计算兄弟姐妹之间的位置一样。对于非平凡的文档,这显然会失败。正确的?例如。 &lt;a&gt;&lt;a&gt;foo&lt;/a&gt;&lt;/a&gt; 会输出//a[1]/a[2]='foo',而[2] 不正确。 @BlaiseDoughan 你能看看这个问题吗?***.com/questions/10698287/…。我在java中使用xml签名,为此我必须使用xpath提取要签名的部分。但它就是行不通。 @LarsH 不,不是,因为在每个 startElement 转换时都会创建一个新的 FragmentContentHandler ,并使用它自己的 elementNameCount 注册表。这应该可以正常工作,但必须自己尝试。【参考方案3】:

使用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() + "'"
);

如果文档是您的示例文档:

<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(尚)不支持匹配/映射属性。因此,您的属性不会产生任何输出。不过,这将在不久的将来实施。

【讨论】:

你能看看这个问题吗 - ***.com/questions/10698287/… 。我在java中使用xml签名,为此我必须使用xpath提取要签名的部分。但它只是不起作用 @Ashwin:对不起,我对“XPath 转换”没有任何经验。我不认识你在那里使用的那个库 美元符号$ 是怎么回事?那是合法的 Java 吗?! @JasonS 这是一个合法的标识符,是的。它是从JOOX.$ 静态导入的。我会更新答案 这很有效,但不适用于大型 XML 文件。有什么建议吗?【参考方案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 表达式更难阅读,因为每个元素都类似于 *:&lt;name&gt;[namespace-uri()='&lt;nsuri&gt;'][&lt;index&gt;],但除此之外它很容易实现。另一方面,混合内容会使 xpath 的使用变得非常乏味,因为您必须能够单独处理元素中的第二个、第三个等文本节点。

【讨论】:

【参考方案5】:
    使用 w3c.dom 递归下去 对于每个节点,有一种简单的方法可以获取它的 xpath:在 #2 时将其存储为数组/列表,或者通过递归直到 parent 为空的函数,然后反转遇到的节点的数组/列表。李>

类似的东西。

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

【讨论】:

【参考方案6】:

我曾经做过类似的任务。使用的主要思想是您可以在 xpath 中使用元素的索引。例如在下面的xml中

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

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

【讨论】:

【参考方案7】:

我编写了一个方法来返回Practical XML 库中元素的绝对路径。为了让您了解它的工作原理,这是unit tests 之一的摘录:

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

因此,您可以通过文档递归,应用您的测试,并使用它来返回 XPath。或者,可能更好的是,您可以使用同一个库中的 XPath-based assertions。

【讨论】:

谢谢你的回答,这个库有文档/例子吗?【参考方案8】:

上周我做了完全相同的事情来将我的 xml 处理为符合 solr 的格式。

既然你想要一个伪代码:这就是我实现它的方法。

// 可以跳过对父子的引用。

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

2_创建一个空列表

3_ 创建 xml 的 dom 表示并遍历节点。对于每个节点,获取相应的信息。像节点名称、属性名称和值这样的所有信息都应该可以从 dom 对象中轻松获得。 (你需要检查dom NodeType,代码应该忽略处理指令和纯文本节点。)

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

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

5_ 在 setAttributes(List) 的 setter 方法中,我将在对象的路径中附加所有可用的属性。 (具有所有可用属性的路径。不是具有每个可能的属性组合的路径列表。您可能想要做其他方式。)

6_ 将 NodeObjectVO 添加到列表中。

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

(注意:就像我提到的,我保持父子关系,你可能应该跳过那部分。有代码膨胀的可能性,特别是在getparentpath时。对于小xml这不是问题,但这是一个问题大 xml)。

【讨论】:

以上是关于从 XML 节点 java 生成/获取 xpath的主要内容,如果未能解决你的问题,请参考以下文章

[Java] 通过XPath获取XML中某个节点的属性

使用xpath从xml获取子节点值

使用 xslt 2 从节点生成 xpath

xpath 只选择相关节点

java 从XML java XPath获取字段

Xpath 从兄弟节点的父节点获取值