PHP-запрос xpath на XML с привязкой пространства имен по умолчанию - PullRequest
4 голосов
/ 25 июня 2011

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

Ниже приведен пример 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);
      }
    }
  }
}
?>

Ответы [ 4 ]

11 голосов
/ 25 июня 2011

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

$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"

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

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

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

1 голос
/ 25 июня 2011

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

$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);

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

0 голосов
/ 03 марта 2017

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

//*[local-name(.) = 'MainType'][@ID='123']
0 голосов
/ 29 июня 2011

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

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

...