XSLT преобразовывает плоскую структуру в массив

Вот исходный XML:

<customers>
    <firstname1>Sean</firstname1>
    <lastname1>Killer</lastname1>
    <sex1>M</sex1>
    <firstname2>Frank</firstname2>
    <lastname2>Woods</lastname2>
    <sex2>M</sex2>
    <firstname3>Jennifer</firstname3>
    <lastname3>Lee</lastname3>
    <sex3>F</sex3>
</customers>

Как я могу преобразовать это в это?

<MyCustomers>
    <Customer>
        <Name> Sean Killer</Name>
        <Sex>M</Sex>
    </Customer>
    <Customer>
        <Name> Frank Woods</Name>
        <Sex>M</Sex>
    </Customer>
    <Customer>
        <Name>Jennifer Lee</Name>
        <Sex>F</Sex>
    </Customer>
</MyCustomers>

person sean    schedule 11.07.2011    source источник
comment
Пожалуйста, используйте code разметку и тщательно продумайте свои сообщения.   -  person Jaques le Fraque    schedule 12.07.2011
comment
Хороший вопрос, +1. См. Мой ответ о наиболее универсальном и гибком решении с использованием XSLT 1.0. Он дает желаемый результат, даже когда дочерние элементы верхнего элемента перетасовываются произвольным образом. :)   -  person Dimitre Novatchev    schedule 12.07.2011
comment
Какой ответ вы тогда примете?   -  person Emiliano Poggi    schedule 13.07.2011


Ответы (3)


Согласно комментариям:

что, если бы элементы не были в последовательном порядке?

