xslt按每个属性分组

时间:2012-07-09 21:52:42

标签: xml xslt composite-key xslt-grouping

我有多种类型的xml消息我需要通过在同一父节点下对多个节点进行分组来“压缩”(相同的父节点意味着它们共享相同的节点名称,并且声明的每个属性也相等)。例如:

<TopLevel CodeTL="Something">
    <Ratings>
          <Rating CodeA="ABC" Start="1-1-2012" End="1-2-2012">
              <RatingByNumber Code="X" Rating="10" Number="1">
              <RatingByNumber Code="X" Rating="19" Number="2">
          </Rating>
    </Ratings>
</TopLevel>
    <TopLevel CodeTL="Something">
    <Ratings>
          <Rating CodeA="ABC" Start="1-2-2012" End="1-3-2012">
              <RatingByNumber Code="X" Rating="10" Number="1">
              <RatingByNumber Code="X" Rating="19" Number="2">
          </Rating>
    </Ratings>
</TopLevel>
<TopLevel CodeTL="Something">
    <Ratings>
          <Rating CodeA="XYZ" Start="1-2-2012" End="1-3-2012">
              <RatingByNumber Code="X" Rating="10" Number="1">
              <RatingByNumber Code="X" Rating="19" Number="2">
          </Rating>
    </Ratings>
</TopLevel>
<TopLevel CodeTL="Something">
    <Ratings>
          <Rating CodeA="XYZ" Start="1-2-2012" End="1-3-2012">
              <RatingByNumber Code="X" Rating="30" Number="3">
              <RatingByNumber Code="X" Rating="39" Number="4">
          </Rating>
    </Ratings>
</TopLevel>

注意它们如何共享相同的CodeTL属性,后两个共享相同的CodeA,Start和End属性,所以我需要的是使用xslt生成以下输出

<TopLevel CodeTL="Something">
    <Ratings>
          <Rating CodeA="ABC" Start="1-1-2012" End="1-2-2012">
              <RatingByNumber Code="X" Rating="10" Number="1">
              <RatingByNumber Code="X" Rating="19" Number="2">
          </Rating>
          <Rating CodeA="ABC" Start="1-2-2012" End="1-3-2012">
              <RatingByNumber Code="X" Rating="10" Number="1">
              <RatingByNumber Code="X" Rating="19" Number="2">
          </Rating>
          <Rating CodeA="XYZ" Start="1-2-2012" End="1-3-2012">
              <RatingByNumber Code="X" Rating="10" Number="1">
              <RatingByNumber Code="X" Rating="19" Number="2">
              <RatingByNumber Code="X" Rating="30" Number="3">
              <RatingByNumber Code="X" Rating="39" Number="4">
          </Rating>
    </Ratings>
</TopLevel>

更干净,根据使用它的应用程序,它可以节省处理时间并节省空间。

我遇到的问题是我有不同类型的xml消息,它们具有不同的节点名称和属性(以及属性数量),但它们都共享我在此处显示的相同结构。 处理所有这些都是一种通用的方法,但我很感激XSLT可以转换我提供的示例,因此我可以为每个需要发送的xml消息创建自定义代码。

2 个答案:

答案 0 :(得分:1)

此XSLT 1.0样式表产生了所需的结果:

