использование XPath для выбора смежных элементов с определенным значением атрибута

У меня есть такой XML:

<span>1</span>
<span class="x">2</span>
<span class="x y">3</span>
<span class="x">4</span>
<span>5</span>
<span class="x">6</span>
<span>7</span>
<span class="x">8</span>

Я хочу использовать таблицу стилей XSLT, чтобы поместить содержимое всех элементов, чей атрибут class содержит x, в один элемент <x>. Таким образом, вывод должен быть таким:

1 <x>234</x> 5 <x>6</x> 7 <x>8</x>

(или, в идеале,

1 <x>2<y>3</y>4</x> 5 <x>6</x> 7 <x>8</x>

но это проблема, которую нужно решить, когда я решил эту.)

Это соответствующий фрагмент моего XSLT:

<xsl:template match="span[contains(@class,'x') and preceding-sibling::span[1][not(contains(@class,'x'))]]">
  <x><xsl:for-each select=". | following-sibling::span[contains(@class,'x')]">
    <xsl:value-of select="text()"/>
  </xsl:for-each></x>
</xsl:template>

<xsl:template match="span[contains(@class,'x') and preceding-sibling::span[1][contains(@class,'x')]]">
</xsl:template>

<xsl:template match="span">
  <xsl:value-of select="text()"/>
</xsl:template>

Что это дает:

1 <x>23468</x> 5 <x>68</x> 7 <x>8</x>

Я почти уверен, что мне нужно использовать счетчик в выражении XPath, чтобы он не выбирал все следующие элементы с классом x, а только смежные. Но как я могу посчитать смежные? Или я делаю это неправильно?


person ptomato    schedule 22.01.2012    source источник
comment
Также опубликуйте свой <xsl:call-template name="text">.   -  person Tomalak    schedule 22.01.2012
comment
Я упростил свой образец XSLT, чтобы он больше не вызывал этот шаблон. См. редактирование.   -  person ptomato    schedule 22.01.2012
comment
Да, но я все еще интересовался этим шаблоном. Мне это казалось ненужным. :)   -  person Tomalak    schedule 22.01.2012
comment
Этот шаблон преобразует такие строки, как text [bracketed text] text, в text <bracket>bracketed text</bracket> text.   -  person ptomato    schedule 22.01.2012
comment
А как насчет <span>1</span><span class="x y">2</span><span class="y">3</span>? И 1 <x><y>2</y></x><y>2</y>, и 1 <y><x>2</x>3</y> хороши?   -  person Markus Jarderot    schedule 22.01.2012
comment
@ptomato Звучит как идеальная работа для <xsl:template match="*" mode="text">. Таким образом, вы можете выбросить <xsl:for-each> и <xsl:call-template> и напрямую использовать <xsl:apply-templates>. Это должно сэкономить вам немного кода.   -  person Tomalak    schedule 22.01.2012
comment
@MizardX, в документах, которые я преобразовываю, y не будет происходить без x, поэтому я бы сказал, что первого варианта достаточно.   -  person ptomato    schedule 22.01.2012


Ответы (3)


Это сложно, но выполнимо (долго читайте вперед, извините за это).

Ключом к «последовательности» с точки зрения осей XPath (которые по определению не являются последовательными) является проверка того, является ли ближайший узел в противоположном направлении, который «первым выполняет условие», также является тем, который « начал" серию под рукой:

a
b  <- first node to fulfill the condition, starts series 1
b  <- series 1
b  <- series 1
a
b  <- first node to fulfill the condition, starts series 2
b  <- series 2
b  <- series 2
a

В вашем случае серия состоит из <span> узлов, у которых есть строка x в их @class:

span[contains(concat(' ', @class, ' '),' x ')] 

Обратите внимание, что я объединяю пробелы, чтобы избежать ложных срабатываний.

<span>, который начинает серию (то есть тот, который «первым выполняет условие»), может быть определен как тот, у которого есть x в своем классе и которому не предшествует другой <span>, у которого также есть x:

not(preceding-sibling::span[1][contains(concat(' ', @class, ' '),' x ')])

Мы должны проверить это условие в <xsl:if>, чтобы шаблон не генерировал выходные данные для узлов, которые находятся в серии (т. е. шаблон будет выполнять реальную работу только для «начальных узлов»).

Теперь к сложной части.

Из каждого из этих «стартовых узлов» мы должны выбрать все узлы following-sibling::span, которые имеют x в своем классе. Также включите текущий span для учета серий, содержащих только один элемент. Хорошо, достаточно просто:

