группировка xslt по каждому атрибуту

У меня есть несколько типов сообщений 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, а последние 2 имеют одни и те же атрибуты 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-сообщения, которое мне нужно отправить.


person Ed Fox    schedule 09.07.2012    source источник


Ответы (2)


Это универсальное преобразование 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() .


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.

  3. Используется тот же алгоритм рекурсивной группировки, что и в решении XSLT 2.0.

person Dimitre Novatchev    schedule 10.07.2012
comment
Кажется, это действительно то, что я хотел, за исключением того, что я застрял на 1.0. Я посмотрю, смогу ли я что-нибудь с этим сделать. Спасибо за подробный ответ. - person Ed Fox; 10.07.2012
comment
@EdFox: в XSLT 1.0 используется та же идея, но с двухпроходным преобразованием, при котором на первом проходе создается копия каждого элемента и добавляется специальный новый элемент (или атрибут), содержащий подпись. Во втором проходе мы делаем простую мюнховскую группировку по этому специальному элементу/атрибуту. - person Dimitre Novatchev; 10.07.2012
comment
не могли бы вы добавить версию 1.0 в свой пост? Извините, я все еще немного перегружен xslt, поэтому я не уверен, что смогу перевести ваше объяснение в реальный код. - person Ed Fox; 10.07.2012
comment
@EdFox: С удовольствием. Скоро я выйду на работу, так что через 10 часов я смогу начать работу над аналогичным решением XSLT 1.0 — так что, пожалуйста, наберитесь терпения. - person Dimitre Novatchev; 10.07.2012
comment
@EdFox: Готово - см. Часть II этого ответа для эквивалентного универсального преобразования XSLT 1.0. - person Dimitre Novatchev; 11.07.2012
comment
почему объявление xmlns в выводе, хотя вы исключили его в заголовке? - person Ed Fox; 11.07.2012
comment
@EdFox: Да, я видел это - это исчезнет, ​​если имя шаблона и имя специального атрибута (my:sign) будут переименованы в без префикса. Я использовал пространство имен для атрибута, чтобы убедиться, что это имя не конфликтует с существующим именем атрибута в документе — вместо этого мы могли бы использовать какое-то очень специальное имя без пространства имен — что-то вроде: "_________A_Very_Special_Attribute_________" - person Dimitre Novatchev; 11.07.2012
comment
@EdFox: проблема с нежелательным пространством имен исправлена, и я заменил преобразование XSLT 1.0 на исправленное - всего лишь незначительное изменение - заменил <xsl:copy> на <xsl:element name="{name()}"> и добавил выборочное копирование узлов пространства имен, за исключением пространства имен "my:my". Поэтому, пожалуйста, проигнорируйте мой предыдущий комментарий, рекомендующий в качестве решения не использовать специальное пространство имен. - person Dimitre Novatchev; 12.07.2012
comment
если некоторые узлы содержат текст, чтобы скопировать это, я должен добавить <xsl:value-of select="text()" /> после <xsl:copy-of select="@*[not(name()='my:sign')]"/> (в версии 1.0), верно? - person Ed Fox; 18.07.2012
comment
@EdFox: Да, я бы использовал <xsl:copy-of select="node()"/>, поскольку он более общий и копирует все поддерево с корнем в текущем узле. - person Dimitre Novatchev; 18.07.2012

Эта таблица стилей 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 соответствующего им TopLevel.

person Wayne    schedule 09.07.2012
comment
кажется, что это работает, но не работает, когда есть 2 узла TopLevel с разным кодом, но с одними и теми же дочерними элементами (они группируются под первым узлом, который появляется в файле). например, pastebin.com/0EPpnycL - person Ed Fox; 10.07.2012
comment
@EdFox - Хороший вопрос. Мы должны включить прародителя @CodeTL в групповой ключ Rating. Смотрите мою правку. - person Wayne; 10.07.2012