<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
    <xsl:key name="byCodeTL" match="TopLevel" use="@CodeTL"/>
    <xsl:key name="byAttrs" match="Rating" 
             use="concat(../../@CodeTL, '|', @CodeA, '|', @Start, '|', @End)"/>
    <xsl:template match="@*|node()">
        <xsl:copy>
            <xsl:apply-templates select="@*|node()"/>
        </xsl:copy>
    </xsl:template>
    <xsl:template match="TopLevel[generate-id()=
                                  generate-id(key('byCodeTL', @CodeTL)[1])]">
        <xsl:copy>
            <xsl:apply-templates select="@*"/>
            <Ratings>
                <xsl:apply-templates 
                        select="key('byCodeTL', @CodeTL)/Ratings/*"/>
            </Ratings>
        </xsl:copy>
    </xsl:template>
    <xsl:template match="Rating[generate-id()=
                                generate-id(key('byAttrs', 
            concat(../../@CodeTL, '|', @CodeA, '|', @Start, '|', @End))[1])]">
        <xsl:copy>
            <xsl:apply-templates select="@*|key('byAttrs', 
                concat(../../@CodeTL, '|', @CodeA, '|', @Start, '|', @End))/*"/>
        </xsl:copy>
    </xsl:template>
    <xsl:template match="TopLevel"/>
    <xsl:template match="Rating"/>
</xsl:stylesheet>

所有TopLevel元素都按其CodeTL属性进行分组。所有Rating元素都按其属性和相应CodeTL的{​​{1}}属性的组合进行分组。

答案 1 :(得分:1)

此通用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="xs my">
 <xsl:output omit-xml-declaration="yes" indent="yes"/>

 <xsl:template match="/*">
     <t>
       <xsl:sequence select="my:grouping(*)"/>
     </t>
 </xsl:template>

 <xsl:function name="my:grouping" as="node()*">
   <xsl:param name="pElems" as="element()*"/>

   <xsl:if test="$pElems">
       <xsl:for-each-group select="$pElems" group-by="my:signature(.)">
         <xsl:copy>
          <xsl:copy-of select="@*"/>

            <xsl:sequence select="my:grouping(current-group()/*)"/>
         </xsl:copy>
       </xsl:for-each-group>
   </xsl:if>
 </xsl:function>

 <xsl:function name="my:signature" as="xs:string">
  <xsl:param name="pElem" as="element()"/>

  <xsl:variable name="vsignAttribs" as="xs:string*">
      <xsl:for-each select="$pElem/@*">
       <xsl:sort select="name()"/>

       <xsl:value-of select="concat(name(), '=', .,'|')"/>
      </xsl:for-each>
  </xsl:variable>

  <xsl:sequence select=
  "concat(name($pElem), '|', string-join($vsignAttribs, ''))"/>
 </xsl:function>
</xsl:stylesheet>

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

<t>
    <TopLevel CodeTL="Something">
        <Ratings>
              <Rating CodeA="ABC" Start="1-1-2012" End="1-2-2012">
                  <RatingByNumber Code="X" Rating="10" Number="1"/>
                  <RatingByNumber Code="X" Rating="19" Number="2"/>
              </Rating>
        </Ratings>
    </TopLevel>
        <TopLevel CodeTL="Something">
        <Ratings>
              <Rating CodeA="ABC" Start="1-2-2012" End="1-3-2012">
                  <RatingByNumber Code="X" Rating="10" Number="1"/>
                  <RatingByNumber Code="X" Rating="19" Number="2"/>
              </Rating>
        </Ratings>
    </TopLevel>
    <TopLevel CodeTL="Something">
        <Ratings>
              <Rating CodeA="XYZ" Start="1-2-2012" End="1-3-2012">
                  <RatingByNumber Code="X" Rating="10" Number="1"/>
                  <RatingByNumber Code="X" Rating="19" Number="2"/>
              </Rating>
        </Ratings>
    </TopLevel>
    <TopLevel CodeTL="Something">
        <Ratings>
              <Rating CodeA="XYZ" Start="1-2-2012" End="1-3-2012">
                  <RatingByNumber Code="X" Rating="30" Number="3"/>
                  <RatingByNumber Code="X" Rating="39" Number="4"/>
              </Rating>
        </Ratings>
    </TopLevel>
</t>

生成想要的正确结果

<t>
   <TopLevel CodeTL="Something">
      <Ratings>
         <Rating CodeA="ABC" Start="1-1-2012" End="1-2-2012">
            <RatingByNumber Code="X" Rating="10" Number="1"/>
            <RatingByNumber Code="X" Rating="19" Number="2"/>
         </Rating>
         <Rating CodeA="ABC" Start="1-2-2012" End="1-3-2012">
            <RatingByNumber Code="X" Rating="10" Number="1"/>
            <RatingByNumber Code="X" Rating="19" Number="2"/>
         </Rating>
         <Rating CodeA="XYZ" Start="1-2-2012" End="1-3-2012">
            <RatingByNumber Code="X" Rating="10" Number="1"/>
            <RatingByNumber Code="X" Rating="19" Number="2"/>
            <RatingByNumber Code="X" Rating="30" Number="3"/>
            <RatingByNumber Code="X" Rating="39" Number="4"/>
         </Rating>
      </Ratings>
   </TopLevel>
</t>

<强>解释

  1. 执行的分组在函数my:grouping()中实现,并且是递归的。

  2. 顶级元素在其级别上是单个元素,不需要任何其他分组,而只需要自身的浅层副本。然后在这个浅层副本的主体内部,通过函数my:grouping()执行较低级别的分组。

  3. 函数my:grouping()有一个参数,它是直接上一级组中所有元素的所有子元素。它返回当前级别的所有组。

  4. 作为参数传递给函数的元素序列根据其签名进行分组 - 元素名称与其属性的所有名称 - 值对的串联它们的相应值,并使用适当的分隔符分隔。元素的签名由函数my:signature()生成。


  5. <强> II。通用XSLT 1.0解决方案:

    <xsl:stylesheet version="1.0"
             xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
             xmlns:ext="http://exslt.org/common"
             xmlns:my="my:my" exclude-result-prefixes="my ext">
             <xsl:output omit-xml-declaration="yes" indent="yes"/>
             <xsl:strip-space elements="*"/>
    
             <xsl:variable name="vrtfPass1">
              <xsl:apply-templates select="/*"/>
             </xsl:variable>
    
             <xsl:variable name="vPass1" select="ext:node-set($vrtfPass1)"/>
    
             <xsl:template match="/">
              <xsl:apply-templates select="$vPass1/*" mode="pass2"/>
             </xsl:template>
    
             <xsl:template match="/*" mode="pass2">
                 <xsl:copy>
                   <xsl:call-template name="my:grouping">
                    <xsl:with-param name="pElems" select="*"/>
                   </xsl:call-template>
                 </xsl:copy>
             </xsl:template>
    
             <xsl:template name="my:grouping">
               <xsl:param name="pElems" select="/.."/>
    
               <xsl:if test="$pElems">
                 <xsl:for-each select="$pElems">
                  <xsl:variable name="vPos" select="position()"/>
    
                  <xsl:if test=
                   "not(current()/@my:sign
                       = $pElems[not(position() >= $vPos)]/@my:sign
                       )">
    
                     <xsl:element name="{name()}">
                      <xsl:copy-of select="namespace::*[not(. = 'my:my')]"/>
                      <xsl:copy-of select="@*[not(name()='my:sign')]"/>
                       <xsl:call-template name="my:grouping">
                        <xsl:with-param name="pElems" select=
                        "$pElems[@my:sign = current()/@my:sign]/*"/>
                       </xsl:call-template>
                     </xsl:element>
                   </xsl:if>
    
                 </xsl:for-each>
               </xsl:if>
             </xsl:template>
    
         <xsl:template match="/*">
                 <xsl:copy>
                   <xsl:apply-templates/>
                 </xsl:copy>
         </xsl:template>
    
         <xsl:template match="*/*">
          <xsl:variable name="vSignature">
           <xsl:call-template name="signature"/>
          </xsl:variable>
          <xsl:copy>
           <xsl:copy-of select="@*"/>
           <xsl:attribute name="my:sign">
            <xsl:value-of select="$vSignature"/>
           </xsl:attribute>
    
           <xsl:apply-templates/>
          </xsl:copy>
         </xsl:template>
    
         <xsl:template name="signature">
           <xsl:variable name="vsignAttribs">
             <xsl:for-each select="@*">
              <xsl:sort select="name()"/>
    
                    <xsl:value-of select="concat(name(), '=', .,'|')"/>
                 </xsl:for-each>
            </xsl:variable>
    
            <xsl:value-of select=
              "concat(name(), '|', $vsignAttribs)"/>
         </xsl:template>
    </xsl:stylesheet>
    

    当在同一个XML文档(上面)上应用此转换时,会再次生成相同的正确结果

    <t>
       <TopLevel>
          <Ratings>
             <Rating CodeA="ABC" Start="1-1-2012" End="1-2-2012">
                <RatingByNumber Code="X" Rating="10" Number="1"/>
                <RatingByNumber Code="X" Rating="19" Number="2"/>
             </Rating>
             <Rating CodeA="ABC" Start="1-2-2012" End="1-3-2012">
                <RatingByNumber Code="X" Rating="10" Number="1"/>
                <RatingByNumber Code="X" Rating="19" Number="2"/>
             </Rating>
             <Rating CodeA="XYZ" Start="1-2-2012" End="1-3-2012">
                <RatingByNumber Code="X" Rating="10" Number="1"/>
                <RatingByNumber Code="X" Rating="19" Number="2"/>
                <RatingByNumber Code="X" Rating="30" Number="3"/>
                <RatingByNumber Code="X" Rating="39" Number="4"/>
             </Rating>
          </Ratings>
       </TopLevel>
    </t>
    

    <强>解释

    1. 这是两次转换。

    2. 在每个元素的第一遍中计算签名,它就成为新属性my:sign的valye。

    3. 使用与XSLT 2.0解决方案相同的递归分组算法。