Группировка элементов в преобразовании XML с помощью XSLT

Я борюсь с концепцией группировки (по нескольким ключам) табличного XML в иерархию с помощью XSLT.

Группировка основана на первых четырех элементах, однако группировка должна разорваться, если между набором есть другой элемент.

Исходный XML:

<RECORDS> 
<RECORD>
    <E1>MICKEY</E1>
    <E2>TEST</E2>
    <E4>14</E4>
    <E5>196</E5>
    <F1>A1</F1>
</RECORD>
<RECORD>
    <E1>MICKEY</E1>
    <E2>TEST</E2>
    <E4>14</E4>
    <E5>196</E5>
    <F1>A2</F1>
</RECORD>
 <RECORD>
    <E1>MICKEY</E1>
    <E2>TEST</E2>
    <E4>14</E4>
    <E5>195</E5>
    <F1>A3</F1>
  </RECORD>
 <RECORD>
    <E1>MICKEY</E1>
    <E2>TEST</E2>
    <E4>14</E4>
    <E5>196</E5>
    <F1>A4</F1>
  </RECORD>
 <RECORD>
    <E1>MICKEY</E1>
    <E2>TEST</E2>
    <E4>14</E4>
    <E5>196</E5>
    <F1>A5</F1>
  </RECORD>
     <RECORD>
    <E1>DONALD</E1>
    <E2>TEST</E2>
    <E4>14</E4>
    <E5>196</E5>
    <F1>A6</F1>
  </RECORD>
 <RECORD>
    <E1>DONALD</E1>
    <E2>TEST</E2>
    <E4>14</E4>
    <E5>196</E5>
    <F1>A7</F1>
  </RECORD>
 </RECORDS>

Выходной XML

 <RECORDS>
 <RECORD>
    <E1>MICKEY</E1>
    <E2>TEST</E2>
    <E4>14</E4>
    <E5>196</E5>
    <F>
     <F1>A1</F1>
     <F1>A2</F1>
    </F>
  </RECORD>
  <RECORD>
    <E1>MICKEY</E1>
    <E2>TEST</E2>
    <E4>14</E4>
    <E5>195</E5>
    <F>
     <F1>A3</F1>
     <F1>A4</F1>
    </F>
  </RECORD>
  <RECORD>
   <E1>MICKEY</E1> <!--Must break and not merge in first group -->
   <E2>TEST</E2>
   <E4>14</E4>
   <E5>196</E5>
   <F>   
   <F1>A5</F1>
   </F>
  </RECORD>
  <RECORD>
    <E1>DONALD</E1>
    <E2>TEST</E2>
    <E4>14</E4>
    <E5>196</E5>
    <F>
     <F1>A6</F1>
     <F1>A7</F1>
    </F>
  </RECORD>
 </RECORDS>

Вот XSL, который я придумал до сих пор ...

<?xml version="1.0"?>
<xsl:stylesheet
    xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="1.0">
<xsl:output method="xml" indent="yes"/>
 <xsl:key name="grouped" match="RECORD"
  use="concat(E1, '+', E2, '+', E4 , '+', E5 )"/>

<xsl:template match="node()|@*">
  <xsl:copy>
   <xsl:apply-templates select="node()|@*"/>
  </xsl:copy>
 </xsl:template>
 <xsl:template match="/*">
  <RECORDS>
   <xsl:apply-templates select=
   "RECORD[generate-id()
          =
           generate-id(key('grouped',
                        concat(E1, '+', E2, '+', E4 , '+', E5 )
                          )
                           [1]
                      )
           ]
   "/>
  </RECORDS>
 </xsl:template>
 <xsl:template match="RECORD">
   <RECORD>
  <E1><xsl:value-of select="E1"/></E1>
<E2><xsl:value-of select="E2"/></E2>
<E4><xsl:value-of select="E4"/></E4>
<F>
<xsl:for select="F1">
<F1><xsl:value-of select="F1"/></F1>
</xsl:for>

</F>
   </RECORD>

</xsl:template>
</xsl:stylesheet>

Проблема в том, что я не могу создать внутренний тег, повторяющийся для каждого f1. Также я должен получить 4 набора ЗАПИСЕЙ, а не 3, которые я получаю с этим.

<RECORDS>
  <RECORD>
    <E1>MICKEY</E1>
    <E2>TEST</E2>
    <E4>14</E4>
    <F></F>
  </RECORD>
  <RECORD>
    <E1>MICKEY</E1>
    <E2>TEST</E2>
    <E4>14</E4>
    <F></F>
  </RECORD>
  <RECORD>
    <E1>DONALD</E1>
    <E2>TEST</E2>
    <E4>14</E4>
    <F></F>
  </RECORD>
