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

#xml #xslt #xpath

#xml #xslt #xpath

Вопрос:

Учитывая XML-документ, подобный этому:

 <r>
  <a/><b/><c/>
  <d>
    <d1/>
    <d2>
      <d2a/>
      <d2b/>
      <d2c/>
    </d2>
  </d>
  <e/>
</r>
  

И, учитывая критерии «Начать с b, остановиться на d2b», существует ли выражение XPath, которое может выбрать либо:

В идеале:

 <c/><d><d1/><d2><d2a/></d2></d>
  

Разумно:

 <c/>
  

Я знаю, что с критериями «начинающимися с ‘a’ и заканчивающимися на ‘e'» я могу использовать выражение //*[preceding-sibling::a][following-sibling::e] ; Мне интересно, есть ли способ выполнить какое-то нечетное пересечение осей предков и предыдущих братьев и сестер, чтобы найти общего предка, когда начальный и конечный элементы не гарантированно разделяютсяодин и тот же родительский элемент.

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

1. вопрос не требует изменения структуры, не так ли? это просто требует возврата несколько странного подмножества. (Я не знаю, возможно ли это)

2. @SteveBennett: На самом деле, вопрос требует изменения структуры, OP хочет новую структуру, в которой d2 у dosn’t есть d2b d2c дочерние элементы и — поэтому эти элементы каким-то образом должны быть удалены — и XPath не может удалять элементы!

Ответ №1:

XPath (как 1.0, так и 2.0) — это язык запросов для XML-документов. Как таковой, он не может изменять узлы и структуру любого XML-документа.