. | following-sibling::span[contains(concat(' ', @class, ' '),' x ')]

Теперь для каждого из них мы выясняем, идентичен ли их ближайший «стартовый узел» тому, над которым работает шаблон (т. е. который начал их серию). Это означает:

  • они должны быть частью серии (т. е. они должны следовать за span с x)

    preceding-sibling::span[1][contains(concat(' ', @class, ' '),' x ')]
    
  • теперь удалите все span, чей начальный узел не идентичен начальному элементу текущей серии. Это означает, что мы проверяем любой предшествующий одноуровневый элемент span (имеющий x), которому непосредственно не предшествует span, с x:

    preceding-sibling::span[contains(concat(' ', @class, ' '),' x ')][
      not(preceding-sibling::span[1][contains(concat(' ', @class, ' '),' x ')])
    ][1]
    
  • Затем мы используем generate-id() для проверки подлинности узла. Если найденный узел идентичен $starter, то текущий отрезок относится к последовательному ряду.

Собираем все вместе:

<xsl:template match="span[contains(concat(' ', @class, ' '),' x ')]">
  <xsl:if test="not(preceding-sibling::span[1][contains(concat(' ', @class, ' '),' x ')])">
    <xsl:variable name="starter" select="." />
    <x>
      <xsl:for-each select="
        . | following-sibling::span[contains(concat(' ', @class, ' '),' x ')][
          preceding-sibling::span[1][contains(concat(' ', @class, ' '),' x ')]
          and
          generate-id($starter)
          =
          generate-id(
            preceding-sibling::span[contains(concat(' ', @class, ' '),' x ')][
              not(preceding-sibling::span[1][contains(concat(' ', @class, ' '),' x ')])
            ][1]
          )
        ]
      ">
        <xsl:value-of select="text()" />
      </xsl:for-each>
    </x>
  </xsl:if>
</xsl:template>

И да, я знаю, что это некрасиво. Существует решение на основе <xsl:key>, которое является более эффективным, ответ Димитры показывает это.

С вашим образцом ввода генерируется этот вывод:

1
<x>234</x>
5
<x>6</x>
7
<x>8</x>
person Tomalak    schedule 22.01.2012
comment
Но нужно думать и об идеальной проблеме тоже :) - person Dimitre Novatchev; 22.01.2012
comment
@Dimitre Я подумал об этом и решил, что реализовать это будет совсем не смешно (учитывая, что class="w x y z" будет означать, что мне придется реализовать стек, токенизатор и обработку ошибок для неправильно вложенных элементов). Я даже не уверен, что мое решение здесь идеально (при условии, что ключ не следует использовать). - person Tomalak; 22.01.2012
comment
Томалак, идеальных решений не бывает — достаточно хороших, рабочих решений. - person Dimitre Novatchev; 22.01.2012
comment
@Dimitre Пока вы подтверждаете, что моя логика / код не содержит ошибок и не может быть упрощена, со мной все в порядке. Это кажется немного повторяющимся, но, насколько я понимаю, каждый предикат необходим. - person Tomalak; 22.01.2012

И. XSLT-решения:

Я хочу использовать таблицу стилей XSLT, чтобы поместить содержимое всех элементов, чей атрибут класса содержит x, в один элемент <x>. Таким образом, вывод должен быть таким:

1 <x>234</x> 5 <x>6</x> 7 <x>8</x>

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

 <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="kFollowing" match=
  "span[contains(concat(' ', @class, ' '),
                 ' x ')
        ]"
   use="generate-id(preceding-sibling::span
                                    [not(contains(concat(' ', @class, ' '),
                                             ' x '))
                                    ][1]
                    )
        "/>

 <xsl:template match=
 "span[contains(concat(' ', @class, ' '), ' x ')
     and
       not(contains(concat(' ', preceding-sibling::span[1]/@class, ' '),
                    ' x '
                    )
           )
      ]"
  >
     <x>
       <xsl:apply-templates mode="inGroup" select=
       "key('kFollowing',
             generate-id(preceding-sibling::span                                                           [not(contains(concat(' ', @class, ' '),                                                       ' x ')
                                 )
                            ][1]
                        )
            )
      "/>
     </x>
 </xsl:template>

 <xsl:template match=
 "span[contains(concat(' ', @class, ' '), ' x ')
     and
       contains(concat(' ', preceding-sibling::span[1]/@class, ' '),
                    ' x '
                    )
      ]
  "/>
</xsl:stylesheet>

при применении к предоставленному XML-документу (завернуто в один верхний элемент html, чтобы сделать его правильно сформированным):

