如何在 XSLT 中使用 XPath 获取元素数组,包括缺失的元素?

Posted

技术标签:

【中文标题】如何在 XSLT 中使用 XPath 获取元素数组,包括缺失的元素?【英文标题】:How can I get array of elements, including missing elements, using XPath in XSLT? 【发布时间】:2012-03-27 18:33:57 【问题描述】:

给定以下符合 XML 的 html

<div>
 <a>a1</a>
 <b>b1</b>
</div>

<div>
 <b>b2</b>
</div>

<div>
 <a>a3</a>
 <b>b3</b>
 <c>c3</c>
</div>

执行//a 将返回:

[a1,a3]

上面的问题是第三列数据现在排在第二位,当没有找到A时就完全跳过了。

你如何表达一个 xpath 来获取所有将返回的 A 元素:

[a1, null, a3]

//c也一样,不知道能不能搞定

[null, null, c3]

更新:考虑另一种情况,没有共同的父母&lt;div&gt;

<h1>heading1</h1>
 <a>a1</a>
 <b>b1</b>


<h1>heading2</h1>
 <b>b2</b>


<h1>heading3</h1>
 <a>a3</a>
 <b>b3</b>
 <c>c3</c>

更新:我现在也可以使用 XSLT。

【问题讨论】:

也许 XPath 不是解决这个问题的方法。也许您想使用更健壮的东西,例如 JAXB。 我对 xpath 的替代品持开放态度。你怎么能用 jaxb 来表达呢? 这个问题 - 更新未定义。请编辑并提供与编辑对应的新源 XML 文档。如果不提供正确的 XML 文档,您怎么能指望人们猜出您的想法呢? 由于您没有提供与最新更新相对应的 XML 文档,我倾向于认为您不知道自己在说什么 所以想象一下上面没有&lt;div&gt; 元素的原始html(不是XML 文档)代码,你将如何使用xpath 来返回这个问题中描述的预期结果。请在我们之前关于使用 xpath 处理 html 文档的讨论中重燃您的记忆:***.com/questions/4250925/… +1 给@DimitreNovaatchev 和Kirill Polishchuk,当没有共同的div 父母时,我不明白“缺失元素”是什么意思。 abc 是否只是随机分布在整个文档中,但如果在一个父节点中少于 3 个,则缺少一个? 【参考方案1】:

XPath 中没有空值。这里有一个半相关的问题也解释了这一点:http://www.velocityreviews.com/forums/t686805-xpath-query-to-return-null-values.html

实际上,您有三个选择:

    根本不使用 XPath。 使用这个://a | //div[not(a)],如果其中没有a,它将返回div元素,并让您的Java代码处理任何div's返回为'没有a元素存在' .根据上下文,这甚至可以让您在需要时输出更有用的内容,因为您将可以访问 div 的全部内容,例如错误“在 div 中找不到 a 元素(某些标识符)” . 使用 XSLT 预处理您的 XML,该 XSLT 将 a 元素插入任何尚未具有合适默认值的 div 元素中。

您的第二种情况有点棘手,老实说,我实际上建议您根本不使用 XPath,但可以这样做:

//a | //h1[not(following-sibling::a) or generate-id(.) != generate-id(following-sibling::a[1]/preceding-sibling::h1[1])]

这将匹配任何a 元素,或任何h1 元素,其中在下一个h1 元素或文档末尾之前不存在后续a 元素。正如 Dimitre 指出的那样,这仅在您从 XSLT 中使用时才有效,因为 generate-id 是一个 XSLT 函数。

如果您不在 XLST 中使用它,您可以使用这个相当人为的公式:

//a | //h1[not(following-sibling::a) or count(. | preceding-sibling::h1) != count(following-sibling::a[1]/preceding-sibling::h1)]

它通过匹配h1 元素来工作,其中自身和所有前面的h1 元素的计数与下一个a 前面的所有h1 元素的计数不同。在 XPath 中可能有更有效的方法,但如果它比这更做作,我绝对建议不要使用 XPath。

【讨论】:

//a | //div[not(a)] 的情况下,是否会先运行| 的左侧,然后运行右侧 不,节点应该按文档顺序返回。你会得到&lt;a&gt;a1&lt;/a&gt;&lt;div&gt;&lt;b&gt;b2&lt;/b&gt;&lt;/div&gt;&lt;a&gt;a3&lt;/a&gt; 谢谢你回答了我最初比较关心的问题的第一部分。但是对上面的第二个例子有什么想法吗? 好吧,这有 8 票...我认为选择这个作为答案是公平的,我已经阅读了所有其他答案,但她列出的 xpath 表达式就足够了。我赞成以下所有其他答案,我认为它们同样好,但我认为公平地说,Flynn1179 是第一个建议使用 not() 函数的人,所以最终我正在使用他的解决方案。我也会在不久的将来使用 xslt 解决方案。【参考方案2】:

