5

我有这样的 XML:

<span>1</span>
<span class="x">2</span>
<span class="x y">3</span>
<span class="x">4</span>
<span>5</span>
<span class="x">6</span>
<span>7</span>
<span class="x">8</span>

我想要的是使用 XSLT 样式表将其class属性包含的所有元素的内容x放入一个<x>元素中。所以输出应该是这样的:

1 <x>234</x> 5 <x>6</x> 7 <x>8</x>

(或者,理想情况下,

1 <x>2<y>3</y>4</x> 5 <x>6</x> 7 <x>8</x>

但是当我解决了这个问题时,这是一个需要解决的问题。)

这是我的 XSLT 的相关片段:

<xsl:template match="span[contains(@class,'x') and preceding-sibling::span[1][not(contains(@class,'x'))]]">
  <x><xsl:for-each select=". | following-sibling::span[contains(@class,'x')]">
    <xsl:value-of select="text()"/>
  </xsl:for-each></x>
</xsl:template>

<xsl:template match="span[contains(@class,'x') and preceding-sibling::span[1][contains(@class,'x')]]">
</xsl:template>

<xsl:template match="span">
  <xsl:value-of select="text()"/>
</xsl:template>

这产生的是:

1 <x>23468</x> 5 <x>68</x> 7 <x>8</x>

我很确定我必须在 XPath 表达式中使用计数,这样它就不会选择以下所有具有类 x 的元素,而只会选择连续的元素。但是我如何计算连续的?还是我这样做是错误的?

4

3 回答 3

8

这很棘手,但可行(请提前阅读,对此感到抱歉)。

就 XPath 轴(根据定义不连续)而言,“连续性”的关键是检查“首先满足条件”的相反方向上最近的节点是否也是“开始”手头系列的节点:

一种
b <- 第一个满足条件的节点,开始系列 1
b <- 系列 1
b <- 系列 1
一种
b <- 第一个满足条件的节点,开始系列 2
b <- 系列 2
b <- 系列 2
一种

在您的情况下,一个系列由具有以下字符串的节点<span>组成:x@class

span[contains(concat(' ', @class, ' '),' x ')] 

请注意,我连接空格以避免误报。

A<span>开始一个系列(即“首先满足条件”)可以定义为一个x在其类中具有 a 并且不直接位于另一个<span>也具有 的之前x

not(preceding-sibling::span[1][contains(concat(' ', @class, ' '),' x ')])

我们必须检查这个条件<xsl:if>以避免模板为一系列节点生成输出(即,模板将只为“起始节点”执行实际工作)。

现在到棘手的部分。

从这些“起始节点”中的每一个中,我们必须选择在其类following-sibling::span中具有 的所有节点。x还包括当前span以解释只有一个元素的系列。好的,很简单:

. | following-sibling::span[contains(concat(' ', @class, ' '),' x ')]

对于其中的每一个,我们现在找出它们最接近的“起始节点”是否与模板正在处理的那个相同(即开始他们的系列)。这表示:

  • 它们必须是系列的一部分(即它们必须跟在 aspan后面x

    preceding-sibling::span[1][contains(concat(' ', @class, ' '),' x ')]
    
  • 现在删除任何span启动节点与当前系列启动节点不同的节点。这意味着我们检查任何本身不直接以 a 和 a 开头的前兄弟span(具有 a ) :xspanx

    preceding-sibling::span[contains(concat(' ', @class, ' '),' x ')][
      not(preceding-sibling::span[1][contains(concat(' ', @class, ' '),' x ')])
    ][1]
    
  • 然后我们generate-id()用来检查节点身份。如果找到的节点与 相同$starter,则当前跨度是属于连续序列的跨度。

把它们放在一起:

<xsl:template match="span[contains(concat(' ', @class, ' '),' x ')]">
  <xsl:if test="not(preceding-sibling::span[1][contains(concat(' ', @class, ' '),' x ')])">
    <xsl:variable name="starter" select="." />
    <x>
      <xsl:for-each select="
        . | following-sibling::span[contains(concat(' ', @class, ' '),' x ')][
          preceding-sibling::span[1][contains(concat(' ', @class, ' '),' x ')]
          and
          generate-id($starter)
          =
          generate-id(
            preceding-sibling::span[contains(concat(' ', @class, ' '),' x ')][
              not(preceding-sibling::span[1][contains(concat(' ', @class, ' '),' x ')])
            ][1]
          )
        ]
      ">
        <xsl:value-of select="text()" />
      </xsl:for-each>
    </x>
  </xsl:if>
</xsl:template>

是的,我知道它不漂亮。有一个<xsl:key>更有效的基于解决方案,Dimitre 的回答表明了这一点。

使用您的示例输入,将生成以下输出:

1
<x>234</x>
5
<x>6</x>
7
<x>8</x>
于 2012-01-22T17:00:51.693 回答
5

一、XSLT 解决方案

我想要的是使用 XSLT 样式表将其类属性包含的所有元素的内容x放入一个<x>元素中。所以输出应该是这样的:

1 <x>234</x> 5 <x>6</x> 7 <x>8</x>

这种转变

 <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:key name="kFollowing" match=
  "span[contains(concat(' ', @class, ' '),
                 ' x ')
        ]"
   use="generate-id(preceding-sibling::span
                                    [not(contains(concat(' ', @class, ' '),
                                             ' x '))
                                    ][1]
                    )
        "/>

 <xsl:template match=
 "span[contains(concat(' ', @class, ' '), ' x ')
     and
       not(contains(concat(' ', preceding-sibling::span[1]/@class, ' '),
                    ' x '
                    )
           )
      ]"
  >
     <x>
       <xsl:apply-templates mode="inGroup" select=
       "key('kFollowing',
             generate-id(preceding-sibling::span                                                           [not(contains(concat(' ', @class, ' '),                                                       ' x ')
                                 )
                            ][1]
                        )
            )
      "/>
     </x>
 </xsl:template>

 <xsl:template match=
 "span[contains(concat(' ', @class, ' '), ' x ')
     and
       contains(concat(' ', preceding-sibling::span[1]/@class, ' '),
                    ' x '
                    )
      ]
  "/>
