Grouping xml nodes by value of a child in Xsl

2020-05-23 04:17发布

问题:

<root>
<element>
<id>1</id>
<group>first</group>
</element>

<element>
<id>2</id>
<group>second</group>
</element>


<element>
<id>3</id>
<group>first</group>
</element>
...
<root>

How I can group my elements by the group name in xslt 1.0. the output:

<root>
<group name="first">
 <element>
    <id>1</id>
    <group>first</group>
 </element>
 <element>
    <id>3</id>
    <group>first</group>
 </element>
</group>
<group name="second">
 <element>
    <id>2</id>
    <group>second</group>
    </element>
</group>
</root>

Any ideas?

回答1:

This is a job for Muenchian Grouping. You will numerous examples of it within the XSLT tag here on StackOverflow.

First, you need to define a key to help you group the groups

<xsl:key name="groups" match="group" use="."/>

This will look up group elements for a given group name.

Next, you need to match all the occurrences of the first instance of each distince group name. This is done with this scary looking statement

<xsl:apply-templates select="element/group[generate-id() = generate-id(key('groups', .)[1])]"/>

i.e Match group elements which happen to be the first occurence of that element in our key.

When you have matched the distinct group nodes, you can then loop through all other group nodes with the same name (where $currentGroup is a variable holding the current group name)

<xsl:for-each select="key('groups', $currentGroup)">

Putting this altogether gives

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

   <xsl:key name="groups" match="group" use="."/>

   <xsl:template match="/root">
      <root>
         <xsl:apply-templates select="element/group[generate-id() = generate-id(key('groups', .)[1])]"/>
      </root>
   </xsl:template>

   <xsl:template match="group">
      <xsl:variable name="currentGroup" select="."/>
      <group>
         <xsl:attribute name="name">
            <xsl:value-of select="$currentGroup"/>
         </xsl:attribute>
         <xsl:for-each select="key('groups', $currentGroup)">
            <element>
               <id>
                  <xsl:value-of select="../id"/>
               </id>
               <name>
                  <xsl:value-of select="$currentGroup"/>
               </name>
            </element>
         </xsl:for-each>
      </group>
   </xsl:template>

</xsl:stylesheet>

Applying this on your sample XML gives the following result

<root>
   <group name="first">
      <element>
         <id>1</id>
         <name>first</name>
      </element>
      <element>
         <id>3</id>
         <name>first</name>
      </element>
   </group>
   <group name="seccond">
      <element>
         <id>2</id>
         <name>seccond</name>
      </element>
   </group>
</root>


回答2:

I. Here is a complete and very short XSLT 1.0 solution:

<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="kElsByGroup" match="element" use="group"/>

 <xsl:template match="/*">
  <root>
   <xsl:apply-templates/>
  </root>
 </xsl:template>

 <xsl:template match=
   "element[generate-id()=generate-id(key('kElsByGroup',group)[1])]">

  <group name="{group}">
   <xsl:copy-of select="key('kElsByGroup',group)"/>
  </group>
 </xsl:template>

 <xsl:template match=
   "element[not(generate-id()=generate-id(key('kElsByGroup',group)[1]))]"/>

</xsl:stylesheet>

when this transformation is applied on the provided XML document:

<root>
    <element>
        <id>1</id>
        <group>first</group>
    </element>
    <element>
        <id>2</id>
        <group>second</group>
    </element>
    <element>
        <id>3</id>
        <group>first</group>
    </element>
</root>

the wanted, correct result is produced:

<root>
    <group name="first"><element>
        <id>1</id>
        <group>first</group>
    </element><element>
        <id>3</id>
        <group>first</group>
    </element></group>
    <group name="second"><element>
        <id>2</id>
        <group>second</group>
    </element></group>
</root>

Do note:

  1. The use of the Muenchian method for grouping. This is the most efficient grouping method in XSLT 1.0.

  2. The use of AVT (Attribute Value Template) to specify a literal result element and its variable - value attribute as one whole. Using AVTs simplifies coding and yields shorter and more understandable code.

II. An even shorter XSLT 2.0 solution:

<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="/*">
     <root>
      <xsl:for-each-group select="element" group-by="group">
       <group name="{current-grouping-key()}">
        <xsl:copy-of select="current-group()"/>
       </group>
      </xsl:for-each-group>
     </root>
 </xsl:template>
</xsl:stylesheet>

when this transformation is applied on the same XML document (above), the same correct result is again produced.

Do note:

.1. The use of the <xsl:for-each-group> XSLT 2.0 instruction.

.2. The use of the standard XSLT 2.0 functions current-group() and current-grouping-key()



回答3:

<xsl:template match="group[not(.=preceding::group)]">
  <xsl:variable name="current-group" select="." />
  <xsl:for-each select="//root/element[group=$current-group]">
    <group>
      <id><xsl:value-of select="id"/></id>
    </group>
  </xsl:for-each>
</xsl:template>


标签: xml xslt xpath