第一个问题的解决方案

这个 XPath 表达式:

    /*/div/a
|
    /*/div[not(a)]

当针对以下 XML 文档进行评估时:

<t>
    <div>
        <a>a1</a>
        <b>b1</b>
    </div>
    <div>
        <b>b2</b>
    </div>
    <div>
        <a>a3</a>
        <b>b3</b>
        <c>c3</c>
    </div>
</t>

选择以下三个节点(adiva):

<a>a1</a>
<div>
    <b>b2</b>
</div>
<a>a3</a>

在您的 java 数组中,任何选定的非a 元素都应被视为(或替换为)null


这是第二个问题的一种解决方案

使用这些 XPath 表达式从每个组中选择 a 元素

第一组:

/*/h1[1]
   /following-sibling::a
      [not(/*/h1[2])
     or
       count(.|/*/h1[2]/preceding-sibling::a)
      =
       count(/*/h1[2]/preceding-sibling::a)
      ]

第二组

/*/h1[2]
   /following-sibling::a
      [not(/*/h1[3])
     or
       count(.|/*/h1[3]/preceding-sibling::a)
      =
       count(/*/h1[3]/preceding-sibling::a)
      ]

第三组

/*/h1[3]
   /following-sibling::a
      [not(/*/h1[4])
     or
      count(.|/*/h1[4]/preceding-sibling::a)
      =
       count(/*/h1[4]/preceding-sibling::a)
      ]

如果:

