SimpleXML中的XPath,用于默认名称空间,无需前缀

时间:2014-01-15 17:09:58

标签: php xml xpath namespaces simplexml

我有一个附加了默认命名空间的XML文档,例如

<foo xmlns="http://www.example.com/ns/1.0">
...
</foo>

实际上,这是一个符合复杂模式的复杂XML文档。我的工作是从中解析出一些数据。为了帮助我,我有一个XPath的电子表格。 XPath是相当深层嵌套的,例如

level1/level2/level3[@foo="bar"]/level4[@foo="bar"]/level5/level6[2]

生成XPath的人是模式的专家,所以我假设我无法简化它,或使用对象遍历快捷方式。

我正在使用SimpleXML解析所有内容。我的问题与如何处理默认命名空间有关。

由于根元素上有一个默认命名空间,我不能只做

$xml = simplexml_load_file($somepath);
$node = $xml->xpath('level1/level2/level3[@foo="bar"]/level4[@foo="bar"]/level5/level6[2]');

我必须register the namespace,将其分配给前缀,然后在我的XPath中使用前缀,例如

$xml = simplexml_load_file($somepath);
$xml->registerXPathNamespace('myns', 'http://www.example.com/ns/1.0');
$node = $xml->xpath('myns:level1/myns:level2/myns:level3[@foo="bar"]/myns:level4[@foo="bar"]/myns:level5/myns:level6[2]');

从长远来看,添加前缀不会是可管理的。

是否有正确的方法来处理默认名称空间而无需使用XPath前缀?

使用空前缀不起作用($xml->registerXPathNamespace('', 'http://www.example.com/ns/1.0');)。我可以输出默认的命名空间,例如

$xml = file_get_contents($somepath);
$xml = str_replace('xmlns="http://www.example.com/ns/1.0"', '', $xml);
$xml = simplexml_load_string($xml);

但是这就是问题所在。

3 个答案:

答案 0 :(得分:11)

从在线阅读中,这不仅限于任何特定的PHP或其他库,而是限于XPath本身 - 至少在XPath版本1.0中

XPath 1.0不包含任何“默认”命名空间的概念,因此无论元素名称如何出现在XML源中,如果它们具有绑定到它们的命名空间,则它们的选择器必须以基本XPath选择器为前缀形式为ns:name。请注意,ns是XPath处理器中定义的前缀,而不是正在处理的文档,因此与XML表示中的xmlns属性的使用方式无关。

参见例如this "common XSLT mistakes" page,谈论密切相关的XSLT 1.0:

  

要在XPath中访问命名空间元素,必须为其命名空间定义前缀。 [...]不幸的是,XSLT 1.0版没有类似于默认命名空间的概念;因此,您必须反复重复名称空间前缀。

根据an answer to a similar question,XPath 2.0 确实包含“默认命名空间”的概念,上面链接的XSLT页面也在XSLT 2.0的上下文中提到了这一点。

不幸的是,PHP中的所有内置XML扩展都建立在libxml2libxslt库的基础之上,这些库仅支持1.0版的XPath和XSLT。

除了预处理文档而不使用命名空间之外,你唯一的选择就是找到一个可以插入PHP的XPath 2.0处理器。

(另外,值得注意的是,如果您在XML文档中有未加前缀的属性,那么它们在技术上并不属于默认命名空间,而是根本没有命名空间;请参阅{{3讨论命名空间规范的这种奇怪之处。)

答案 1 :(得分:2)

  

是否有正确的方法来处理默认名称空间而无需   使用带有XPath的前缀?

没有。处理任何命名空间的正确方法是将某个值(前缀)与该命名空间相关联,以便可以在XPath表达式中显式选择它。默认命名空间没有什么不同。

以这种方式思考:某个命名空间中的元素和其他某个命名空间中具有相同名称的另一个元素(或根本没有命名空间)是不同的元素。它们可能意味着(即代表)不同的事物。这就是重点。您需要告诉XPath 您要选择哪一个。没有它,XPath不知道你要求的是什么。

  

从长远来看,添加前缀不会是可管理的。

我真的不明白为什么。无论什么创建XPath表达式都应该能够指定一个正确的XPath表达式(或者它是一个破坏的工具)。

您可能会想,“为什么我不能忽略命名空间并获取与该名称匹配的所有元素?”有很多方法可以做到这一点(比如基于XSLT的答案)发布了),但他们是破碎的设计。 XML中的元素由其命名空间和本地名称的组合来标识,就像您的房子可以在某个城市和州(命名空间)中使用街道号(本地名称)来标识一样。如果我告诉你我住在422 Main St,那么你仍然不知道我住在哪里,直到我告诉你哪个城市和州。

你仍然可能会想,“足够愚蠢的类比,我真的,真的想要这样做。”您可以通过仅匹配元素的本地名称部分来选择所有名称空间中具有给定名称的元素,如下所示:

*[local-name()='level1']/*[local-name()='level2']
    /*[local-name()='level3' and @foo="bar"]/*[local-name()='level4' and 
        @foo="bar"]/*[local-name()='level5']/*[local-name()='level6'][2]');

请注意,这不会将自身限制为默认命名空间。它完全忽略命名空间。这很难看,我不推荐它,但有时你只想忽略最好的东西并完成一些事情。

顺便说一下,这不是PHP的错。这是XPath规范所要求的。您必须指定一个前缀来选择命名空间中的节点。如果PHP允许你以其他方式执行,那么无论他们称之为什么,它都将不再是XPath(根据规范)。

答案 2 :(得分:0)

为了避免像你所拥有的str_replace那样的黑客攻击(我建议避免使用),你可以通过XSLT运行XML文件来删除命名空间:

<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
    xmlns:myns="http://www.example.com/ns/1.0">
  <xsl:output method="xml" indent="yes" omit-xml-declaration="yes"/>

  <xsl:template match="@* | node()">
    <xsl:copy>
      <xsl:apply-templates select="@* | node()" />
    </xsl:copy>
  </xsl:template>

  <xsl:template match="myns:*">
    <xsl:element name="{local-name()}">
      <xsl:apply-templates select="@* | node()" />
    </xsl:element>
  </xsl:template>
</xsl:stylesheet>

在以下任何一个输入上运行时:

<foo xmlns="http://www.example.com/ns/1.0">
  <a>
    <child attr="5"></child>
  </a>
</foo>

<ex:foo xmlns:ex="http://www.example.com/ns/1.0">
  <ex:a>
    <ex:child attr="5"></ex:child>
  </ex:a>
</ex:foo>

输出相同:

<foo>
  <a>
    <child attr="5" />
  </a>
</foo>

这将允许您在结果上使用无前缀的XPath。