使用默认名称空间绑定对XML进行PHP xpath查询

使用默认名称空间绑定对XML进行PHP xpath查询,php,xml,xpath,domxpath,Php,Xml,Xpath,Domxpath,对于这个主题问题,我有一个解决方案,但这是一个破解,我想知道是否有更好的方法来解决这个问题 下面是一个示例XML文件和一个PHP CLI脚本,它执行作为参数给出的xpath查询。对于此测试用例,命令行为: ./xpeg "//MainType[@ID=123]" 最奇怪的是这句话,没有这句话,我的方法就行不通: $result->loadXML($result->saveXML($result)); 据我所知,这只是重新解析修改后的XML,在我看来,这是不必要的 有没有更好的方法

对于这个主题问题,我有一个解决方案,但这是一个破解,我想知道是否有更好的方法来解决这个问题

下面是一个示例XML文件和一个PHP CLI脚本,它执行作为参数给出的xpath查询。对于此测试用例,命令行为:

./xpeg "//MainType[@ID=123]"
最奇怪的是这句话,没有这句话,我的方法就行不通:

$result->loadXML($result->saveXML($result));
据我所知,这只是重新解析修改后的XML,在我看来,这是不必要的

有没有更好的方法在PHP中对该XML执行xpath查询


XML(请注意默认名称空间的绑定):


$0.20
N
$99.95
N
$600.00
N
$5000.00
N

PHP CLI脚本:

#!/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);
      }
    }
  }
}
?>
#/usr/bin/php cli
loadXML($xml);
//删除默认的命名空间绑定
$e=$domdoc->documentElement;
$e->RemoveAttribute($e->getAttributeNode(“xmlns”)->nodeValue“”;
//哈克,咳嗽,哈克
$domdoc->loadXML($domdoc->saveXML($domdoc));
$xpath=新的DOMXpath($domdoc);
$str=trim($argv[1]);
$result=$xpath->query($str);
如果($result!==FALSE){
dump_dom_levels($result);
}
否则{
回显“错误\n”;
}
//下面的函数实际上不是
//问题。它只是简单地提供了一个关于
//结果呢。
函数转储\u dom\u级别($node,$level=0){
$class=获取类($node);
如果($class==“DOMNodeList”){
echo“Level$Level($class):$node->length items\n”;
foreach($node作为$child\u节点){
dump_dom_levels($child_node,$level+1);
}
}
否则{
$nChildren=0;
foreach($node->childNodes作为$child\u节点){
if($child\u node->hasChildNodes()){
$nChildren++;
}
}
如果($n儿童){
echo“Level$Level($class):$nChildren children children\n”;
}
foreach($node->childNodes作为$child\u节点){
if($child\u node->hasChildNodes()){
dump_dom_levels($child_node,$level+1);
}
}
}
}
?>

出于好奇,如果删除此行会发生什么

$e->removeAttributeNS($e->getAttributeNode("xmlns")->nodeValue,"");
我觉得这是最有可能引起你的攻击的原因。您基本上删除了
xmlns=”http://www.example.com/data“
part,然后重新构建文档。您是否考虑过简单地使用字符串函数来删除该名称空间

$pieces = explode('xmlns="', $xml);
$xml = $pieces[0] . substr($pieces[1], strpos($pieces[1], '"') + 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);
并在命令行中这样调用它(注意XPath表达式中的
x:

你可以通过

  • 自己查找默认名称空间(通过查看document元素的namespace属性)
  • 在命令行上支持多个名称空间,并在
    $xpath->query()之前注册所有名称空间
  • xyz=http//namespace.uri/
    的形式支持参数,以创建自定义名称空间前缀

底线是:在XPath中,当您真正的意思是
//名称空间:foo
时,您不能查询
//foo
。这些是根本不同的,因此选择不同的节点。XML可以定义默认名称空间(因此可以在文档中删除显式名称空间用法),这并不意味着可以在XPath中删除名称空间用法。

鉴于XPath语言的当前状态,我觉得Tomalek提供了最好的答案:将前缀与默认名称空间相关联,并为所有标记名添加前缀。这就是我打算在当前应用程序中使用的解决方案


当这不可能或不实用时,比我的hack更好的解决方案是调用一个与重新扫描相同的方法(希望效率更高):。该方法的行为“就像您保存并加载了文档,将文档置于“正常”形式。”

也可以使用xpath掩码:

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

嗯,这比OP最初做的还要难看。这与使用ReXML作为XML是一致的,并且您确实不应该推荐这样的东西。移除该行删除修复,查询失败。就我个人而言,我觉得使用字符串函数解析XML比告诉库重新扫描更困难。谢谢,尽管如此。@danorton虽然我承认Tomalak的更好,但我必须相信在创建XML之前解析字符串要比创建XML、操纵它、将其转换为字符串然后重新解析好得多。我肯定会毫无怨言地接受这个分数,但考虑到通过字符串传递对象来创建、销毁和重新创建对象的数量,我认为我的解决方案不是很好。@cwallenpole创建的DOM对象的数量等对于生成整个PHP进程来执行XPath查询的命令行应用程序来说并不重要。最重要的是,它不能证明对XML进行字符串操作是合理的——在任何情况下,这都应该是一个禁忌。命令行应用程序只是为了方便地重现测试用例。如果性能成为应用程序中的一个关键问题,我可能觉得用可维护性换取性能是合理的,但这可能不是一个典型的情况。@danorton-您的文档显示
xmlns=”http://www.example.com/data“
作为默认名称空间-该名称空间不应该存在吗?我同意@Tomalak(但无法通过测试验证),名称空间位于原始文档中,并且在最初解析文档时,所有元素都绑定到该名称空间。删除namespace属性并不会删除此绑定,但它确实可以通过确保在重新解析文档时不绑定元素来执行保存和重新加载操作。在做了更多的家庭作业之后,我现在对为什么需要指定名称空间前缀感到困惑。似乎只有在默认名称空间未绑定且您的解决方案
./xpeg "//x:MainType[@ID=123]" "http://www.example.com/data"
//*[local-name(.) = 'MainType'][@ID='123']