SimpleXML 中的 XPath 用于默认命名空间,无需前缀
Posted
技术标签:
【中文标题】SimpleXML 中的 XPath 用于默认命名空间,无需前缀【英文标题】:XPath in SimpleXML for default namespaces without needing prefixes 【发布时间】:2014-02-04 07:13:42 【问题描述】:我有一个附加了默认命名空间的 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);
但这是在回避问题。
【问题讨论】:
“从长远来看,添加前缀将无法管理”是什么意思?这是为什么呢? @JLRishe 我试图尽可能简化问题。 XPath 当前位于 XLS 中。我们最终可能会自动化该过程,因此系统将读取 XML 文件目录 XLS,然后注入所有数据映射。我发现通过代码向 XPath 添加前缀很容易出错。 能否修改用于生成 XLS 的过程以使 XPath 包含前缀? @JLRishe 再次简化。 XLS 将来自第三方(来自第四方的输入),并且 XPath 已经在他们的系统中。我没有看到该过程的任何部分发生变化,所以我的问题确实与 SimpleXML 和 XPath 如何与默认命名空间一起使用有关。 【参考方案1】:是否有适当的方法来处理默认命名空间而无需 在 XPath 中使用前缀?
没有。处理 any 命名空间的正确方法是将某个值(前缀)与该命名空间相关联,以便可以在 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(根据规范)。
【讨论】:
谢谢,我得到了命名空间的类比。我对 PHP 处理这个问题的方式感到很困惑。如果文档上有默认命名空间,那么我可以使用 SimpleXML 的对象遍历来获取元素,而无需显式提供命名空间或在各种方法上使用$ns
参数。但是,如果我想在 SimpleXML 中对同一个文档使用->xpath
方法,我需要注册命名空间并为其分配前缀。
这不是 PHP 的错。这是 XPath 规范所要求的。您必须指定前缀以选择命名空间中的节点。如果 PHP 允许您以其他方式进行操作,那么无论他们如何称呼它,它都不再是 XPath(根据规范)。
那么将前缀分配给没有前缀的命名空间的语法是什么?【参考方案2】:
通过在线阅读,这不限于任何特定的 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 扩展都是基于 libxml2 和 libxslt 库构建的,它们仅支持 1.0 版的 XPath 和 XSLT。
因此,除了对文档进行预处理以不使用名称空间之外,您唯一的选择是找到可以插入 PHP 的 XPath 2.0 处理器。
(顺便说一句,值得注意的是,如果您的 XML 文档中有无前缀的 属性,从技术上讲,它们不在默认命名空间中,而是在任何命名空间中;参见 XML Namespaces and Unprefixed Attributes用于讨论命名空间规范的这种奇怪之处。)
【讨论】:
【参考方案3】:为了避免像 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。
【讨论】:
如果只需要剥离命名空间(声明和前缀),PHP 的 DOM API 可以在几行中给出相同的结果。 @salathe 如果是这样的话,那么请大家多多指教。以上是关于SimpleXML 中的 XPath 用于默认命名空间,无需前缀的主要内容,如果未能解决你的问题,请参考以下文章