Желаемый результат может быть получен с помощью преобразования XSLT (I. 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:param name="pStart" select="/*/b"/>
 <xsl:param name="pEnd" select="/*/d/d2/d2b"/>

  <xsl:variable name="vFollowingStart" select=
  "$pStart/following::* | $pStart/descendant::*"/>

 <xsl:variable name="vPrecedingEnd" select=
  "$pEnd/preceding::* | $pEnd/ancestor::*"/>

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

 <xsl:template match="*">
  <xsl:choose>
      <xsl:when test=
      "count(.|$vFollowingStart) = count($vFollowingStart)
      and
       count(.|$vPrecedingEnd) = count($vPrecedingEnd)
      ">
       <xsl:call-template name="identity"/>
      </xsl:when>
      <xsl:otherwise>
       <xsl:apply-templates/>
      </xsl:otherwise>
  </xsl:choose>
 </xsl:template>
</xsl:stylesheet>
  

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

 <r>
  <a/><b/><c/>
  <d>
    <d1/>
    <d2>
      <d2a/>
      <d2b/>
      <d2c/>
    </d2>
  </d>
  <e/>
</r>
  

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

 <c/>
<d>
   <d1/>
   <d2>
      <d2a/>
   </d2>
</d>
  

Explanation:

  1. The identity rule copies every matched node «as-is».

  2. There is a single overriding template matching any element.

  3. Внутри этого шаблона выполняются два теста: принадлежит ли текущий узел к набору всех элементов, «следующих за началом», и принадлежит ли текущий узел к набору всех элементов, «предшествующих концу». Если это так, текущий узел передается в шаблон идентификации (копируется), в противном случае он игнорируется (удаляется).


Решение II. XSLT 2.0

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

 <xsl:param name="pStart" select="/*/b"/>
 <xsl:param name="pEnd" select="/*/d/d2/d2b"/>

  <xsl:variable name="vFollowingStart" select=
  "$pStart/following::* | $pStart/descendant::*"/>

 <xsl:variable name="vPrecedingEnd" select=
  "$pEnd/preceding::* | $pEnd/ancestor::*"/>

 <xsl:variable name="vWanted" select=
  "$vFollowingStart intersect $vPrecedingEnd"/>

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

 <xsl:template match="*[not(. intersect $vWanted)]">
  <xsl:apply-templates/>
 </xsl:template>
</xsl:stylesheet>
  

Когда это преобразование применяется к приведенному выше XML-файлу, снова получается тот же правильный результат.

Объяснение: Использование оператора XPath 2.0 intersect .


III. Решение XPath 1.0, выбор только узлов без изменения документа:

Для удобства чтения я предоставляю преобразование XSLT, которое выводит результат выбора нужных узлов. С той же целью подвыражения определяются как переменные:

 <xsl:stylesheet version="1.0"
 xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
 xmlns:xs="http://www.w3.org/2001/XMLSchema">
 <xsl:output omit-xml-declaration="yes" indent="yes"/>

 <xsl:param name="pStart" select="/*/b"/>
 <xsl:param name="pEnd" select="/*/d/d2/d2b"/>

 <xsl:template match="node()|@*">
  <xsl:variable name="vFollowingStart" select=
  "$pStart/following::* | $pStart/descendant::*"/>

 <xsl:variable name="vPrecedingEnd" select=
  "$pEnd/preceding::* | $pEnd/ancestor::*"/>

  <xsl:copy-of select=
   "$vFollowingStart
      [count(.|$vPrecedingEnd)
      =
       count($vPrecedingEnd)
      ]
   "/>
 </xsl:template>
</xsl:stylesheet>
  

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

 <c/>
<d>

   <d1/>

   <d2>

      <d2a/>

      <d2b/>

      <d2c/>

   </d2>

</d>
<d1/>
<d2>

   <d2a/>

   <d2b/>

   <d2c/>

</d2>
<d2a/>
  

Объяснение: Здесь я использую формулу Kayessian (by @Michael Kay) для пересечения двух наборов узлов $ns1 и $ns2 :

 $ns1[count(.|$ns2) = count($ns2)]
  

IV. Наконец, решение Xpath 2.0 (соответствующее решению XPath 1.0):

Я снова использую преобразование 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">
     <xsl:output omit-xml-declaration="yes" indent="yes"/>

     <xsl:param name="pStart" select="/*/b"/>
     <xsl:param name="pEnd" select="/*/d/d2/d2b"/>

     <xsl:template match="node()|@*">
      <xsl:variable name="vFollowingStart" select=
      "$pStart/following::* | $pStart/descendant::*"/>

     <xsl:variable name="vPrecedingEnd" select=
      "$pEnd/preceding::* | $pEnd/ancestor::*"/>

      <xsl:sequence select=
       "$vFollowingStart intersect $vPrecedingEnd"/>
     </xsl:template>
</xsl:stylesheet>
  

Выдаются те же результаты (точно те же нужные узлы), что и в решении XPath 1.0:

 <c/>
<d>
        <d1/>
        <d2>
               <d2a/>
               <d2b/>
               <d2c/>
        </d2>
    </d>
<d1/>
<d2>
            <d2a/>
            <d2b/>
            <d2c/>
</d2>
<d2a/>
  

UPDATE: Here is a XPath 1.0 solution for the «reasonably» question. Again it is expressed as XSLT stylesheet module, in which, for better readability, subexpressions are defined as separate variables:

 <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:param name="pStart" select="/*/*/b"/>
 <xsl:param name="pEnd" select="/*/*/d/d2/d2b"/>

  <xsl:variable name="vFollowingStart" select=
  "$pStart/following::* | $pStart/descendant::*"/>

  <xsl:variable name="vcommonAncestor" select=
  "$pStart/ancestor::*
    [count(.|$pEnd/ancestor::*)
    =
     count($pEnd/ancestor::*)
    ][1]
    "/>
 <xsl:variable name="vEndHighestAncestor" select=
  "$vcommonAncestor/*
       [count($pEnd | descendant::*)
       =
        count(descendant::*)
       ]"/>

  <xsl:variable name="vPrecedingEnd" select=
  "$vEndHighestAncestor/preceding::*
  |
   $vEndHighestAncestor/ancestor::*"/>


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

 <xsl:template match="/">
  <xsl:copy-of select=
  "//*[count(.|$vFollowingStart) = count($vFollowingStart)
      and
       count(.|$vPrecedingEnd) = count($vPrecedingEnd)
      ]
  "/>
 </xsl:template>
