Почему XML:: LibXML не находит узлов для этого запроса xpath при использовании пространства имен


Я пытаюсь выбрать узел с помощью запроса XPath, и я не понимаю, почему XML:: LibXML не находит узел, когда он имеет атрибут xmlns. Вот сценарий для демонстрации проблемы:

#!/usr/bin/perl

use XML::LibXML; # 1.70 on libxml2 from libxml2-dev 2.6.16-7sarge1 (don't ask)
use XML::XPath;  # 1.13
use strict;
use warnings;

use v5.8.4; # don't ask

my ($xpath, $libxml, $use_namespace) = @ARGV;

my $xml = sprintf(<<'END_XML', ($use_namespace ? 'xmlns="http://www.w3.org/2000/xmlns/"' : q{}));
<?xml version="1.0" encoding="iso-8859-1"?>
<RootElement>
  <MyContainer %s>
    <MyField>
        <Name>ID</Name>
        <Value>12345</Value>
    </MyField>
    <MyField>
        <Name>Name</Name>
        <Value>Ben</Value>
    </MyField>
  </MyContainer>
</RootElement>
END_XML

my $xml_parser
    = $libxml ? XML::LibXML->load_xml(string => $xml, keep_blanks => 1)
    :           XML::XPath->new(xml => $xml);

my $nodecount = 0;
foreach my $node ($xml_parser->findnodes($xpath)) {
    $nodecount ++;
    print "--NODE $nodecount--n"; #would use say on newer perl
    print $node->toString($libxml && 1), "n";
}

unless ($nodecount) {
    print "NO NODES FOUNDn";
}

Этот скрипт позволяет выбирать между синтаксическим анализатором XML:: LibXML и синтаксическим анализатором XML::XPath. Он также позволяет определить атрибут xmlns для элемента MyContainer или оставить его в зависимости от переданных аргументов.

Выражение xpath, которое я использую, является "RootElement/MyContainer". Когда я запускаю запрос с помощью синтаксического анализатора XML::LibXML без пространства имен, он находит узел без проблем:

benb@enkidu:~$ ROC/ECG/libxml_xpath.pl 'RootElement/MyContainer' libxml
--NODE 1--
<MyContainer>
    <MyField>
        <Name>ID</Name>
        <Value>12345</Value>
    </MyField>
    <MyField>
        <Name>Name</Name>
        <Value>Ben</Value>
    </MyField>
  </MyContainer>

Однако, когда я запускаю его с пространством имен на месте, он не находит узлов:

benb@enkidu:~$ ROC/ECG/libxml_xpath.pl 'RootElement/MyContainer' libxml use_namespace
NO NODES FOUND

Сравните это с выводом при использовании xmll:: XPath parser:

benb@enkidu:~$ ROC/ECG/libxml_xpath.pl 'RootElement/MyContainer' 0 # no namespace
--NODE 1--
<MyContainer>
    <MyField>
        <Name>ID</Name>
        <Value>12345</Value>
    </MyField>
    <MyField>
        <Name>Name</Name>
        <Value>Ben</Value>
    </MyField>
  </MyContainer>
benb@enkidu:~$ ROC/ECG/libxml_xpath.pl 'RootElement/MyContainer' 0 1 # with namespace
--NODE 1--
<MyContainer xmlns="http://www.w3.org/2000/xmlns/">
    <MyField>
        <Name>ID</Name>
        <Value>12345</Value>
    </MyField>
    <MyField>
        <Name>Name</Name>
        <Value>Ben</Value>
    </MyField>
  </MyContainer>

Какая из этих реализаций парсера делает это "правильно"? Почему XML:: LibXML трактует его иначе, когда я использую пространство имен? Что я могу сделать, чтобы получить узел, когда пространство имен на месте?

3 6

3 ответа:

Это часто задаваемые вопросы. XPath считает, что любое незафиксированное имя в выражении принадлежит "no namespace".

Тогда выражение:

RootElement/MyContainer

Выбирает все элементы MyContainer, принадлежащие "no namespace" и являющиеся дочерними элементами всех элементов RootElement, принадлежащих "no namespace" и являющихся дочерними элементами контекста (текущего узла). Тем не менее, во всем документе нет элементов, которые принадлежат "no namespace" - все элементы принадлежат пространству имен по умолчанию.

Это объясняет результат, который вы получаете. XML:: LibXML - это правильно.

Общее решение состоит в том, что API языка хостинга позволяет привязать определенный префикс к пространству имен путем "регистрации" пространства имен. Тогда можно использовать выражение типа:

x:RootElement/x:MyContainer

Где x - префикс, с которым было зарегистрировано пространство имен.

В очень редких случаях, когда язык хостинга не предлагает регистрировать пространства имен, используйте следующее выражение:

*[name()='RootElement']/*[name()='MyContainer']

@Dmitre прав. Вам нужно взглянуть на XML:: LibXML:: XPathContext , который позволит вам объявить пространство имен, а затем вы можете использовать операторы XPath с учетом пространства имен. Я привел пример использования этого некоторое время назад на stackoverflow-посмотрите на Почему я должен использовать XPathContext с XML Perl:: LibXML

Использование XML:: LibXML 1.69.

Может быть, это XML:: LibXML 1.69 вещь, но странная часть заключается в том, что я могу использовать обычный XPath и findnodes() и код ниже печатает узлы.

use strict;
use XML::LibXML;

my $xml = <<END_XML;
<?xml version="1.0" encoding="iso-8859-1"?>
<RootElement>
   <MyContainer xmlns="http://www.w3.org/2000/xmlns/">
    <MyField>
        <Name>ID</Name>
        <Value>12345</Value>
    </MyField>
    <MyField>
        <Name>Name</Name>
        <Value>Ben</Value>
    </MyField>
  </MyContainer>
</RootElement>
END_XML

my $parser = XML::LibXML->new();

$parser->recover_silently(1);

my $doc = $parser->parse_string($xml);

my $root = $doc->documentElement();

foreach my $node ($root->findnodes('MyContainer/MyField')) {
     print $node->toString();
}

Но если я изменю пространство имен на что-то другое, чем "http://www.w3.org/2000/xmlns/", затем с помощью XML:: LibXML:: XPathContext требуется получить те же узлы для печати.

use strict;
use XML::LibXML;

my $xml = <<END_XML;
<?xml version="1.0" encoding="iso-8859-1"?>
<RootElement>
  <MyContainer xmlns="http://something.org/2000/something/">
    <MyField>
        <Name>ID</Name>
        <Value>12345</Value>
    </MyField>
    <MyField>
        <Name>Name</Name>
        <Value>Ben</Value>
    </MyField>
  </MyContainer>
</RootElement>
END_XML

my $parser = XML::LibXML->new();

$parser->recover_silently(1);

my $doc = $parser->parse_string($xml);

my $root = $doc->documentElement();

my $xpc = XML::LibXML::XPathContext->new($root);

$xpc->registerNs("x", "http://something.org/2000/something/");

foreach my $node ($xpc->findnodes('x:MyContainer/x:MyField')) {
    print $node->toString();
}