Удаление повторяющихся узлов из вывода xsl

Учитывая следующий Xml:

<record>
    <category>Sport/Racket Sports/Tennis</category>
    <category>Sport/Racket Sports/Badminton</category>
</record>

Я пытаюсь разбить категории, чтобы создать следующий Xml:

<add>
   <doc>
      <field name="category_0">Sport</field>
      <field name="category_1">Sport/Racket Sports</field>
      <field name="category_2">Sport/Racket Sports/Tennis</field>
      <field name="category_2">Sport/Racket Sports/Badminton</field>
   </doc>
</add>

Мне удалось создать что-то, что почти готово. Теперь мне нужен способ удаления дубликатов? Любые идеи?

<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="1.0">
    <xsl:output indent="yes"/>
    <xsl:template match="record">
        <add>
            <doc>
                <xsl:for-each select="category[. != '']">
                    <xsl:call-template name="split-cats">
                        <xsl:with-param name="prefix" select="''"/>
                        <xsl:with-param name="text" select="."/>
                        <xsl:with-param name="level" select="number(0)"/>
                    </xsl:call-template>
                </xsl:for-each>
            </doc>
        </add>
    </xsl:template>

    <xsl:template name="split-cats">
        <xsl:param name="text" select="."/>
        <xsl:param name="prefix"/>
        <xsl:param name="level" select="0"/>
            <xsl:choose>
                <xsl:when test="contains($text, '/')">
                    <field>
                        <xsl:attribute name="name">
                            <xsl:text>category_</xsl:text><xsl:value-of select="$level"/>
                        </xsl:attribute>
                        <xsl:value-of select="concat($prefix, substring-before($text, '/'))"/>
                    </field>
                    <xsl:call-template name="split-cats">
                        <xsl:with-param name="prefix" select="concat($prefix, concat(substring-before($text, '/'), '/'))"/>
                        <xsl:with-param name="text" select="substring-after($text, '/')"/>
                        <xsl:with-param name="level" select="$level + 1"/>
                    </xsl:call-template>
                </xsl:when>
                <xsl:otherwise>
                    <field>
                        <xsl:attribute name="name">
                            <xsl:text>category_</xsl:text><xsl:value-of select="$level"/>
                        </xsl:attribute>
                        <xsl:value-of select="concat($prefix, $text)"/>
                    </field>
                </xsl:otherwise>
            </xsl:choose>        
    </xsl:template>



</xsl:stylesheet>

Этот шаблон производит:

<add>
   <doc>
      <field name="category_0">Sport</field>
      <field name="category_1">Sport/Racket Sports</field>
      <field name="category_2">Sport/Racket Sports/Tennis</field>
      <field name="category_0">Sport</field>
      <field name="category_1">Sport/Racket Sports</field>
      <field name="category_2">Sport/Racket Sports/Badminton</field>
   </doc>
</add>

