Сортировка XML с помощью XSLT

#xml #xslt #sorting #recursive-query

#xml #xslt #сортировка #рекурсивный запрос

Вопрос:

У меня есть xml, который описывает дерево каталога. Он может иметь любое количество дочерних узлов. Вот пример:

 <Catalog name="AccessoriesCatalog">
<Category Definition="AccessoriesCategory" name="1532" id="1532">
</Category>
<Category Definition="AccessoriesCategory" name="16115" id="16115">
    <ParentCategory>1532</ParentCategory>
</Category>
<Category Definition="AccessoriesCategory" name="16116" id="16116">
    <ParentCategory>16115</ParentCategory>
</Category>
<Category Definition="AccessoriesCategory" name="16126" id="16126">
    <ParentCategory>16115</ParentCategory>
</Category>
<Category Definition="AccessoriesCategory" name="16131" id="16131">
    <ParentCategory>1532</ParentCategory>
</Category>
<Category Definition="AccessoriesCategory" name="16132" id="16132">
    <ParentCategory>16131</ParentCategory>
</Category>
<Category Definition="AccessoriesCategory" name="16136" id="16136">
    <ParentCategory>16131</ParentCategory>
</Category>
<Category Definition="AccessoriesCategory" name="16139" id="16139">
    <ParentCategory>16131</ParentCategory>
</Category>
<Category Definition="AccessoriesCategory" name="16144" id="16144">
    <ParentCategory>16131</ParentCategory>
</Category>
<Category Definition="AccessoriesCategory" name="16195" id="16195">
    <ParentCategory>16131</ParentCategory>
</Category>
  

Мне нужно иметь возможность сортировать его по названию категории и ParentCategory. Все родительские категории должны быть первыми в xml, а «конечные категории» — последними. В этом примере xml уже отсортирован.

Приведенный выше xml выглядит следующим образом, когда он представлен в виде дерева
1532
-16115
—16116
—16126
-16131
—16132
—16136
—16139
—16144
—16195

Я хочу, чтобы он был отсортирован следующим образом
1532
-16115
-16131
—16116
—16126
—16132
—16136
—16139
—16144
—16195

Это может быть несколько уровней дочерних элементов (в данном случае только трехуровневое дерево). Я хочу, чтобы все элементы уровня 1 были первыми в xml, затем все элементы уровня 2, а затем все элементы уровня 3 и т.д.

Комментарии:

1. Я запутался в отношениях между a Category и its ParentCategory . В вашем примере похоже, что элементы отсортированы только по названию категории.

2. Вы могли бы получить отображаемый результат, просто выполнив числовую сортировку по @name (или, если уж на то пошло, @id). Не могли бы вы показать нам пример, чтобы проиллюстрировать, где ParentCategory входит в картину?

3. Если это плоское представление дерева, то /Catalog/Category[@id = ../Category/ParentCategory] будет выбран каждый родительский элемент на любом уровне и /Catalog/Category[not(@id = ../Category/ParentCategory)] будет выбран каждый лист. Неясно, хотите ли вы такой порядок (все родительские, все конечные) или вам нужен потомок после каждого родителя.

4. Отличный вопрос, 1. Смотрите мой ответ для полного решения.

5. Кроме того, добавлено подробное объяснение.

Ответ №1:

