xslt 1.0 合并空元素名称

xslt 1.0 consolidate empty element names

一个关于使用 xslt 1.0 的快速问题,您可能会帮助我 with.I 有一个如下所示的输入 xml

<Root>
    <FirstName>Bob</FirstName>
    <LastName>Marley</LastName>
    <ID>BM1234</ID>
    <Songs>
        <Song>
            <EmptyElements></EmptyElements>
            <SongName>No woman no cry</SongName>
            <Year>1974</Year>
            <album></album>
            <studio></studio>
            <rating></rating>
        </Song>
    </Songs>
</Root>

输出需要看起来像

<Root>
    <FirstName>Bob</FirstName>
    <LastName>Marley</LastName>
    <ID>BM1234</ID>
    <Songs>
        <Song>
            <EmptyElements>album, studio, rating</EmptyElements>
            <SongName>No woman no cry</SongName>
            <Year>1974</Year>
        </Song>
    </Songs>
</Root>

所以基本上是将所有空元素的逗号分隔列表放入 EmptyElements 标记中。

这个变换:

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

  <xsl:template match="node()|@*">
    <xsl:copy>
      <xsl:apply-templates select="node()|@*"/>
    </xsl:copy>
  </xsl:template>

  <xsl:template match="EmptyElements" priority="5">
    <xsl:copy>
      <xsl:apply-templates mode="enumerate" select=
      "../*[not(self::EmptyElements) and not(node())]" />
    </xsl:copy>
  </xsl:template>

  <xsl:template match="Songs/Song/*" mode="enumerate">
    <xsl:value-of select="substring(',', not(position() = 1), 1)"/>
    <xsl:value-of select="name()"/>
  </xsl:template>
  <xsl:template match="Songs/Song/*[not(node())]"/>
</xsl:stylesheet>

应用于提供的来源 XML 文档时:

<Root>
    <FirstName>Bob</FirstName>
    <LastName>Marley</LastName>
    <ID>BM1234</ID>
    <Songs>
        <Song>
            <EmptyElements></EmptyElements>
            <SongName>No woman no cry</SongName>
            <Year>1974</Year>
            <album></album>
            <studio></studio>
            <rating></rating>
        </Song>
    </Songs>
</Root>

产生想要的正确结果:

<Root>
   <FirstName>Bob</FirstName>
   <LastName>Marley</LastName>
   <ID>BM1234</ID>
   <Songs>
      <Song>
         <EmptyElements>album,studio,rating</EmptyElements>
         <SongName>No woman no cry</SongName>
         <Year>1974</Year>
      </Song>
   </Songs>
</Root>

解释:

  1. The identity rule, when selected for execution, copies the matched node "as-is"
  2. 匹配 Songs/Song/*[not(node())] 的模板在选择执行时不执行任何操作,这导致 "deleting"(不复制)输出中的匹配节点。
  3. 匹配EmptyElements的模板比上面提到的"deleting"模板指定的优先级高,所以它被选择在任何EmptyElements元素上执行。
  4. 将匹配的 EmptyElements 元素浅复制到输出,然后通过将模式 enumerate 中的模板应用于所有空兄弟元素来生成其内容(正文)。
  5. 最后,模式 enumerate 中的模板匹配作为 Songs 元素子元素的 Song 元素的任何子元素。它由上面步骤 4. 中的 <xsl:apply-templates> 指令选择执行,并且仅应用于 EmptyElements 元素的空元素兄弟。这个模板做了两件事:a) 如果这不是节点列表中的第一个节点,则输出一个逗号; b) 输出匹配元素的名称。这样,EmptyElements 元素的空兄弟元素的所有名称都被输出,以逗号分隔。

更新:

亲爱的reader, 我们有这个问题的配套答案,以 Or simply: 开头 并暗示它比这个答案中的代码更简单。

我没有告诉你这个答案比 Or simply: 答案简单,我总结了一些与简单性相关的事实,你可以自己得出结论。在下面的 table 中,每个左侧子列中的值都是针对当前解决方案的。每个右侧子列中的值用于 Or simply:-解决方案:

除此之外, Or simply: - 解决方案也有潜在的性能问题,以及一定的流动性问题 - - 查看此片段:

             <xsl:if test="position()!=last()">
                <xsl:text>, </xsl:text>
             </xsl:if>

与当前解决方案使用的比较:

not(position() = 1)

Dr. Michael Kay's recommendation,后者比前者"a much better way of coding this",他的解释是:

"Why? Because however hard the optimizer works, the last() function is hard work: it involves some kind of lookahead. With "position() ne last()" the lookahead might be limited to one element, but it's still a lot more complicated than testing whether the position is 1.

With streaming coming along, the latter formulation is also more likely to be streamable (because lookahead is impossible with streaming)."

结论: 每当有人告诉我们:“Or simply:”时,最好在将其声明视为理所当然之前采用一些指标...

或者简单地说:

XSLT 1.0

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

<!-- identity transform -->
<xsl:template match="@*|node()">
    <xsl:copy>
        <xsl:apply-templates select="@*|node()"/>
    </xsl:copy>
</xsl:template>

<xsl:template match="Song">
    <xsl:copy>
        <EmptyElements>
            <xsl:for-each select="*[not(node() or self::EmptyElements)]">
                 <xsl:value-of select="name()"/>
                 <xsl:if test="position()!=last()">
                    <xsl:text>, </xsl:text>
                 </xsl:if>
            </xsl:for-each> 
        </EmptyElements>
        <xsl:apply-templates select="*[node()]"/>
    </xsl:copy>
</xsl:template>

</xsl:stylesheet>

注:

这个解决方案自豪地使用了 last() 函数。没有与使用此功能相关的性能问题。

XPath 规范指出:

The last function returns a number equal to the context size from the expression evaluation context.

XSLT 规范告诉我们:

Expression evaluation occurs with respect to a context. ... The context consists of:

• a node (the context node)
• a pair of non-zero positive integers (the context position and the context size)
...

处理器将返回并为列表中的每个节点一次又一次地计算当前节点列表中的所有节点的想法简直是荒谬的。一旦建立了上下文(通过调用 xsl:for-eachxsl:apply-templates),上下文大小就已知并且不会改变。

这个结论也可以很容易地进行测试:使用 10k 项的列表,在评估时没有发现明显的差异:

<xsl:for-each select="item">
    <xsl:value-of select="position()!=last()"/>
</xsl:for-each>

反对:

<xsl:for-each select="item">
    <xsl:value-of select="not(position() = 1)"/>
</xsl:for-each>

(使用 libxslt、Xalan 和 Saxon 测试)。