Который, как вы можете видеть, имеет Sport и Sport/Racket Sports дважды :(

К вашему сведению: мне нужно сделать это с помощью XSLT 1.0.

Спасибо

Дэйв


person CraftyFella    schedule 11.01.2011    source источник
comment
Пожалуйста, в отдельном вопросе объясните, каковы требования/правила формирования вывода (в частности, непонятно, как должны формироваться имена категорий). Я думаю, что можно найти более простое решение и полностью избежать проблемы дублирования.   -  person Dimitre Novatchev    schedule 11.01.2011
comment
Ах да.. извините, число представляет уровень категории, где 0 — это корневой уровень.. в данном случае «Спорт», а 1 — первый уровень подкатегории, 2 — 2-й уровень подкатегории и т. д. Это обозначение используется по порядку. отправить документ в Solr. Посмотрите на третий вариант в этом письме search.lucidimagination.com/search/document/8a14673728e3a722/   -  person CraftyFella    schedule 11.01.2011


Ответы (3)


Вот простой подход, который просто вставляет второй шаг преобразования, используя мюнхианскую группировку, в результате, который создает ваш код:

<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="1.0"
  xmlns:exsl="http://exslt.org/common"
  exclude-result-prefixes="exsl">
    <xsl:output indent="yes"/>

    <xsl:key name="k1" match="cats/field" use="."/>

    <xsl:template match="record">
        <xsl:variable name="cats">
          <cats>
                <xsl:for-each select="category[. != '']">
                    <xsl:call-template name="split-cats">
                        <xsl:with-param name="prefix" select="''"/>
                        <xsl:with-param name="text" select="."/>
                        <xsl:with-param name="level" select="0"/>
                    </xsl:call-template>
                </xsl:for-each>
          </cats>
        </xsl:variable>
        <add>
            <doc>
              <xsl:copy-of select="exsl:node-set($cats)/cats/field[generate-id() = generate-id(key('k1', .)[1])]"/>
            </doc>
        </add>
    </xsl:template>

    <xsl:template name="split-cats">
        <xsl:param name="text" select="."/>
        <xsl:param name="prefix"/>
        <xsl:param name="level" select="0"/>
            <xsl:choose>
                <xsl:when test="contains($text, '/')">
                    <field>
                        <xsl:attribute name="name">
                            <xsl:text>category_</xsl:text><xsl:value-of select="$level"/>
                        </xsl:attribute>
                        <xsl:value-of select="concat($prefix, substring-before($text, '/'))"/>
                    </field>
                    <xsl:call-template name="split-cats">
                        <xsl:with-param name="prefix" select="concat($prefix, concat(substring-before($text, '/'), '/'))"/>
                        <xsl:with-param name="text" select="substring-after($text, '/')"/>
                        <xsl:with-param name="level" select="$level + 1"/>
                    </xsl:call-template>
                </xsl:when>
                <xsl:otherwise>
                    <field>
                        <xsl:attribute name="name">
                            <xsl:text>category_</xsl:text><xsl:value-of select="$level"/>
                        </xsl:attribute>
                        <xsl:value-of select="concat($prefix, $text)"/>
                    </field>
                </xsl:otherwise>
            </xsl:choose>        
    </xsl:template>



</xsl:stylesheet>

Это использует exsl:node-set однако, если вы ориентируетесь на XSLT 1.0 в браузерах, то для IE/MSXML вам нужно исправить это с помощью сценария, показанного в http://dpcarlisle.blogspot.com/2007/05/exslt.-node-set-function.html.

person Martin Honnen    schedule 11.01.2011
comment
Спасибо .. Я не был уверен, что такое набор узлов .. просто посмотрел его и, кажется, вы можете сделать это в .NET, используя пространство имен xmlns: msxsl = urn: schemas-microsoft-com: xslt :) - person CraftyFella; 11.01.2011
comment
Если вы используете .NET и XslCompiledTransform, вы можете использовать мою таблицу стилей в опубликованном виде (поскольку XslCompiledTransform поддерживает функцию набора узлов как в пространстве имен EXSLT, так и в пространстве имен Microsoft). Если вы используете .NET и XslTransform, то вы правы, вам нужно изменить URI пространства имен на тот, который вы опубликовали. - person Martin Honnen; 11.01.2011
comment
@CraftyFella: лучше использовать XslCompiledTransform, потому что, помимо прочего, он использует псевдостандартный URI пространства имен EXSLT. Кроме того, проверьте мой ответ для решения без расширения. - person ; 12.01.2011

Другой способ сделать это без использования функции расширения (но который не обязательно будет таким же эффективным, как использование мюнхианской группировки) — добавить проверку для сравнения предыдущих записей категории, чтобы увидеть, начинаются ли они со строки, о которой вы собираетесь узнать.

<xsl:if test="not(/record/category
              [. != '']
              [position() &lt; $pos]
              [substring(., 1, string-length($field-text)) = $field-text])">

В этом фрагменте кода $pos — это параметр, содержащий позицию текущего элемента category, с которым вы в настоящее время сопоставляетесь, а $field-text — это переменная, содержащая текст, который вы собираетесь вывести.

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