count(/*/h1)

$cnt,

生成$cnt 这样的表达式(用于i = 1 to $cnt)并评估所有这些表达式。每个节点选择的节点要么包含a 元素,要么不包含。如果$k-th 组(从评估$k-th 表达式中选择的节点)包含a,则使用其字符串值生成所需数组的$k-th 项——否则生成null对于想要的数组的$k-th 项。

这是对上述 XPath 表达式的基于 XSLT 的验证

<xsl:stylesheet version="1.0"
 xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
 <xsl:output omit-xml-declaration="yes" indent="yes"/>

 <xsl:template match="/">
   <xsl:variable name="vGroup1" select=
   "/*/h1[1]
       /following-sibling::a
          [not(/*/h1[2])
         or
           count(.|/*/h1[2]/preceding-sibling::a)
          =
           count(/*/h1[2]/preceding-sibling::a)
          ]
   "/>

   <xsl:variable name="vGroup2" select=
   "/*/h1[2]
       /following-sibling::a
          [not(/*/h1[3])
         or
           count(.|/*/h1[3]/preceding-sibling::a)
          =
           count(/*/h1[3]/preceding-sibling::a)
          ]
   "/>

   <xsl:variable name="vGroup3" select=
   "/*/h1[3]
       /following-sibling::a
          [not(/*/h1[4])
         or
          count(.|/*/h1[4]/preceding-sibling::a)
          =
           count(/*/h1[4]/preceding-sibling::a)
          ]
   "/>

 Group1:  "<xsl:copy-of select="$vGroup1"/>"

 Group2:  "<xsl:copy-of select="$vGroup2"/>"

 Group3:  "<xsl:copy-of select="$vGroup3"/>"

 </xsl:template>
</xsl:stylesheet>

当此转换应用于以下 XML 文档时(OP 未提供完整且格式正确的 XML 文档!!!):

<t>
    <h1>heading1</h1>
    <a>a1</a>
    <b>b1</b>

    <h1>heading2</h1>
    <b>b2</b>

    <h1>heading3</h1>
    <a>a3</a>
    <b>b3</b>
    <c>c3</c>
</t>

对三个 XPath 表达式求值并输出它们各自选择的节点

 Group1:  "<a>a1</a>"

 Group2:  ""

 Group3:  "<a>a3</a>"

说明

我们对两个节点集的交集使用众所周知的凯叶斯公式

$ns1[count(. | $ns2) = count($ns2)]

计算这个表达式的结果正好包含同时属于节点集$ns1节点集$ns2的节点。

剩下的就是用与问题相关的表达式替换$ns1$ns2

我们将$ns1 替换为:

/*/h1[1]
    /following-sibling::a

我们将$ns2 替换为:

/*/h1[2]
    /preceding-sibling::a

换句话说,在第一个和第二个/*/h1 之间的a 元素是/*/h1[1] 的兄弟元素之后的a 元素和/*/h1[1] 的兄弟元素之前的a 元素的交集/*/h1[2].

此表达式仅对/*/h1 元素的最后一个之后的a 元素有问题。这就是为什么我们添加一个额外的谓词,它使用以下布尔表达式检查下一个 /*/h1 元素和 or 是否不存在。

最后,作为 Java 实现的指导性示例,这里是一个完整的 XSLT 转换,它执行类似的操作——生成一个序列化数组,并且可以机械地转换为相应的 Java 解决方案

<xsl:stylesheet version="1.0"
         xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
         xmlns:my="my:my">
         <xsl:output method="text"/>

         <my:null>null</my:null>
         <my:Q>"</my:Q>

         <xsl:variable name="vNull" select="document('')/*/my:null"/>
         <xsl:variable name="vQ" select="document('')/*/my:Q"/>

         <xsl:template match="/">
           <xsl:variable name="vGroup1" select=
           "/*/h1[1]
               /following-sibling::a
                  [not(/*/h1[2])
                 or
                   count(.|/*/h1[2]/preceding-sibling::a)
                  =
                   count(/*/h1[2]/preceding-sibling::a)
                  ]
           "/>

           <xsl:variable name="vGroup2" select=
           "/*/h1[2]
               /following-sibling::a
                  [not(/*/h1[3])
                 or
                   count(.|/*/h1[3]/preceding-sibling::a)
                  =
                   count(/*/h1[3]/preceding-sibling::a)
                  ]
           "/>

           <xsl:variable name="vGroup3" select=
           "/*/h1[3]
               /following-sibling::a
                  [not(/*/h1[4])
                 or
                  count(.|/*/h1[4]/preceding-sibling::a)
                  =
                   count(/*/h1[4]/preceding-sibling::a)
                  ]
           "/>

         [<xsl:value-of select=
          "concat($vQ[$vGroup1/self::a[1]],
                  $vGroup1/self::a[1],
                  $vQ[$vGroup1/self::a[1]],
                  $vNull[not($vGroup1/self::a[1])])"/>
          <xsl:text>,</xsl:text>

         <xsl:value-of select=
          "concat($vQ[$vGroup2/self::a[1]],
                  $vGroup2/self::a[1],
                  $vQ[$vGroup2/self::a[1]],
                  $vNull[not($vGroup2/self::a[1])])"/>
          <xsl:text>,</xsl:text>

         <xsl:value-of select=
          "concat($vQ[$vGroup3/self::a[1]],
                  $vGroup3/self::a[1],
                  $vQ[$vGroup3/self::a[1]],
                  $vNull[not($vGroup3/self::a[1])])"/>]
         </xsl:template>
</xsl:stylesheet>

当此转换应用于同一个 XML 文档(如上)时,会产生所需的正确结果

     ["a1",null,"a3"]

更新2

现在 OP 已添加他可以使用 XSLT 解决方案。这是一个:

<xsl:stylesheet version="1.0"
         xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
         xmlns:my="my:my" exclude-result-prefixes="xsl">
         <xsl:output omit-xml-declaration="yes" indent="yes"/>

         <xsl:key name="kFollowing" match="a"
              use="generate-id(preceding-sibling::h1[1])"/>

         <my:null/>
         <xsl:variable name="vNull" select="document('')/*/my:null"/>

         <xsl:template match="/*">
           <xsl:copy-of select=
           "h1/following-sibling::a[1]
          |
            h1[not(key('kFollowing', generate-id()))]"/>

           =============================================

           <xsl:apply-templates select="h1"/>

         </xsl:template>

         <xsl:template match="h1">
           <xsl:variable name="vAsInGroup" select=
               "key('kFollowing', generate-id())"/>
           <xsl:copy-of select="$vAsInGroup[1] | $vNull[not($vAsInGroup)]"/>
         </xsl:template>
</xsl:stylesheet>

这种转换实现了两种不同的解决方案。不同之处在于使用什么元素来表示“null”。在第一种情况下,它是 h1 元素。不建议这样做,因为任何 h1 已经有自己的含义,不同于“表示 null”。第二种解决方案使用特殊的my:null 元素来表示null。

当此转换应用于与上述相同的 XML 文档时

<t>
        <h1>heading1</h1>
        <a>a1</a>
        <b>b1</b>

        <h1>heading2</h1>
        <b>b2</b>

        <h1>heading3</h1>
        <a>a3</a>
        <b>b3</b>
        <c>c3</c>
</t>

对两个 XPath 表达式(包含 XSLT key() 引用)中的每一个进行求值并输出选定的节点(分别位于“=======”上方和下方):

<a>a1</a>
<h1>heading2</h1>
<a>a3</a>

           =============================================

           <a>a1</a>
<my:null xmlns:my="my:my" xmlns:xsl="http://www.w3.org/1999/XSL/Transform"/>
<a>a3</a>

性能说明

因为使用了键,所以当进行多次搜索时,此解决方案的效率会显着提高——例如,当需要生成abc 的对应数组时。

【讨论】:

@DimitreNovaatchev,这是一个非常详细的答案,但它没有回答问题的第一部分。我将问题更改为接受 XSLT 解决方案。在这里没有人反对你 Dimitre,很明显你知道很多,你有我的尊重,但也许是出于某种原因,无论是有意还是无意,这种轻微的居高临下/被动的攻击性确实会降低你答案的质量,我从不质疑他们,但你的态度有时肯定会以错误的方式消失,但这一切都很好,当时我添加了第二部分,在我看来这是个好主意。 Kim Jong Woo:恕我直言,从你的问题中仍然不清楚到底想要什么。忘记人们需要猜测真正想要什么的时间,但如果问题被明确地陈述,你将不会得到最好的答案。事实上,像基里尔这样的优秀专家已经删除了他的答案并且再也没有回来——想想这个,这可能意味着什么。 仍然没有解决问题的第一部分。 金钟宇:第一部分是什么?您从第一部分恢复,导致两个人删除了他们的好答案,现在要求“第一部分”???什么是“问题”?你从来没有明确地解释过这一点。 删除大部分对话,@Flynn,Dimitre。请不要使用 cmets 讨论投票。选民信息是私密的是有原因的,而这种无用的对话是其中很大一部分原因。【参考方案3】:

我建议您使用以下内容,它可能会被重写为 xsl:function,其中父节点名称(此处:div)已参数化。

<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet version="2.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="xml" version="1.0" encoding="UTF-8" indent="yes"/>

<xsl:template match="/">
    <root>
        <aList><xsl:copy-of select="$divIncludingNulls//a"/></aList>
        <bList><xsl:copy-of select="$divIncludingNulls//b"/></bList>
        <cList><xsl:copy-of select="$divIncludingNulls//c"/></cList>
    </root>
</xsl:template>

<xsl:variable name="divChild" select="distinct-values(//div/*/name())"/>

<xsl:variable name="divIncludingNulls">
    <xsl:for-each select="//div">
        <xsl:variable name="divElt" select="."/>
        <div>
            <xsl:for-each select="$divChild">
                <xsl:variable name="divEltvalue" select="$divElt/*[name()=current()]"/>
                <xsl:element name=".">
                    <xsl:choose>
                        <xsl:when test="$divEltvalue"><xsl:value-of select="$divEltvalue"/></xsl:when>
                        <xsl:otherwise>null</xsl:otherwise>
                    </xsl:choose>
                </xsl:element>
            </xsl:for-each>
       </div>
    </xsl:for-each>
</xsl:variable>

</xsl:stylesheet>

适用于

<?xml version="1.0" encoding="UTF-8"?>
<root>
    <div>
     <a>a1</a>
     <b>b1</b>
    </div>

    <div>
     <b>b2</b>
    </div>

    <div>
     <a>a3</a>
     <b>b3</b>
     <c>c3</c>
    </div>
</root>

输出是

<?xml version="1.0" encoding="UTF-8"?>
<root>
    <aList>
        <a>a1</a>
        <a>null</a>
        <a>a3</a>
    </aList>
    <bList>
        <b>b1</b>
        <b>b2</b>
        <b>b3</b>
    </bList>
    <cList>
        <c>null</c>
        <c>null</c>
        <c>c3</c>
    </cList>
</root>

【讨论】:

以上是关于如何在 XSLT 中使用 XPath 获取元素数组,包括缺失的元素?的主要内容,如果未能解决你的问题,请参考以下文章

XML-XSLT-XPATH:如何获取重复记录?

XPath语法

使用 XSLT 打印元素和属性的 xpath 和值

如何将 xslt 中的参数用作 XPath?

XPath 相关内容

如何使用 C# 将 XSLT 更改为使用动态 XPath?