Можно ли обрезать ось XPath в данном узле? - PullRequest
0 голосов
/ 09 февраля 2012

Я написал некоторый код, который извлекает основной текстовый контент из веб-страниц. Одна из полезных стратегий заключается в том, чтобы найти первый абзац контента, а затем выбрать все из следующих родственных элементов, вплоть до первого, который не является p, ul, ol или blockquote элемент. В Perl код выглядит примерно так:

my ($firstpara) = $document->findnodes('//p[whatever]');
my @content = ($firstpara);
for my $sibling ($firstpara->findnodes('following-sibling::*')) {
    last if $sibling->tag !~ /^(?:p|ol|ul|blockquote)\z/;
    push @content, $sibling;
}

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

my ($firstpara) = $document->findnodes('//p[whatever]');
my @content = ($firstpara, $firstpara->findnodes('<query>'));

Я провел много экспериментов, но не смог понять, как написать последний запрос. Наиболее близким к правильному решению, которое я смог найти, является что-то вроде:

$firstpara->findnodes('following-sibling::*[position() < $EXPR]');

... где $EXPR - это некоторое выражение, которое возвращает позицию следующего родного брата, чей тег не является p, ul, ol или blockquote, но я не смог выяснить, можно ли такое выражение выразить в XPath.

Есть ли способ сделать то, что я описал в XPath?

* * 1 022 Пример: * 1 023 *

Предположим, мой документ выглядит так:

<h1>Header</h1>
<p>Paragraph 1</p>
<p id="first">Paragraph 2</p>
<p>Paragraph 3</p>
<ul><li>Item 1</li><li>Item 2</li></ul>
<p>Paragraph 4</p>
<hr>
<p>Paragraph 5</p>
<blockquote>Blockquote 1</blockquote>
...

У меня есть ссылка на элемент <p> с идентификатором first. Я следую за выражением XPath, используя этот элемент first в качестве узла содержимого, который даст мне следующие братья и сестры Paragraph 3, неупорядоченный список и Paragraph 4. Элемент <hr> не входит в число тех, которые мне нужны (<p>, <ul>, <ol> и <blockquote>), поэтому этот элемент и все его братья и сестры после этого не должны быть частью возвращенного набора узлов.

1 Ответ

1 голос
/ 12 февраля 2012

Как объяснил ОП, он хочет:

все следующие родственные элементы до, но не включая первый элемент, который не является элементом p, ul, ol или blockquote

I. Решение XPath 1.0:

Требуемые узлы являются пересечением двух наборов узлов :

  1. Все элементы, которые следуют за братьями и сестрами p с id со значением 'first'.

  2. Все элементы, которые предшествуют братьям и сестрам hr.

Чтобы найти это в XPath 1.0, мы используем формулу Кейсиана для пересечения узлов :

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

Вышеупомянутое выражение XPath выбирает все узлы, которые принадлежат как к набору узлов $ns1 , так и к набору узлов $ns2.

Пусть $vP1 определяется как /*/p[@id='first'].

Пусть $vFirstNotInRange это:

   $vP1/following-sibling::*
    [not(self::p or self::ul
        or self::ol or self::blockquote)
    ] [1]

Это выбирает первый нежелательный узел (в данном случае hr) или, точнее: первый элемент, который является следующим родным братом $vP1, и это не p, ul, a ol или blockquote.

Тогда два набора узлов, которые мы хотим пересечь, это все следующие братья и сестры $vP1 и все предшествующие братья и сестры $vFirstNotInRange:

Обозначим $vFollowingP1 первый набор узлов - это:

$vP1/following-sibling::*

И давайте обозначим $vPreceedingNotInRange второй набор узлов - это:

$vFirstNotInRange/preceding-sibling::*

Наконец, мы подставляем в формулу Кейсины $ns1 с $vPreceedingNotInRange и $ns2 с $vFollowingP1. В результате этих подстановок выбираются именно нужные узлы:

$vPreceedingNotInRange
         [count(.|$vFollowingP1)
         =
          count($vFollowingP1)
         ]

Если мы подставим все переменные, пока не получим выражение, которое не содержит никаких переменных, мы получим:

   /*/p[@id='first']/following-sibling::*
     [not(self::p or self::ul
         or self::ol or self::blockquote
          )
     ] [1]
        /preceding-sibling::*
          [count(.| /*/p[@id='first']/following-sibling::*)
          =
           count(/*/p[@id='first']/following-sibling::*)
          ]

Это выражение выбирает именно нужные узлы.

Вот проверка на основе XSLT :

<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:variable name="vP1" select="/*/p[@id='first']"/>

 <xsl:variable name="vFirstNotInRange" select=
  "$vP1/following-sibling::*
    [not(self::p or self::ul
        or self::ol or self::blockquote)
    ] [1]"/>

 <xsl:variable name="vFollowingP1"
      select="$vP1/following-sibling::*"/>

 <xsl:variable name="vPreceedingNotInRange"
      select="$vFirstNotInRange/preceding-sibling::*"/>

 <xsl:template match="/">
  <xsl:copy-of select=
  "$vPreceedingNotInRange
   [count(.|$vFollowingP1)
   =
    count($vFollowingP1)
   ]"/>
================

  <xsl:copy-of select=
  "/*/p[@id='first']/following-sibling::*
     [not(self::p or self::ul
         or self::ol or self::blockquote
          )
     ] [1]
        /preceding-sibling::*
          [count(.| /*/p[@id='first']/following-sibling::*)
          =
           count(/*/p[@id='first']/following-sibling::*)
          ]

  "/>
 </xsl:template>
</xsl:stylesheet>

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

<html>
    <h1>Header</h1>
    <p>Paragraph 1</p>
    <p id="first">Paragraph 2</p>
    <p>Paragraph 3</p>
    <ul>
        <li>Item 1</li>
        <li>Item 2</li>
    </ul>
    <p>Paragraph 4</p>
    <hr/>
    <p>Paragraph 5</p>
    <blockquote>Blockquote 1</blockquote>
</html>

два выражения XPath (одно с переменными и одно со всеми замещенными переменными) вычисляются и выводятся нужные, правильные выбранные узлы :

<p>Paragraph 3</p>
<ul>
   <li>Item 1</li>
   <li>Item 2</li>
</ul>
<p>Paragraph 4</p>
================

  <p>Paragraph 3</p>
<ul>
   <li>Item 1</li>
   <li>Item 2</li>
</ul>
<p>Paragraph 4</p>

II. Решение XPath 2.0 :

$vFirstNotInRange/preceding-sibling::*
                              [. >> $vP1]

Это выбирает любой предшествующий брат $vFirstNotInRange, который также следует за $vP1, и выбирает те же самые требуемые узлы:

<p>Paragraph 3</p>
<ul>
   <li>Item 1</li>
   <li>Item 2</li>
</ul>
<p>Paragraph 4</p>

Объяснение : здесь мы используем XPath 2.0, оператор «следует» >>.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...