SimpleXML中的XPath用于默认命名空间而无需使用前缀

9

我有一个带有默认命名空间的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]');

我需要注册命名空间,将其分配给一个前缀,然后在我的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
@JLRishe 我尽可能简化了问题。XPath当前在XLS中。我们可能最终会自动化这个过程,因此系统将读取XLS、XML文件目录,然后摄入所有数据映射。我认为通过代码添加XPath前缀容易出错。 - mpdonadio
你使用的生成XLS的过程是否可以修改,以便在XPaths中包含前缀? - JLRishe
@JLRishe 再次简化。XLS 将来自第三方(由第四方提供输入),XPath 已经在他们的系统中。我没有看到该过程的任何部分会发生变化,因此我的问题实际上与 SimpleXML 和 XPath 如何使用默认命名空间有关。 - mpdonadio
3个回答

12

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

XPath 1.0不包括“默认”命名空间的概念,因此,无论元素名称在XML源中如何出现,如果它们绑定了命名空间,则必须在基本XPath选择器中添加前缀ns:name。请注意, ns 是由XPath处理器内部定义的前缀,与正在处理的文档中使用 xmlns 属性的方式无关。

例如,请参见这个“常见的XSLT错误”页面,其中讨论了密切相关的XSLT 1.0:

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

根据类似问题的答案,XPath 2.0确实包含“默认命名空间”的概念,在XSLT页面中也提到了这一点,但PHP中所有内置的XML扩展都是基于libxml2libxslt库构建的,这些库仅支持XPath和XSLT的1.0版本。因此,除了预处理文档以不使用命名空间之外,您唯一的选择就是找到一个XPath 2.0处理器,可以将其插入到PHP中。(另外值得注意的是,如果您的XML文档中有未带前缀的属性,它们并不属于默认命名空间,而是根本没有命名空间;请参见XML命名空间和未带前缀的属性以讨论这个命名空间规范的奇怪之处。)

2
有没有一种适当的方式来处理默认命名空间而不需要在XPath中使用前缀呢?
没有。处理任何命名空间的适当方式是将某个值(前缀)与该命名空间关联起来,以便可以在XPath表达式中明确选择它。默认命名空间也不例外。
这样想:某个命名空间中的元素和另一个命名空间中具有相同名称的元素(或根本没有命名空间)是不同的元素。它们可能表示不同的东西。这就是重点。您需要告诉XPath您要选择哪一个。如果没有它,XPath不知道您要求什么。
添加前缀在长期内并不可管理。
我真的不知道为什么。创建XPath表达式的任何工具都应该能够指定正确的XPath表达式(否则它就是一个有问题的工具)。
您可能会想:“为什么我不能忽略命名空间并获取所有匹配该名称的元素?” 这确实有一些hacky的方法(如已发布的基于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方法,我需要注册命名空间并分配一个前缀。 - mpdonadio
3
这不是PHP的错,这是XPath规范所要求的。你必须指定一个前缀才能选择命名空间中的节点。如果PHP允许你用其他方式来完成,那么无论他们称之为什么,它将不再符合规范定义的XPath了。 - Wayne
1
那么,如何为没有前缀的命名空间分配前缀的语法是什么? - ahnbizcad

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。

如果只想要剥离命名空间(声明和前缀),PHP的DOM API可以在几行代码中给出相同的结果。 - salathe
1
@salathe 如果是这样的话,请不要客气,给我们指点一下。 - JLRishe

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接