Запрос PHP xpath на XML с привязкой пространства имен по умолчанию

У меня есть одно решение проблемы темы, но это взлом, и мне интересно, есть ли лучший способ сделать это.

Ниже приведен пример XML-файла и скрипта PHP CLI, который выполняет запрос xpath, заданный в качестве аргумента. Для этого тестового примера в командной строке:

./xpeg "//MainType[@ID=123]" 

Самое странное – это эта строка, без которой мой подход не работает:

 $result->loadXML($result->saveXML($result)); 

Насколько я знаю, это просто пере анализирует измененный XML, и мне кажется, что это не обязательно.

Есть ли лучший способ выполнить запросы xpath для этого XML в PHP?


XML ( обратите внимание на привязку пространства имен по умолчанию ):

 <?xml version="1.0" encoding="utf-8"?> <MyRoot xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.example.com/data http://www.example.com/data/MyRoot.xsd" xmlns="http://www.example.com/data"> <MainType ID="192" comment="Bob's site"> <Price>$0.20</Price> <TheUrl><![CDATA[http://www.example.com/path1/]]></TheUrl> <Validated>N</Validated> </MainType> <MainType ID="123" comment="Test site"> <Price>$99.95</Price> <TheUrl><![CDATA[http://www.example.com/path2]]></TheUrl> <Validated>N</Validated> </MainType> <MainType ID="922" comment="Health Insurance"> <Price>$600.00</Price> <TheUrl><![CDATA[http://www.example.com/eg/xyz.php]]></TheUrl> <Validated>N</Validated> </MainType> <MainType ID="389" comment="Used Cars"> <Price>$5000.00</Price> <TheUrl><![CDATA[http://www.example.com/tata.php]]></TheUrl> <Validated>N</Validated> </MainType> </MyRoot> 

PHP CLI Script:

 #!/usr/bin/php-cli <?php $xml = file_get_contents("xpeg.xml"); $domdoc = new DOMDocument(); $domdoc->loadXML($xml); // remove the default namespace binding $e = $domdoc->documentElement; $e->removeAttributeNS($e->getAttributeNode("xmlns")->nodeValue,""); // hack hack, cough cough, hack hack $domdoc->loadXML($domdoc->saveXML($domdoc)); $xpath = new DOMXpath($domdoc); $str = trim($argv[1]); $result = $xpath->query($str); if ($result !== FALSE) { dump_dom_levels($result); } else { echo "error\n"; } // The following function isn't really part of the // question. It simply provides a concise summary of // the result. function dump_dom_levels($node, $level = 0) { $class = get_class($node); if ($class == "DOMNodeList") { echo "Level $level ($class): $node->length items\n"; foreach ($node as $child_node) { dump_dom_levels($child_node, $level+1); } } else { $nChildren = 0; foreach ($node->childNodes as $child_node) { if ($child_node->hasChildNodes()) { $nChildren++; } } if ($nChildren) { echo "Level $level ($class): $nChildren children\n"; } foreach ($node->childNodes as $child_node) { if ($child_node->hasChildNodes()) { dump_dom_levels($child_node, $level+1); } } } } ?> 

Решение использует пространство имен, не избавляясь от него.

 $result = new DOMDocument(); $result->loadXML($xml); $xpath = new DOMXpath($result); $xpath->registerNamespace("x", trim($argv[2])); $str = trim($argv[1]); $result = $xpath->query($str); 

И назовите его как это в командной строке (обратите внимание на x: в выражении XPath)

 ./xpeg "//x:MainType[@ID=123]" "http://www.example.com/data" 

Вы можете сделать это более блестящим

  • самостоятельно определять пространства имен по умолчанию (просмотрев свойство пространства имен элемента документа)
  • поддерживая более одного пространства имен в командной строке и регистрируя их до $xpath->query()
  • поддерживающие аргументы в виде xyz=http//namespace.uri/ для создания пользовательских префиксов пространства имен

Итог: в XPath вы не можете запросить //foo когда вы действительно имеете в виду //namespace:foo . Они принципиально разные и поэтому выбирают разные узлы. Тот факт, что XML может иметь пространство имен по умолчанию, определенное (и, следовательно, может отказаться от использования явного использования пространства имен в документе), не означает, что вы можете отказаться от использования пространства имен в XPath.

Просто из любопытства, что произойдет, если вы удалите эту строку?

 $e->removeAttributeNS($e->getAttributeNode("xmlns")->nodeValue,""); 

Это поражает меня, как наиболее вероятно, чтобы вызвать необходимость в вашем хаке. Вы в основном удаляете часть xmlns="http://www.example.com/data" а затем повторно создаете DOMDocument. Рассматривали ли вы просто использование строковых функций для удаления этого пространства имен?

 $pieces = explode('xmlns="', $xml); $xml = $pieces[0] . substr($pieces[1], strpos($pieces[1], '"') + 1); 

Затем продолжайте свой путь? Это может даже оказаться быстрее.

Учитывая текущее состояние языка XPath, я считаю, что лучший ответ предоставлен Tomalek: связать префикс с пространством имен по умолчанию и префиксом всех имен тегов. Это решение, которое я намерен использовать в своем текущем приложении.

Когда это невозможно или практично, лучшим решением, чем мой взлом, является вызов метода, который делает то же самое, что и повторное сканирование (надеюсь, более эффективно): DOMDocument :: normalizeDocument () . Метод ведет себя «как если бы вы сохранили и затем загрузили документ, поставив документ в« нормальную »форму».

Также в качестве варианта вы можете использовать маску xpath:

 //*[local-name(.) = 'MainType'][@ID='123']