</RECORDS>

person ggonsalv    schedule 05.05.2016    source источник
comment
Укажите свою проблему - желательно опубликуйте свою попытку, чтобы мы могли ее исправить.   -  person michael.hor257k    schedule 05.05.2016


Ответы (2)


Вот решение с использованием ключей. Короче (на 28% меньше строк кода и без горизонтальной прокрутки). Более надежный (подробности см. в конце этого ответа)

Он более общий, потому что он будет работать даже в том случае, когда между элементами, которые мы хотим сгруппировать, есть другие элементы, которые необходимо игнорировать (то есть где preceding-sibling::*[1] может быть элементом, который мы хотим исключить из группировки - в текущая проблема - не элемент RECORD):

<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="kStartGroup" match="/*/*" use=
    "generate-id(preceding-sibling::*
      [not(concat(E1, '|', E2, '|', E4, '|', E5)
          = concat(current()/E1, '|', current()/E2, '|', current()/E4, '|', current()/E5)
          )
      ][1])"/>
  <xsl:template match="*[not(concat(E1, '|', E2, '|', E4, '|', E5) 
                            = 
                              concat(preceding-sibling::*[1]/E1, '|', 
                                     preceding-sibling::*[1]/E2, '|', 
                                     preceding-sibling::*[1]/E4, '|',
                                     preceding-sibling::*[1]/E5)
                             )]">
    <xsl:copy>
      <xsl:copy-of select="E1 | E2 | E4 | E5"/>
      <F><xsl:copy-of select=
                      "key('kStartGroup', generate-id(preceding-sibling::*[1]))/F1"/></F>
    </xsl:copy>
  </xsl:template>
  <xsl:template match="/*"><xsl:copy><xsl:apply-templates/></xsl:copy></xsl:template>      
  <xsl:template match="text()"/>
</xsl:stylesheet>

Надежность / масштабируемость

Поскольку это преобразование не содержит рекурсии (вложенные вызовы <xsl:apply-templates), оно является надежным и масштабируемым при применении к большим файлам XML.

С другой стороны, предоставленное в другом ответе решение рекурсии братьев и сестер дает сбой из-за переполнения стека, когда преобразование применяется к достаточно большому XML-документу. В моем случае этот сбой наблюдался с исходным XML-документом размером около 13 000 (13 тысяч строк) - это может варьироваться в зависимости от доступной оперативной памяти, процессора XSLT и т. Д.

Текущее преобразование успешно выполняется даже с очень большими XML-документами, такими как 1 200 000 (один миллион 200 тысяч строк).

person Dimitre Novatchev    schedule 25.05.2016

Очевидно, вы хотите сделать в XSLT 1.0 эквивалент group-adjacent в XSLT 2.0. Это может быть достигнуто с помощью техники, известной как «родственная рекурсия»:

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:template match="/RECORDS">
    <xsl:copy>
        <!-- start the first group -->
        <xsl:apply-templates select="RECORD[1]"/>
    </xsl:copy>
</xsl:template>

<xsl:template match="RECORD">
    <xsl:variable name="key" select="concat(E1, '+', E2, '+', E4 , '+', E5)" />
    <xsl:copy>
        <xsl:copy-of select="E1 | E2 | E4 | E5"/>
        <F>
            <xsl:copy-of select="F1"/>
            <!-- immediate sibling in the same group -->
            <xsl:apply-templates select="following-sibling::RECORD[1][concat(E1, '+', E2, '+', E4 , '+', E5) = $key]" mode="collect"/>
        </F>
    </xsl:copy>
    <!-- start the next group -->
    <xsl:apply-templates select="following-sibling::RECORD[not(concat(E1, '+', E2, '+', E4 , '+', E5)=$key)][1]"/>
</xsl:template>

<xsl:template match="RECORD" mode="collect">
    <xsl:variable name="key" select="concat(E1, '+', E2, '+', E4 , '+', E5)" />
    <xsl:copy-of select="F1"/>
    <!-- immediate sibling in the same group -->
    <xsl:apply-templates select="following-sibling::RECORD[1][concat(E1, '+', E2, '+', E4 , '+', E5) = $key]" mode="collect" />
</xsl:template> 

</xsl:stylesheet>
person michael.hor257k    schedule 20.05.2016
comment
Отличное решение! Мне потребовалось время, чтобы понять, но, наконец, понял - person ggonsalv; 26.05.2016