использование XPath для выбора смежных элементов с определенным значением атрибута - PullRequest
5 голосов
/ 22 января 2012

У меня есть 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, а только смежные. Но как я могу сосчитать смежные? Или я делаю это неправильно?

Ответы [ 3 ]

8 голосов
/ 22 января 2012

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

Ключом к "последовательности" в терминах осей 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 ')] 

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

A <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>
5 голосов
/ 22 января 2012

I.Решения 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>
2 голосов
/ 22 января 2012

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

<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()или ключи, но я, конечно, кое-что узнал из ваших решений!

...