<html>
    <span>1</span>
    <span class="x">2</span>
    <span class="x y">3</span>
    <span class="x">4</span>
    <span>5</span>
    <span class="x">6</span>
    <span>7</span>
    <span class="x">8</span>
</html>

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

1<x>234</x>5<x>6</x>7<x>8</x>

Затем добавление "в идеале":

или, в идеале,

1 <x>2<y>3</y>4</x> 5 <x>6</x> 7 <x>8</x> 

но это проблема, которую нужно решить, когда я решил эту.)

Просто добавьте к приведенному выше решению этот шаблон:

  <xsl:template mode="inGroup" match=
    "span[contains(concat(' ', @class, ' '),
                   ' y '
                   )
         ]">
    <y><xsl:value-of select="."/></y>
  </xsl:template>

При применении модифицированного таким образом решения к тому же XML-документу снова создается (новый) желаемый результат:

1<x>2<y>3</y>4</x>5<x>6</x>7<x>8</x>

II. Решение 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="my xs"
>
 <xsl:output omit-xml-declaration="yes" indent="yes"/>

 <xsl:template match="/*">
         <xsl:for-each-group select="span" group-adjacent=
          "contains(concat(' ',@class,' '), ' x ')">

           <xsl:sequence select=
           "if(current-grouping-key())
              then
                my:formatGroup(current-group())
              else
                data(current-group())
           "/>
         </xsl:for-each-group>
 </xsl:template>

 <xsl:function name="my:formatGroup" as="node()*">
  <xsl:param name="pGroup" as="node()*"/>

  <x>
   <xsl:apply-templates select="$pGroup"/>
  </x>
 </xsl:function>

 <xsl:template match=
   "span[contains(concat(' ',@class, ' '), ' y ')]">
  <y><xsl:apply-templates/></y>
 </xsl:template>
</xsl:stylesheet>

Когда это преобразование XSLT 2.0 применяется к тому же XML-документу (выше), получается желаемый "идеальный" результат:

1<x>2<y>3</y>4</x>5<x>6</x>7<x>8</x>
person Dimitre Novatchev    schedule 22.01.2012
comment
@Tomalak, как обычно, решение XSLT 2.0 намного проще. - person Dimitre Novatchev; 22.01.2012

Спасибо за решения. Тем временем мне удалось кое-что собрать, используя совершенно другую тактику. Я только изучаю XSLT для этого проекта, и самое полезное, что я прочитал, это то, что XSLT похож на функциональное программирование. Итак, я написал кое-что, используя рекурсию, после того как это указало правильное направление:

<xsl:template match="span[
                       contains(@class,'x')
                       and
                       preceding-sibling::span[1][
                         not(contains(@class,'x'))
                       ]
                     ]">
  <x><xsl:value-of select="text()"/>
    <xsl:call-template name="continue">
      <xsl:with-param name="next" select="following-sibling::span[1]"/>
    </xsl:call-template>
  </x>
</xsl:template>

<xsl:template name="continue">
  <xsl:param name="next"/>
  <xsl:choose>
    <xsl:when test="$next[contains(@class,'x')]">
      <xsl:apply-templates mode="x" select="$next"/>
      <xsl:call-template name="continue">
        <xsl:with-param name="next" select="$next/following-sibling::span[1]"/>
      </xsl:call-template>
    </xsl:when>
    <xsl:otherwise/><!-- Do nothing -->
  </xsl:choose>
</xsl:template>

<xsl:template match="span[
                       contains(@class,'x')
                       and
                       preceding-sibling::span[1][
                         contains(@class,'x')
                       ]
                     ]"/>

<xsl:template match="span">
  <xsl:value-of select="text()"/>
</xsl:template>

<xsl:template mode="x" match="span[contains(@class,'y')]">
  <y><xsl:value-of select="text()"/></y>
</xsl:template>

<xsl:template mode="x" match="span">
  <xsl:value-of select="text()"/>
</xsl:template>

Я понятия не имею, более или менее эффективно это делать, чем делать это с помощью generate-id() или ключей, но я определенно научился чему-то из ваших решений!

person ptomato    schedule 22.01.2012
comment
ptomato: В рекурсивном решении нет ничего плохого, за исключением того, что при достаточно больших входных данных рекурсия часто (если только специально не закодирована с использованием DVC или хвостовой рекурсии — последняя не распознается всеми процессорами XSLT) приводит к переполнению стека и/или вообще меньше чем оптимальная эффективность. - person Dimitre Novatchev; 23.01.2012