В этом случае (при условии XSLT 1.0) вы можете использовать translate() для получения идентификаторов элементов, а затем искать соответствующие элементы по правильному имени, построенному с использованием concat(). Я бы изменил ось following-sibling:: на ../ (сокращение от parent::), чтобы в конечном итоге поймать также элементы, предшествующие текущему firstname.

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

    <xsl:template match="customers">
        <MyCustomers>
            <xsl:apply-templates select="*[starts-with(name(),'firstname')]"/>
        </MyCustomers>
    </xsl:template>

    <xsl:template match="*[starts-with(name(),'firstname')]">
        <xsl:variable name="id" select="translate(name(),'firstname','')"/>

        <Customer>
            <Name><xsl:value-of select="concat(.,' ',
                    ../*[name()=concat('lastname',$id)])"/></Name>
            <Sex><xsl:value-of select="../*[name()=concat('sex',$id)]"/></Sex>
        </Customer>
    </xsl:template>

</xsl:stylesheet>

Устаревший ответ

Предполагая фиксированную структуру входного документа, как показано в вопросе, отлично работающее преобразование XSLT 1.0:

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

    <xsl:template match="customers">
        <MyCustomers>
            <xsl:apply-templates select="*[starts-with(name(),'firstname')]"/>
        </MyCustomers>
    </xsl:template>

    <xsl:template match="*[starts-with(name(),'firstname')]">
        <Customer>
            <Name><xsl:value-of select="concat(.,' ',
                    following-sibling::*[1]
                    [starts-with(name(),'lastname')])"/></Name>
            <Sex><xsl:value-of select="following-sibling::*[2]
                    [starts-with(name(),'sex')]"/></Sex>
        </Customer>
    </xsl:template>

</xsl:stylesheet>

Небольшое объяснение

Вам нужна функция XPath 1.0 starts-with() из-за печального названия тегов в вашем вводе XML. Вы можете использовать ось following-sibling::, чтобы получить необходимые следующие родственные теги любого элемента, имя которого начинается с firstname.

person Emiliano Poggi    schedule 11.07.2011
comment
Спасибо за Ваш ответ. Но как быть, если элементы не находятся в последовательном порядке: ‹customers› ‹firstname1› Шон ‹/firstname1› ‹firstname2› Фрэнк ‹/firstname2› ‹lastname1› Killer ‹/lastname1 ‹›sex1› M ‹/sex1› ‹lastname2› Вудс ‹/lastname2› ‹firstname3› Дженнифер ‹/firstname3› ‹lastname3› Lee ‹/lastname3› ‹sex3› F ‹/sex3› ‹sex2› M ‹/sex2› ‹/customers› - person sean; 12.07.2011
comment
Если элементы расположены не в последовательном порядке, ваш образец ввода не отражает должным образом ваш настоящий XML. Однако, если элементы расположены не в последовательном порядке, как вы можете узнать, например, что определенный тег sex принадлежит определенному firstname? Не могли бы вы пояснить, пожалуйста? - person Emiliano Poggi; 12.07.2011
comment
Теперь я понимаю, вы имеете в виду зависимость от целого числа, добавленного в имя элемента? - person Emiliano Poggi; 12.07.2011
comment
@sean - см. мой ответ о решении XSLT 2.0. Надеюсь, вы сможете использовать 2.0. - person Daniel Haley; 12.07.2011
comment
@empo - мне нравится, как вы использовали translate() вместо чего-то вроде substring(). +1 - person Daniel Haley; 12.07.2011
comment
@empo: translate() совсем не нужен в этом решении и приводит к неэффективности. Вы можете просто использовать substring(name(), 9). Кроме того, при произвольной перестановке ваше решение производит клиентов, которые не упорядочены по идентификатору, который стоит в конце имени элемента. В своем ответе я делаю это очень просто. :) - person Dimitre Novatchev; 12.07.2011
comment
@Dimitre: у тебя отличные ответы. Большое спасибо. - person sean; 12.07.2011
comment
@Dimitre, ты (очевидно) прав. Меня не заботила сортировка (не упомянутая как требование в вопросе OP) и translate() может привести к (небольшой) неэффективности. Однако я не собираюсь менять ответ, в котором было получено сорок повторений. :)). OP может легко настроить его, применив сортировку (при необходимости) и изменив translate() на substring() (при необходимости). - person Emiliano Poggi; 12.07.2011

Вот таблица стилей XSLT 2.0, которая даст результат, который вы ищете, даже если они не в порядке. Он также сортируется по именам элементов "firstname".

Пример ввода XML (смешанный, чтобы показать другой порядок):

<customers>
  <lastname1>Killer</lastname1>
  <sex3>F</sex3>
  <firstname2>Frank</firstname2>
  <firstname1>Sean</firstname1>
  <lastname2>Woods</lastname2>
  <sex2>M</sex2>
  <firstname3>Jennifer</firstname3>
  <sex1>M</sex1>
  <lastname3>Lee</lastname3>
</customers>

Таблица стилей XSLT 2.0 (протестировано с Saxon-HE 9.3):

<xsl:stylesheet version="2.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
  <xsl:output indent="yes"/>
  <xsl:strip-space elements="*"/>

  <xsl:template match="node()|@*">
    <xsl:choose>
      <xsl:when test="name()[starts-with(.,'firstname')]">
        <xsl:variable name="suffix" select="substring(name(),10)"></xsl:variable>
        <xsl:message><xsl:value-of select="$suffix"/></xsl:message>
        <customer>
          <Name>
            <xsl:value-of select="concat(.,' ',/customers/*[starts-with(name(),'lastname')][ends-with(name(),$suffix)])"/>  
          </Name>
          <Sex>
            <xsl:value-of select="/customers/*[starts-with(name(),'sex')][ends-with(name(),$suffix)]"/>
          </Sex>
        </customer>
      </xsl:when>
      <xsl:when test="name()='customers'">
        <MyCustomers>
          <xsl:apply-templates>
            <xsl:sort select="name()[starts-with(.,'firstname')]"></xsl:sort>
          </xsl:apply-templates>
        </MyCustomers>
      </xsl:when>
      <xsl:otherwise>
        <xsl:apply-templates select="node()|@*"/>
      </xsl:otherwise>
    </xsl:choose>
  </xsl:template>

</xsl:stylesheet>

Вывод:

<MyCustomers>
   <customer>
      <Name>Sean Killer</Name>
      <Sex>M</Sex>
   </customer>
   <customer>
      <Name>Frank Woods</Name>
      <Sex>M</Sex>
   </customer>
   <customer>
      <Name>Jennifer Lee</Name>
      <Sex>F</Sex>
   </customer>
</MyCustomers>
person Daniel Haley    schedule 11.07.2011

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

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

 <xsl:variable name="vNumCustomers"
      select="count(/*/*) div 3"/>

 <xsl:template match="/*">
     <MyCustomers>
       <xsl:for-each select=
           "*[not(position() > $vNumCustomers)]">
         <xsl:variable name="vNum" select="position()"/>

         <Customer>
          <Name>
            <xsl:value-of select=
             "concat(/*/*[name()=concat('firstname',$vNum)],
                     ' ',
                     /*/*[name()=concat('lastname',$vNum)]
                     )
             "/>
          </Name>
          <Sex>
            <xsl:value-of select=
             "/*/*[name()=concat('sex',$vNum)]
             "/>
          </Sex>
         </Customer>
       </xsl:for-each>
     </MyCustomers>
 </xsl:template>