Это преобразование:

 <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:key name="kElemById" match="Category"
  use="@id"/>

 <xsl:template match="node()|@*">
  <xsl:copy>
   <xsl:apply-templates select="node()|@*"/>
  </xsl:copy>
 </xsl:template>

 <xsl:template match="/*">
  <xsl:copy>
   <xsl:copy-of select="@*"/>

   <xsl:call-template name="sortHier">
    <xsl:with-param name="pNodes" select=
    "*[ParentCategory]"/>
    <xsl:with-param name="pParents" select=
    "*[not(ParentCategory)]"/>
   </xsl:call-template>
  </xsl:copy>
 </xsl:template>

 <xsl:template name="sortHier">
  <xsl:param name="pNodes"/>
  <xsl:param name="pParents"/>

  <xsl:apply-templates select=
   "$pParents|$pNodes[not($pParents)]">
   <xsl:sort select="@name"/>
  </xsl:apply-templates>

   <xsl:if test="$pNodes and $pParents">
    <xsl:variable name="vNewParents"
     select="key('kElemById', $pNodes/ParentCategory)
                [not(@id=$pParents/@id)]
     "/>

    <xsl:variable name="vNewChildren"
     select="$pNodes[not(@id=$vNewParents/@id)]"/>

    <xsl:call-template name="sortHier">
     <xsl:with-param name="pNodes"
          select="$vNewChildren"/>
     <xsl:with-param name="pParents"
          select="$vNewParents"/>
    </xsl:call-template>
   </xsl:if>
 </xsl:template>
</xsl:stylesheet>
  

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

 <Catalog name="AccessoriesCatalog">
    <Category Definition="AccessoriesCategory"
    name="16144" id="16144">
        <ParentCategory>16131</ParentCategory>
    </Category>
    <Category Definition="AccessoriesCategory"
    name="16116" id="16116">
        <ParentCategory>16115</ParentCategory>
    </Category>
    <Category Definition="AccessoriesCategory"
    name="16126" id="16126">
        <ParentCategory>16115</ParentCategory>
    </Category>
    <Category Definition="AccessoriesCategory"
    name="16131" id="16131">
        <ParentCategory>1532</ParentCategory>
    </Category>
    <Category Definition="AccessoriesCategory"
    name="16132" id="16132">
        <ParentCategory>16131</ParentCategory>
    </Category>
    <Category Definition="AccessoriesCategory"
    name="16136" id="16136">
        <ParentCategory>16131</ParentCategory>
    </Category>
    <Category Definition="AccessoriesCategory"
    name="16139" id="16139">
        <ParentCategory>16131</ParentCategory>
    </Category>
    <Category Definition="AccessoriesCategory"
    name="16115" id="16115">
        <ParentCategory>1532</ParentCategory>
    </Category>
    <Category Definition="AccessoriesCategory"
    name="1532" id="1532"></Category>
    <Category Definition="AccessoriesCategory"
    name="16195" id="16195">
        <ParentCategory>16131</ParentCategory>
    </Category>
</Catalog>
  

выдает желаемый, правильный результат:

 <Catalog name="AccessoriesCatalog">
   <Category Definition="AccessoriesCategory" name="1532" id="1532"/>
   <Category Definition="AccessoriesCategory" name="16115" id="16115">
      <ParentCategory>1532</ParentCategory>
   </Category>
   <Category Definition="AccessoriesCategory" name="16131" id="16131">
      <ParentCategory>1532</ParentCategory>
   </Category>
   <Category Definition="AccessoriesCategory" name="16116" id="16116">
      <ParentCategory>16115</ParentCategory>
   </Category>
   <Category Definition="AccessoriesCategory" name="16126" id="16126">
      <ParentCategory>16115</ParentCategory>
   </Category>
   <Category Definition="AccessoriesCategory" name="16132" id="16132">
      <ParentCategory>16131</ParentCategory>
   </Category>
   <Category Definition="AccessoriesCategory" name="16136" id="16136">
      <ParentCategory>16131</ParentCategory>
   </Category>
   <Category Definition="AccessoriesCategory" name="16139" id="16139">
      <ParentCategory>16131</ParentCategory>
   </Category>
   <Category Definition="AccessoriesCategory" name="16144" id="16144">
      <ParentCategory>16131</ParentCategory>
   </Category>
   <Category Definition="AccessoriesCategory" name="16195" id="16195">
      <ParentCategory>16131</ParentCategory>
   </Category>
</Catalog>
  

