Найти положение узла в наборе узлов с помощью xpath

Поигравшись с position() напрасно, я искал решение в Google и пришел к этому старый вопрос о stackoverflow, который почти описывает мою проблему.

Разница в том, что набор узлов, в котором мне нужна позиция, является динамическим, а не непрерывным разделом документа.

Для иллюстрации я изменю пример из связанного вопроса, чтобы он соответствовал моим требованиям. Обратите внимание, что каждый элемент <b> находится внутри другого элемента <a>. Это критический момент.

<root>
    <a>
        <b>zyx</b>
    </a>
    <a>
        <b>wvu</b>
    </a>
    <a>
        <b>tsr</b>
    </a>
    <a>
        <b>qpo</b>
    </a>
</root>

Теперь, если бы я запросил, используя XPath для a/b, я бы получил набор узлов из четырех <b> узлов. Затем я хочу найти позицию в этом наборе узлов узла, который содержит строку 'tsr'. Решение в другом посте разбивается здесь: count(a/b[.='tsr']/preceding-sibling::*)+1 возвращает 1, потому что preceding-sibling перемещается по документу, а не по набору узлов контекста.

Можно ли работать в контекстном наборе узлов?


person philsquared    schedule 09.04.2010    source источник
comment
Умеете ли вы использовать XSLT 2.0 или застряли на 1.0?   -  person LarsH    schedule 18.08.2010


Ответы (6)


Вот общее решение, которое работает с любым узлом, принадлежащим любому набору узлов в том же документе:

Я использую XSLT для реализации решения, но в итоге получаю одно выражение XPath, которое можно использовать с любым другим языком хостинга.

Пусть $vNodeSet будет набором узлов, а $vNode будет узлом в этом наборе узлов, положение которого мы хотим найти.

Затем пусть $vPrecNodes содержит все узлы в документе XML, предшествующие $vNode.

Затем пусть $vAncNodes содержит все узлы в документе XML, которые являются предками $vNode.

Набор узлов в $vNodeSet, который предшествует $vNode в порядке документа, состоит из всех узлов в наборе узлов, которые также принадлежат $vPrecNodes, и всех узлов в наборе узлов, которые также принадлежат $vAncNodes.

Я буду использовать известную формулу Кайсиана для пересечения двух наборов узлов:

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

содержит точно узлы на пересечении $ns1 с $ns2.

Исходя из всего этого, пусть $vPrecInNodeSet - это набор узлов в $vNodeSet, которые предшествуют $vNode в порядке документа. Следующее выражение XPath определяет $vPrecInNodeSet:

$vNodeSet
      [count(.|$vPrecNodes) = count($vPrecNodes)
      or
       count(.|$vAncNodes) = count($vAncNodes)
      ]

Наконец, желаемая позиция: count($vPrecInNodeSet) +1

Вот как все это работает вместе:

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

 <xsl:variable name="vNodeSet" select="/*/a/b"/>

 <xsl:variable name="vNode" select="$vNodeSet[. = 'tsr'][1]"/>

 <xsl:variable name="vPrecNodes" select="$vNode/preceding::node()"/>

 <xsl:variable name="vAncNodes" select="$vNode/ancestor::node()"/>

 <xsl:variable name="vPrecInNodeSet" select=
  "$vNodeSet
      [count(.|$vPrecNodes) = count($vPrecNodes)
      or
       count(.|$vAncNodes) = count($vAncNodes)
      ]
  "/>

 <xsl:template match="/">
   <xsl:value-of select="count($vPrecInNodeSet) +1"/>
 </xsl:template>
</xsl:stylesheet>

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

<root>
    <a>
        <b>zyx</b>
    </a>
    <a>
        <b>wvu</b>
    </a>
    <a>
        <b>tsr</b>
    </a>
    <a>
        <b>qpo</b>
    </a>
</root>

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

3

Обратите внимание: это решение не зависит от XSLT (используется только в иллюстративных целях). Вы можете собрать одно выражение XPath, заменяя переменные их определением, пока не останется больше переменных для замены.