</xsl:stylesheet>

при применении к этому XML-документу (произвольная перетасовка предоставленного):

<customers>
    <sex1>M</sex1>
    <lastname2>Woods</lastname2>
    <lastname1>Killer</lastname1>
    <sex2>M</sex2>
    <firstname3>Jennifer</firstname3>
    <firstname2>Frank</firstname2>
    <lastname3>Lee</lastname3>
    <firstname1>Sean</firstname1>
    <sex3>F</sex3>
</customers>

Получен желаемый правильный результат:

<MyCustomers>
   <Customer>
      <Name>Sean Killer</Name>
      <Sex>M</Sex>
   </Customer>
   <Customer>
      <Name>Frank Woods</Name>
      <Sex>M</Sex>
   </Customer>
   <Customer>
      <Name>Jennifer Lee</Name>
      <Sex>F</Sex>
   </Customer>
</MyCustomers>

Объяснение:

  1. Подсчитываем количество клиентов, данные о которых представлены. Переменная $vNumCustomers хранит эти данные.

  2. Для каждого покупателя {i} (i = от 1 до $vNumCustomers) мы создаем соответствующий элемент <Customer{i}>. Чтобы избежать использования рекурсии, мы используем метод Piez. здесь.

person Dimitre Novatchev    schedule 12.07.2011
comment
Мне нравится идея count (/ * / *) div 3, но это может не сработать, если исходный xml содержит другие нерелевантные элементы, например: ‹Customers› ‹date› 2011-07-11 ‹/date› ‹firstname1 /› ‹Lastname1 /› ...... ‹Customers› - person sean; 12.07.2011
comment
@sean: Конечно. Есть гораздо более общее решение, мне нужно только найти немного свободного времени, чтобы вписать его в свой ответ. Мой текущий ответ был получен за 5 минут. - person Dimitre Novatchev; 12.07.2011
comment
Я ценю это. Кстати, какое решение вы предпочли? Это решение для каждого цикла или решение для сопоставления шаблонов? - person sean; 12.07.2011
comment
@Dimitre: Мне очень нравится ваш count(/*/*) div 3, но он может вызвать проблемы, если один из (трех) элементов отсутствует. Для проверки попробуйте удалить sex2 из ввода, и вывод, произведенный этим преобразованием, не попадет в цель. Попробуйте с моим собственным, и желаемый результат будет сохранен. - person Emiliano Poggi; 12.07.2011
comment
@Sean: нет идеальных ответов, даже если @Dimitre обычно бреют безупречно. - person Emiliano Poggi; 12.07.2011