Объяснение:

  1. Рекурсивно вызываемый именованный шаблон с двумя параметрами: «набор текущих родителей» (или «последние найденные родители») и набор текущих (все еще не обработанных) узлов.

  2. Условие остановки: либо «набор текущих родительских элементов», либо «набор текущих узлов», либо оба они пусты. Здесь мы выводим (и сортируем по @name ) оставшийся непустой набор параметров.

  3. Рекурсивный шаг: непосредственные дочерние элементы «текущих родителей» становятся новыми «текущими родителями». Остальные «текущие узлы» становятся новыми «текущими узлами». Скопируйте все текущие родительские файлы или все текущие узлы, если не осталось текущих родительских файлов.

Обновить:

В комментариях OP утверждал, что решение работало с небольшими файлами,

«Но когда я пробую это для всего xml с большим количеством элементов и уровней, это не работает. У меня есть xml размером около 8 Мб, поэтому я не могу опубликовать его здесь.»

Я попросил его предоставить (автономно) XML-файлы, и когда я их получил, я подтвердил, что это решение работает без проблем как с маленькими, так и с большими (44000 строк, 700 КБ) файлами, которые мне были предоставлены.

Производительность для файла большего размера была не так уж плоха, за исключением MSXML3.

Вот данные о производительности для файла в 44000 строк, которые можно увидеть на моем 8-летнем ПК (2 ГБ оперативной памяти, 3 ГГц одноядерный):

 MSXML3:                 91 sec.

MSXML6:                  6 sec.

AltovaXML (XMLSpy):      6 sec.

Saxon 6.5.4:             2 sec.

Saxon 9.1.05:            1.6 sec.

XslCompiledTransform     1.3 sec.

XQSharp:                 0.8 sec.
  

Комментарии:

1. Привет, это решение работает почти так, как я его хочу, спасибо. Но когда я пробую это для всего xml с большим количеством элементов и уровней, это не работает. У меня есть xml размером около 8 Мб, поэтому я не могу опубликовать его здесь. Или это можно куда-то загрузить?

2. @Thomas: Вы должны быть более конкретны в значении «не работает». И есть множество мест, где вы можете сделать XML доступным — одно из них skydrive.com . Вы также можете отправить этот файл мне. Я использую почту на базе Google, и мой идентификатор dnovatchev.

3. @Thomas: Я тестировал это решение на документах с большим количеством уровней, и оно дает желаемые результаты. Я предполагаю, что ваш XML-документ имеет некоторую аномалию данных (например, содержит цикл), что делает его не деревом, а более общим видом графика. Пожалуйста, рассмотрите возможность принятия этого ответа и публикации нового вопроса с определенной и предоставленной вашей новой структурой данных.

4. @Dimitre, xml является стандартным форматом Microsoft Commerce Server и описывает дерево каталога. Вы можете найти пример здесь volvopentashop.com/MarincenterSyd/en-GB/Details /… . Я отправлю вам XML. После того, как вы опробовали свой xslt на этом, взгляните на элемент с именем 12100. У этого элемента есть родительский элемент, указанный с именем 1797. Но фактический родительский элемент указан позже в преобразованном выводе. Он должен быть указан перед элементом с идентификатором 12100. Родители должны быть перечислены первыми в выходных данных. Сначала родительские элементы на уровне 1, затем родительские элементы на уровне 2 и т.д.

5. Еще одна вещь. Я хочу, чтобы сначала были перечислены родительские файлы, поскольку этот XML-файл будет импортирован Commerce Server (CS), и CS считывает его поэлементно. Если CS считывает элемент, у которого указан родительский элемент, и этот родительский элемент еще не был импортирован, то возникнет ошибка. Вот почему я хочу, чтобы все родительские файлы были сначала расположены по порядку по их уровню в дереве.

Ответ №2:

Я думаю, ваш ответ можно было бы найти здесь: http://www.programmersheaven.com/2/FAQ-XML-Sort-XML-By-Multiple-Attributes-XSLT