person Dimitre Novatchev    schedule 25.08.2010
comment
Отличный ответ, как и ожидалось. Это методы, которые я видел раньше, но недостаточно хорошо владел, чтобы придумывать их при необходимости. Ради интереса я попытался расширить переменные, чтобы посмотреть, как выглядит эквивалент только для XPath: count(/*/a/b[count(. | /*/a/b[. = 'tsr'][1]/preceding::node()) = count(/*/a/b[. = 'tsr'][1]/preceding::node()) or count(. | /*/a/b[. = 'tsr'][1]/ancestor::node()) = count(/*/a/b[. = 'tsr'][1]/ancestor::node()) ]) + 1 (около 212 символов). Для безопасности может потребоваться заключить в скобки значение $ vNodeSet, например если его последняя ось обратная. - person LarsH; 25.08.2010

Думаю, у меня есть рабочее решение

Идея состоит в том, чтобы подсчитать, сколько элементов предшествует нашему целевому элементу в документе, и подсчитать, сколько узлов в наборе узлов имеет меньшее или равное количество предшествующих элементов. В XPath это:

count(//a/b[count(./preceding::node()) &lt;= count(//a/b[.='tsr']/preceding::node())])

Вы также можете использовать переменные в этом выражении, чтобы найти разные наборы узлов или сопоставить различное текстовое содержимое. Важной частью здесь является то, что переменные имеют правильный тип. Ниже приведен пример XSLT и пример вывода с использованием примера документа вопроса в качестве входного файла.

Документ XSLT

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

    <xsl:output encoding="utf-8" method="text"/>

    <xsl:variable name="nodeset" select="//a/b"/>
    <xsl:variable name="path-string">//a/b</xsl:variable>
    <xsl:variable name="text">tsr</xsl:variable>

    <xsl:template match="/">
        <xsl:text>Find and print position of a node within a nodeset&#10;&#10;</xsl:text>

        <xsl:text>Position of "tsr" node in the nodeset = "</xsl:text>
        <xsl:value-of select="count(//a/b[count(./preceding::node()) &lt;= count(//a/b[.='tsr']/preceding::node()) ])"/>
        <xsl:text>"&#10;&#10;</xsl:text>

        <xsl:text>( Try the same using variables "$nodeset" and "$text" )&#10;</xsl:text>
        <xsl:text>Size of nodeset "$nodeset" = "</xsl:text>
        <xsl:value-of select="count($nodeset)"/>
        <xsl:text>"&#10;</xsl:text>
        <xsl:text>Variable "$text" = "</xsl:text>
        <xsl:value-of select="$text"/>
        <xsl:text>"&#10;</xsl:text>
        <xsl:text>Position of "</xsl:text>
        <xsl:value-of select="$text"/>
        <xsl:text>" node in the nodeset = "</xsl:text>
        <xsl:value-of select="count($nodeset[count(./preceding::node()) &lt;= count($nodeset[.=$text]/preceding::node()) ])"/>
        <xsl:text>"&#10;&#10;</xsl:text>

        <xsl:text>( Show that using a variable that has the path as a string does not work )&#10;</xsl:text>
        <xsl:text>Variable "$path-string" = "</xsl:text>
        <xsl:value-of select="$path-string"/>
        <xsl:text>"&#10;</xsl:text>
        <xsl:text>Result of "count($path-string)" = "</xsl:text>
        <xsl:value-of select="count($path-string)"/>
        <xsl:text>"&#10;&#10;</xsl:text>

        <xsl:text>End of tests&#10;</xsl:text>
    </xsl:template>

</xsl:stylesheet>

Вывод из образца документа

Find and print position of a node within a nodeset

Position of "tsr" node in the nodeset = "3"

( Try the same using variables "$nodeset" and "$text" )
Size of nodeset "$nodeset" = "4"
Variable "$text" = "tsr"
Position of "tsr" node in the nodeset = "3"

( Show that using a variable that has the path as a string does not work )
Variable "$path-string" = "//a/b"
Result of "count($path-string)" = "1"

End of tests

Я не тщательно тестировал свое решение, поэтому, пожалуйста, оставьте отзыв, если вы его используете.

person jasso    schedule 18.08.2010
comment
+1 Умное решение. Это может стать медленным для больших наборов узлов, O (m * n), где m - это узлы в рабочем наборе, а n - узлы в документе. Однако он должен работать для любых выражений XPath. Вам нужно дважды указать XPath для рабочего набора узлов, но вы делаете это точно так же, чтобы это не было подвержено ошибкам. Единственный случай, когда этот подход может не работать, - это если вы выбираете узлы, которые не являются элементами и не имеют промежуточных элементов: например, выбор текстовых узлов среди узлов комментариев. Но если вы измените preceding::* на preceding::node(), вы будете в хорошей форме. - person LarsH; 19.08.2010
comment
Я хотел сказать, что этот ответ примечателен тем, что он использует только XPath. Однако, если вы можете использовать XSLT или другие сопутствующие инструменты, мой ответ, вероятно, будет более эффективным. :-) - person LarsH; 19.08.2010
comment
@LarsH Спасибо, я стремился к решению только для XPath. В целом я также считаю, что по возможности лучшим решением будет зацикливание набора в шаблоне XSLT. - person jasso; 21.08.2010
comment
Я отредактировал решение, чтобы оно соответствовало вашему предложению. Первоначально я не использовал node(), потому что начал сомневаться в результатах, если бы выбор содержал также атрибуты или узлы пространства имен. Я понял, что у них действительно нет порядка в XML-документе, и положение может варьироваться в зависимости от реализации синтаксического анализатора / XSLT-процессора. Например, было бы ошибкой синтаксического анализатора всегда читать XML как канонический, что привело бы к перегруппировке атрибутов по имени, а не по имени, написанному в документе. В конце концов, не останется ли модель документа прежней? - person jasso; 21.08.2010
comment
Верно, что атрибуты и узлы пространства имен (в одном элементе) не имеют определенного порядка документов в XSLT. (Я мало знаю о каноническом XML и считаю, что это отдельная проблема.) Однако это не влияет на наш вопрос, если только вы не пытаетесь найти положение узла атрибута или пространства имен среди набора узлов, который включает другой атрибут / узлы пространства имен на одном элементе; и в этом случае ответ не определен, поэтому никакая реализация не может правильно его вычислить. - person LarsH; 25.08.2010

Предыдущие ответы «count-the-previous (-sibling)» хорошо работают в некоторых случаях; вы просто повторно указываете контекстный набор узлов с точки зрения выбранного элемента, а затем применяете к нему count(preceding:: ).

Но в других случаях, как вы намекали, действительно сложно уложиться в набор узлов, с которым вы хотите работать. Например. предположим, что ваш рабочий набор узлов был / html / body / div [3] // a (все <a> якоря в третьем <div> веб-странице), и вы хотели найти положение a[@href="foo.html"] в этом наборе. Если вы попытаетесь использовать count(preceding::a), вы случайно будете подсчитывать <a> якорей из других div, то есть вне вашего рабочего набора узлов. И если вы попробуете count(preceding-sibling::a), вы не получите их всех, потому что соответствующие элементы <a> могут быть на любом уровне.

Вы можете попытаться ограничить счет с помощью preceding::a[ancestor::div[count(preceding-sibling::div) = 2]], но это быстро становится очень неудобно и все равно будет невозможно во всех случаях. Более того, вам придется переработать это выражение, если вы когда-нибудь обновите выражение XPath для своего рабочего набора, и сохранить их эквивалентными было бы нетривиально.

Однако, если вы используете XSLT, эти проблемы можно избежать следующим образом. Если вы можете указать рабочий набор узлов, вы можете найти положение узла в нем, соответствующее указанным критериям. И вам не нужно указывать набор узлов дважды:

    <xsl:for-each select="/root/a/b">
        <xsl:if test=". = 'tsr'"><xsl:value-of select="position()"/></xsl:if>
    </xsl:for-each>

Это работает, потому что внутри for-each позиция контекста «определяет позицию элемента контекста в обрабатываемой последовательности».

Если вы не работаете в XSLT, в какой среде вы находитесь? Вероятно, существует аналогичная конструкция для итерации по результату внешнего выражения XPath, и там вы можете поддерживать свой собственный счетчик (если нет доступной позиции контекста) и проверять каждый элемент на соответствие вашим внутренним критериям.

Причина, по которой другой парень пытается ответить на старый вопрос, a/b[.='tsr']/position(), не сработало, потому что при каждой косой черте новый контекст помещается в стек, поэтому, когда вызывается position (), позиция контекста всегда равна 1. (Кстати, этот синтаксис работает только в XPath 2.0.)

person LarsH    schedule 19.08.2010
comment
Хорошее объяснение (+1). Может мой ответ будет вам интересен? - person Dimitre Novatchev; 25.08.2010
comment
@Dimitre: Я с нетерпением жду возможности переварить это. И узнать правильный ответ. :-) - person LarsH; 25.08.2010

Причина, по которой вы получаете 1, не имеет ничего общего с контекстом и документом, а потому, что вы подсчитываете только b узлов в пределах одного a узла (так что вы всегда будете получать счет 0, потому что никогда не бывает никаких предшествующих узлов 'b'.

Скорее вам нужно найти количество предшествующих узлов 'a' перед 'b', который содержит ваш 'a'.

Что-то типа:

count(a[b[.='tsr']]/preceding-sibling::a)
person Richard    schedule 09.04.2010
comment
Что ж, в данном случае это счет «а», но я не хочу считать это на самом деле. Я хочу подсчитать количество букв в моем наборе контекстных узлов. Если вы представите себе, что некоторые из узлов 'a' содержат более одного узла 'b', это может привести к другому счету. - person philsquared; 09.04.2010
comment
@Tomalak - ближе - но это не дает никаких дополнительных узлов ‹b›, которые могут быть в пределах текущего ‹a› (но до соответствующего) - person philsquared; 09.04.2010
comment
@Phil: Просто добавьте /b, чтобы включить b потомков предыдущих a в счетчик (в конце). Ключевым моментом здесь является то, что вам нужно начать с a родителя вашего b, а не непосредственно с b .. - person Richard; 09.04.2010
comment
благодаря. Все еще пытаюсь разобраться в этом (мои мысли переключились на другие вещи. Думаю, я понимаю, о чем вы говорите, но все еще не знаю, как соединить это воедино) - person philsquared; 09.04.2010

От (т.е. против) корня:

count(//a/b[.='tsr']/preceding::b)

Если бы вы сказали другой узел, например:

<c>
    <b>qqq</b>
</c>

и хотел игнорировать все элементы, не имеющие родителя "a", вы могли бы сделать что-то вроде

count(//a/b[.='tsr']/preceding::b[local-name(parent::node())='a'])

и т.д

person dingo99    schedule 28.07.2010

Как насчет этого..

count(a/b[.='tsr']/preceding-sibling::b) + count(a[b[.='tsr']]/preceding-sibling::a/b) + 1

Подсчитайте предыдущих братьев и сестер элемента b в текущем элементе a, а затем подсчитайте элементы b всех предыдущих братьев и сестер элемента a. Или что-то вроде того.

person Tim C    schedule 09.04.2010