Поиск связанного текста в нескольких узлах XML
Я должен выполнить поиск в "упорядоченных" XML-файлах, где мой текст для ретрансляции передается по нескольким узлам, как это.
<root>
<div id="1">Hello</div>
<div id="2">Hel</div>
<div id="3">lo dude</div>
<div id="4">H</div>
<div id="5">el</div>
<div id="6">lo</div>
</root>
Поиск должен быть выполнен по объединенному тексту:
HelloHello dudeHello
Но мне нужно иметь возможность получать атрибуты узлов. Например, для поиска 'll' я хочу получить узлы:
<div id="1">Hello</div>
<div id="2">Hel</div>
<div id="3">lo dude</div>
<div id="5">el</div>
<div id="6">lo</div>
или, по крайней мере, идентификаторы.
У кого-то есть идея, как сделать это в XPath или каким-либо другим способом?
Я думаю, что это немного сложно, у меня нет (простой) идеи на данный момент. Спасибо за вашу помощь.
РЕДАКТИРОВАТЬ: текст должен быть объединен, прежде чем поиск является ключевой информацией и должен быть уточнен!
2 ответа
Ваши требования к обновлениям значительно усложняют проблему, поскольку "перенос элементов" может происходить в произвольных точках внутри маркера поиска и, возможно, даже охватывать несколько элементов. Я не думаю, что вы сможете написать запрос в XPath < 3.0 (если вы в любом случае сможете сделать это только в XPath). Я использовал для этого XQuery, который расширяет XPath. Код отлично работает в BaseX, но должен также работать во всех других движках XQuery (возможно, требуется XQuery 3.0, на это не смотрели).
Код стал довольно сложным, я думаю, я поместил достаточно комментариев, чтобы сделать его понятным. Для этого требуется, чтобы узлы находились внутри следующего элемента, но с небольшими корректировками его также можно использовать для обхода произвольных структур XML (вспомним HTML с <span/>
с и другая разметка).
(: functx dependencies :)
declare namespace functx = "http://www.functx.com";
declare function functx:is-node-in-sequence
( $node as node()? ,
$seq as node()* ) as xs:boolean {
some $nodeInSeq in $seq satisfies $nodeInSeq is $node
} ;
declare function functx:distinct-nodes
( $nodes as node()* ) as node()* {
for $seq in (1 to count($nodes))
return $nodes[$seq][not(functx:is-node-in-sequence(
.,$nodes[position() < $seq]))]
} ;
declare function local:search( $elements as item()*, $pattern as xs:string) as item()* {
functx:distinct-nodes(
for $element in $elements
return ($element[contains(./text(), $pattern)], local:start-search($element, $pattern))
)
};
declare function local:start-search( $element as item(), $pattern as xs:string) as item()* {
let $splits := (
(: all possible prefixes of search token :)
for $i in 1 to string-length($pattern) - 1
(: check whether element text starts with prefix :)
where ends-with($element/text(), substring($pattern, 1, $i))
return $i
)
(: go on for all matching prefixes :)
for $split in $splits
return
(: recursive call to next element :)
let $continue := local:continue-search($element/following-sibling::*[1], substring($pattern, $split+1))
where not(empty($continue))
return ($element, $continue)
};
declare function local:continue-search( $element as item()*, $pattern as xs:string) as item()* {
if (empty($element)) then () else
(: case a) text node contains whole remaining token :)
if (starts-with($element/text(), $pattern))
then ($element)
(: case b) text node is part of token :)
else if (starts-with($pattern, $element/text()))
then
(: recursive call to next element :)
let $continue := local:continue-search($element/following-sibling::*[1], substring($pattern, 1+string-length($element/text())))
where not(empty($continue))
return ($element, $continue)
(: token not found :)
else ()
};
let $token := 'll'
return local:search(//div, $token)
В XPath 2 вы можете использовать tokenize, чтобы подсчитать, как часто встречается искомый текст, а затем проверить для каждого узла, если этот узел не включен в текст, уменьшает количество вхождений. Если число уменьшается, этот узел должен быть включен в результат. Это не так быстро до конца.
Предполагая, что только текст в прямых дочерних узлах имеет значение, как в примере, это выглядит так:
for $searched in "ll"
return //*/ for $matches in count(tokenize(string-join(*, ""), $searched)) - 1
return *[$matches > count(tokenize(concat(" ",string-join(preceding-sibling::*, "")), $searched)) +
count(tokenize(concat(" ",string-join(following-sibling::*, "")), $searched)) - 2]