</xsl:stylesheet>

当应用于提供的 XML 文档时(包装到单个顶部元素html中以形成良好的格式):

<html>
    <span>1</span>
    <span class="x">2</span>
    <span class="x y">3</span>
    <span class="x">4</span>
    <span>5</span>
    <span class="x">6</span>
    <span>7</span>
    <span class="x">8</span>
</html>

产生想要的正确结果

1<x>234</x>5<x>6</x>7<x>8</x>

然后是“理想”的添加

或者,理想情况下,

1 <x>2<y>3</y>4</x> 5 <x>6</x> 7 <x>8</x> 

但是当我解决了这个问题时,这是一个需要解决的问题。)

只需将此模板添加到上述解决方案中

  <xsl:template mode="inGroup" match=
    "span[contains(concat(' ', @class, ' '),
                   ' y '
                   )
         ]">
    <y><xsl:value-of select="."/></y>
  </xsl:template>

当将如此修改的解决方案应用于同一个 XML 文档时,再次产生了(新的)想要的结果

1<x>2<y>3</y>4</x>5<x>6</x>7<x>8</x>

二、XSLT 2.0 解决方案

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

 <xsl:template match="/*">
         <xsl:for-each-group select="span" group-adjacent=
          "contains(concat(' ',@class,' '), ' x ')">

           <xsl:sequence select=
           "if(current-grouping-key())
              then
                my:formatGroup(current-group())
              else
                data(current-group())
           "/>
         </xsl:for-each-group>
 </xsl:template>

 <xsl:function name="my:formatGroup" as="node()*">
  <xsl:param name="pGroup" as="node()*"/>

  <x>
   <xsl:apply-templates select="$pGroup"/>
  </x>
 </xsl:function>

 <xsl:template match=
   "span[contains(concat(' ',@class, ' '), ' y ')]">
  <y><xsl:apply-templates/></y>
 </xsl:template>
</xsl:stylesheet>

当这个 XSLT 2.0 转换应用于同一个 XML 文档(上图)时,就会产生想要的“理想”结果

1<x>2<y>3</y>4</x>5<x>6</x>7<x>8</x>
于 2012-01-22T16:43:54.353 回答
2

感谢您的解决方案。与此同时,我设法使用完全不同的策略组合了一些东西。我只是为这个项目学习 XSLT,我读过的最有用的东西是 XSLT 就像函数式编程。所以我用递归写了一些东西,在被这个指向正确的方向之后:

<xsl:template match="span[
                       contains(@class,'x')
                       and
                       preceding-sibling::span[1][
                         not(contains(@class,'x'))
                       ]
                     ]">
  <x><xsl:value-of select="text()"/>
    <xsl:call-template name="continue">
      <xsl:with-param name="next" select="following-sibling::span[1]"/>
    </xsl:call-template>
  </x>
</xsl:template>

<xsl:template name="continue">
  <xsl:param name="next"/>
  <xsl:choose>
    <xsl:when test="$next[contains(@class,'x')]">
      <xsl:apply-templates mode="x" select="$next"/>
      <xsl:call-template name="continue">
        <xsl:with-param name="next" select="$next/following-sibling::span[1]"/>
      </xsl:call-template>
    </xsl:when>
    <xsl:otherwise/><!-- Do nothing -->
  </xsl:choose>
</xsl:template>

<xsl:template match="span[
                       contains(@class,'x')
                       and
                       preceding-sibling::span[1][
                         contains(@class,'x')
                       ]
                     ]"/>

<xsl:template match="span">
  <xsl:value-of select="text()"/>
</xsl:template>

<xsl:template mode="x" match="span[contains(@class,'y')]">
  <y><xsl:value-of select="text()"/></y>
</xsl:template>

<xsl:template mode="x" match="span">
  <xsl:value-of select="text()"/>
</xsl:template>

我不知道这是否比使用generate-id()或键更有效,但我确实从您的解决方案中学到了一些东西!

于 2012-01-22T18:21:20.663 回答