<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="1.0">
   <xsl:output indent="yes"/>

   <xsl:template match="record">
      <add>
         <doc>
            <xsl:for-each select="category[. != '']">
               <xsl:call-template name="split-cats">
                  <xsl:with-param name="prefix" select="''"/>
                  <xsl:with-param name="text" select="."/>
                  <xsl:with-param name="level" select="number(0)"/>
                  <!-- Position of the current category -->
                  <xsl:with-param name="pos" select="position()"/>
               </xsl:call-template>
            </xsl:for-each>
         </doc>
      </add>
   </xsl:template>

   <xsl:template name="split-cats">
      <xsl:param name="text" select="."/>
      <xsl:param name="prefix"/>
      <xsl:param name="level" select="0"/>
      <xsl:param name="pos"/>
      <xsl:choose>
         <xsl:when test="contains($text, '/')">
            <xsl:variable name="field-text" select="concat($prefix, substring-before($text, '/'))"/>
            <!-- Test no previous category begins with the text we are about to output -->
            <xsl:if test="not(/record/category
                             [. != '']
                             [position() &lt; $pos]
                             [substring(., 1, string-length($field-text)) = $field-text])">
               <field>
                  <xsl:attribute name="name">
                     <xsl:text>category_</xsl:text>
                     <xsl:value-of select="$level"/>
                  </xsl:attribute>
                  <xsl:value-of select="$field-text"/>
               </field>
            </xsl:if>
            <xsl:call-template name="split-cats">
               <xsl:with-param name="prefix" select="concat($prefix, concat(substring-before($text, '/'), '/'))"/>
               <xsl:with-param name="text" select="substring-after($text, '/')"/>
               <xsl:with-param name="level" select="$level + 1"/>
               <xsl:with-param name="pos" select="$pos"/>
            </xsl:call-template>
         </xsl:when>
         <xsl:otherwise>
            <xsl:variable name="field-text" select="concat($prefix, $text)"/>
            <!-- Test no previous category begins with the text we are about to output -->
            <xsl:if test="not(/record/category
                             [. != '']
                             [position() &lt; $pos]
                             [substring(., 1, string-length($field-text)) = $field-text])">
               <field>
                  <xsl:attribute name="name">
                     <xsl:text>category_</xsl:text>
                     <xsl:value-of select="$level"/>
                  </xsl:attribute>
                  <xsl:value-of select="$field-text"/>
               </field>
            </xsl:if>
         </xsl:otherwise>
      </xsl:choose>
   </xsl:template>
</xsl:stylesheet>
person Tim C    schedule 11.01.2011

Без функций расширения эта таблица стилей:

<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
    <xsl:strip-space elements="*"/>
    <xsl:template match="record">
        <add>
            <doc>
                <xsl:apply-templates select="category[1]"/>
            </doc>
        </add>
    </xsl:template>
    <xsl:template match="category" name="category">
        <xsl:param name="pOutput"/>
        <xsl:param name="pPrefix"/>
        <xsl:param name="pLevel" select="0"/>
        <xsl:param name="pSequence" select="concat(.,'/')"/>
        <xsl:choose>
            <xsl:when test="$pSequence">
                <xsl:variable name="vItem"
                              select="concat($pPrefix,
                                             substring-before($pSequence,
                                                              '/'))"/>
                <xsl:variable name="vOutput"
                              select="concat('|',$vItem,'|')"/>
                <xsl:if test="not(contains($pOutput,$vOutput))">
                    <field name="category_{$pLevel}">
                        <xsl:value-of select="$vItem"/>
                    </field>
                </xsl:if>
                <xsl:call-template name="category">
                    <xsl:with-param name="pOutput"
                                    select="concat($pOutput,$vOutput)"/>
                    <xsl:with-param name="pPrefix" 
                                    select="concat($vItem,'/')"/>
                    <xsl:with-param name="pLevel" select="$pLevel + 1"/>
                    <xsl:with-param name="pSequence"
                                    select="substring-after($pSequence,'/')"/>
                </xsl:call-template>
            </xsl:when>
            <xsl:otherwise>
                <xsl:apply-templates select="following-sibling::category[1]">
                    <xsl:with-param name="pOutput" select="$pOutput"/>
                </xsl:apply-templates>
            </xsl:otherwise>
        </xsl:choose>
    </xsl:template>
</xsl:stylesheet>

Выход:

<add>
    <doc>
        <field name="category_0">Sport</field>
        <field name="category_1">Sport/Racket Sports</field>
        <field name="category_2">Sport/Racket Sports/Tennis</field>
        <field name="category_2">Sport/Racket Sports/Badminton</field>
    </doc>
</add>
person Community    schedule 11.01.2011