</xsl:stylesheet>
  

Когда это преобразование применяется к следующему XML-документу (такому же, как предоставленный, но обернутому в еще один верхний элемент, и двум дочерним элементам ( g и h ), добавленным в c —, чтобы сделать его более интересным:

 <t>
<r>
  <a/><b/><c><g/><h/></c>
  <d>
    <d1/>
    <d2>
      <d2a/>
      <d2b/>
      <d2c/>
    </d2>
  </d>
  <e/>
</r>
</t>
  

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

 <c>
   <g/>
   <h/>
</c>
<g/>
<h/>
  

Объяснение: Это почти то же самое, что и раньше, но мы принимаем в качестве $pEnd его старшего предка — это непосредственный потомок общего предка $pStart и $pEnd .

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

1. Отличный ответ для первого «идеального» случая. Я подозревал, что только выбор не может привести к тому, что я описывал. Приятно, что вы включили решение XPath для тех, кому оно может понадобиться. У вас есть ответ на «Разумный» случай? (Найдите узел (ы) между предками каждой конечной точки, которые имеют общий родительский элемент.)

2. @Phrogz: Я думал, что решение XSLT — это то, что вам нужно, поэтому я не рассмотрел менее сложное пожелание. Сделаю это, но после возвращения домой с работы (куда я только что прибыл).

3. @Phrogz: Если я понимаю вашу «разумную» задачу, выбранными узлами должны быть: c и d — не только c . Если мое понимание неверно, пожалуйста, объясните.

4. Выбор между <b> и <e> даст <c> и <d> ; выбор между <b> и <d> даст только <c> . Для обеспечения согласованности я предлагаю, чтобы <d2b> это рассматривалось как <d> , и, следовательно <c> , было выбрано только.

5. @Phrogz: В обновлении в конце моего ответа я предоставляю решение (чистый XPath) для вашего «разумного» требования.

Ответ №2:

Для вашей «разумной» цели: учитывая критерии «Начать с b, остановиться на d2b», вы можете использовать следующий XPath:

 //b/following-sibling::*[following::d2b]
  

Поскольку following:: ось исключает потомков, будут выбраны только следующие братья и сестры b вплоть до того, который является предком (или самим собой) d2b.

(Я предполагаю, что в документе есть только один <b> элемент, как вы, похоже, предполагали.)

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

1. @_LarsH: К сожалению, ваше простое выражение XPath выбирает только один элемент с XML-документом в моем обновлении, но необходимо выбрать три элемента.

2. @Dimitre: Я интерпретировал спецификацию по-разному, особенно. «Выбор между <b> и <e> даст <c> и <d> ; выбор между <b> и <d> даст только <c> . Для согласованности я предлагаю, чтобы <d2b> это рассматривалось как <d> , и, таким образом, будет выбран только <c> . » Я обращусь к @OP, чтобы оценить, правильно ли я понял, что он хотел. Если нет, я буду рад это узнать.

3. Да, в этой части вопрос не задан.

4. @LarsH Вы интерпретировали мои комментарии так, как я их предполагал. Я все еще не понимаю, как это может работать, но я вижу, что это так. Мне нужно будет прочитать об following оси, потому что, похоже, она ведет себя противоположно тому, как я бы подозревал. Спасибо!

5. Хорошо, теперь я понял. Использование foo[following::bar] означает «Совпадение foo , если за ним bar следует» , а не «Совпадение foo , если оно следует bar » . Это мощная ось, которую я раньше не видел. Очень приятно, это пригодится. Спасибо!