如何使用XSLT对值进行排序?

3

我有一个类似于这样的XML:

<Test>
  <grapes>
     <a>TypeA</a>
     <b>value1</b>
  </grapes>
  <oranges>
     <a>TypeB</a>
     <b>value2</b>
  </oranges>
  <apples>
    <a>TypeA</a>
     <b>value3</b>
  </apples>
</Test>

这里的值是唯一的,但类型可能相同。

我正在尝试对其进行排序,以便输出类似于以下内容:

<group type="TypeA">
  <value v="value1" />
  <value v="value3" />
</group>
<group type="TypeB">
  <value v="value2" />
</group>

我很难确保输出的组是唯一的,并且值在正确的组中。我的XSL应该如何结构化?

1
由于标题中没有看到分组,我想澄清一下:您想先分组,然后再排序吗? - Brian
非常有用的问题(以及很棒的答案)。我同意标题应该重新表述,因为这是一个常见的问题,如果他们能更好地发现这个问题是他们的,那么答案对许多人来说都会很有趣。 - Alain BECKER
3个回答

2

这里有一个更简单的解决方案(完全是“推式”方法,没有 <xsl:for-each>,没有嵌套,没有 <xsl:variable>,没有 current(),没有 //,没有轴):

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

 <xsl:key name="kGoodsByType" match="/*/*" use="a"/>

 <xsl:template match=
  "/*/*[generate-id()
       =
        generate-id(key('kGoodsByType', a)[1])
       ]
  ">
     <group type="{a}">
       <xsl:apply-templates select="key('kGoodsByType', a)/b"/>
     </group>
 </xsl:template>

 <xsl:template match="b">
  <value v="{.}"/>
 </xsl:template>

 <xsl:template match="*/* | text()" priority="-1"/>
</xsl:stylesheet>

当应用于所提供的XML文档时,此转换会发生以下情况

<Test>
    <grapes>
        <a>TypeA</a>
        <b>value1</b>
    </grapes>
    <oranges>
        <a>TypeB</a>
        <b>value2</b>
    </oranges>
    <apples>
        <a>TypeA</a>
        <b>value3</b>
    </apples>
</Test>

期望的、正确的结果已经生成:

<group type="TypeA">
   <value v="value1"/>
   <value v="value3"/>
</group>
<group type="TypeB">
   <value v="value2"/>
</group>

解释:使用a子节点的字符串值作为键,对/*/*进行Muenchian分组

II. XSLT 2.0解决方案

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

 <xsl:template match="/*">
  <xsl:for-each-group select="*/a" group-by=".">
   <group type="{current-grouping-key()}">
     <xsl:sequence select="current-group()/../b"/>
   </group>
  </xsl:for-each-group>
 </xsl:template>
</xsl:stylesheet>

当对同一XML文档(如上所示)进行此转换时,将产生相同的正确结果:

<group type="TypeA">
   <b>value1</b>
   <b>value3</b>
</group>
<group type="TypeB">
   <b>value2</b>
</group>

解释:

  1. <xsl:for-each-group>

    用于根据指定的分组条件对节点集合进行分组,然后在每个分组上执行相应的操作。

  2. current-group()

    返回当前分组中的所有节点。

  3. current-grouping-key()

    返回当前分组使用的分组键。


1

XSLT 1.0:

首先,您需要使用Muenchian方法为类型创建唯一的分组。请搜索谷歌以了解该方法。然后,只需通过迭代它们并按照所需的方式打印出来即可:

<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="1.0">
<xsl:output method="xml" indent="yes"/>
<xsl:key name="types" match="a" use="text()"/>
<xsl:template match="/">
<result>
  <xsl:for-each select="//a[generate-id(.) = generate-id(key('types', text())[1])]">
    <group type="{current()/text()}">
      <xsl:for-each select="//a[text() = current()/text()]">
        <xsl:variable name="values" select="following-sibling::b | preceding-sibling::b"/>
        <xsl:for-each select="$values">
          <value v="{current()}"/>
        </xsl:for-each>
      </xsl:for-each>
    </group>
  </xsl:for-each>
</result>
</xsl:template>
</xsl:stylesheet>

你会发现输出结果与你预期的完全相同。


1
请确保您理解这里所写的内容。如果您有任何问题,请随时提问。 - FailedDev
你能解释一下这里到底发生了什么吗? //a[generate-id(.) = generate-id(key('types', text())[1])] - deken
基本上,您根据a元素的文本值生成唯一的id,并进行迭代。 - FailedDev
啊,好的,还有一件事,为什么需要这个: <xsl:for-each select="following-sibling::b | preceding-sibling::b"> 如果你已经循环遍历了与组名匹配的每一个。 - deken
因为一旦我拥有了所需的组,我需要获取所有兄弟节点并对它们进行迭代。在这种情况下,你的兄弟节点名字是 b,但我不知道例如 b 是否总是紧跟在 a 之后或其前面,因此此轴仅选择了在 a 元素之后和之前的所有 b 元素。 - FailedDev

0

我的提议与FailedDev的略有不同:

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

  <xsl:template match="/Test">
    <root>
      <xsl:for-each select="*[a != following::a]">
        <xsl:sort select="a" data-type="text" />
        <group type="{a}">
          <xsl:for-each select="/Test/*[a = current()/a]">
            <xsl:sort select="b" data-type="text" />
            <value v="{b}" />
          </xsl:for-each>
        </group>
      </xsl:for-each>
    </root>
  </xsl:template>
</xsl:stylesheet>

外部的

<xsl:for-each select="*[a != following::a]" />

选择所有唯一的类型,即 TypeATypeB

<xsl:sort select="a" data-type="text" />

按名称对它们进行排序,确保TypeA出现在TypeB之上。内部

<xsl:for-each select="/Test/*[a = current()/a]" />

为每种类型选择一组唯一的值,例如对于TypeA,提取出值value1value3。然后,将结果列表按照value1value3之前的顺序进行排序。


非常错误--当操作数之一是节点集时,不要使用!=运算符。此外,在发布之前,请始终运行/测试您的解决方案! - Dimitre Novatchev
这个经过测试,似乎没有问题。 虽然我同意你使用Muenchian分组的解决方案可能是正确的选择。 - Malibak
2
@_Mace:你的答案是错误的——你只是幸运地在提供的XML文档上“成功”了。用这个XML文档试试,看看它是否为“TypeA”生成两个组<Test> <grapes> <a>TypeA</a> <b>value1</b> </grapes> <oranges> <a>TypeB</a> <b>value2</b> </oranges> <apples> <a>TypeA</a> <b>value3</b> </apples> <peaches> <a>TypeB</a> <b>value4</b> </peaches> </Test> - Dimitre Novatchev

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