函数讲解)
前言和getName()、getDocNamespaces()一样getNamespaces()也不是全局函数而是SimpleXMLElement类上的方法SimpleXMLElement::getNamespaces()。它返回当前元素以及可选的后代元素用到的 XML 命名空间列表。它被需要的原因只有一个但这个原因足够硬XPath 1.0 表达式里的无前缀名字永远只匹配「不属于任何命名空间」的节点。现实中的 XMLAtom、SOAP、RSS、各种行业标准几乎都声明了默认命名空间或前缀于是直接写//entry什么都选不到。你必须先把命名空间 URI 和一个前缀绑定起来才能查询。而「文档里到底有哪些命名空间」这件事就要靠getNamespaces()和getDocNamespaces()来问。这两个方法名字太像是使用中最主要的混乱来源。本文会把它们的区别讲透并给出「枚举命名空间 → 注册给 XPath → 查询」这条完整链路。一、签名与返回形状public SimpleXMLElement::getNamespaces(bool $recursive false): array参数$recursive默认false只看当前元素可见的命名空间传true时会把当前元素及其所有后代用到的命名空间都收进来。返回值数组。数组的形式是「前缀 命名空间 URI」的映射。?php // 适用于 PHP 8.0需要 ext-simplexml$xml XML?xml version1.0 encodingUTF-8?feed xmlnshttp://www.w3.org/2005/Atom xmlns:dchttp://purl.org/dc/elements/1.1/entrytitle第一篇/titledc:creatoralice/dc:creator/entry/feedXML;$sxe new SimpleXMLElement($xml, LIBXML_NONET);$ns $sxe-getNamespaces(true);// 打印时同时输出键和值格式一目了然foreach ($ns as $prefix $uri) {printf(%-10s %s\n, $prefix ? (无前缀) : $prefix, $uri);}如果你不确定自己环境里返回数组的键值方向最省事的做法是先var_dump($ns)看一眼真实结构再按需取用用foreach ($ns as $prefix $uri)遍历则不受影响因为两个变量都有了。默认命名空间没有前缀的那种通常表现为前缀是空字符串的一项。二、recursive 参数到底改变了什么$recursive false时返回的是当前元素上可见的命名空间声明$recursive true时会把后代元素里也出现的命名空间一并收进来。?php // 适用于 PHP 8.0$xml XML?xml version1.0 encodingUTF-8?root xmlns:aurn:example:aa:child xmlns:burn:example:bb:leaf//a:child/rootXML;$root new SimpleXMLElement($xml, LIBXML_NONET);// 只从 root 这一层看$shallow $root-getNamespaces(false);// 把整个子树里出现的都收进来$deep $root-getNamespaces(true);printf(shallow 数量%d\n, count($shallow));printf(deep 数量%d\n, count($deep));urn:example:a声明在根元素上所以两种情况都能看到urn:example:b声明在内层元素上只有在recursivetrue时才会被收进来。这就是「浅」和「深」的实际差别。对大型文档要注意recursive true意味着要遍历整个子树文档越大开销越明显。如果你只想拿到根部的命名空间声明用默认的false更快。三、和 getDocNamespaces() 的区别这是最容易混的一对。它们的目标不同对比项getNamespaces()getDocNamespaces()语义当前元素可见/使用到的命名空间文档中声明的命名空间参数bool $recursive falsebool $recursive false, bool $fromRoot true从哪里开始找当前对象所在的元素默认从文档根元素开始适合做什么检查某个节点上下文里的命名空间一次性拿到整篇文档声明的命名空间用于注册 XPath 前缀是否受当前节点影响是默认否$fromRoot为true时从根找实践中的经验法则要用 XPath 查询整篇文档时用getDocNamespaces(true)一次性拿全只想了解某个节点周围的命名空间情况时用getNamespaces()。四、实战枚举命名空间并注册给 XPath下面这段是这两个方法最典型的用法。它把文档里声明的所有命名空间批量注册然后用前缀查询。要注意默认命名空间的处理——它在前缀维度上是空字符串不能直接写进 XPath 表达式需要自己给它起一个名字。?php // 适用于 PHP 8.0$xml XML?xml version1.0 encodingUTF-8?feed xmlnshttp://www.w3.org/2005/Atom xmlns:dchttp://purl.org/dc/elements/1.1/entrytitle第一篇/titledc:creatoralice/dc:creator/entryentrytitle第二篇/titledc:creatorbob/dc:creator/entry/feedXML;$sxe new SimpleXMLElement($xml, LIBXML_NONET);// 1) 把文档里声明的命名空间全部注册给没有前缀的默认命名空间起个别名foreach ($sxe-getDocNamespaces(true) as $prefix $uri) {$usable $prefix ? default : $prefix;$sxe-registerXPathNamespace($usable, $uri);}// 2) 现在可以正常查询了foreach ($sxe-xpath(//default:entry) as $entry) {// 注册是挂在对象上的要对哪个对象查询就在哪个对象上注册$entry-registerXPathNamespace(default, http://www.w3.org/2005/Atom);$entry-registerXPathNamespace(dc, http://purl.org/dc/elements/1.1/);$title (string) $entry-xpath(./default:title)[0];$creator (string) $entry-xpath(./dc:creator)[0];printf(%s by %s\n, $title, $creator);}有一个细节值得留意前缀注册是挂在对象上的。为避免「在 A 对象上注册、却用 B 对象查询」这类意外最稳的写法是在真正发起xpath()的那个对象上注册一次需要时多注册几遍的成本可以忽略。安全上要强调一点命名空间前缀完全由你决定URI 才是身份。所以「只允许白名单里的 URI」才是有效的校验方式检查前缀字符串if ($prefix dc)没有意义攻击者可以把任意 URI 写成dc前缀。?php // 适用于 PHP 8.0 的白名单校验示例$allowedUris [http://www.w3.org/2005/Atom,http://purl.org/dc/elements/1.1/,];foreach ($sxe-getNamespaces(true) as $prefix $uri) {if (!in_array($uri, $allowedUris, true)) {// 出现了白名单之外的命名空间按业务决定是忽略还是拒绝整份文档error_log(意外的命名空间 . $uri);}}常见坑点❌ 把getNamespaces()当全局函数调用getNamespaces($xml)。✅ 它是SimpleXMLElement的方法必须先有对象$sxe-getNamespaces(true)。❌ 混用getNamespaces()和getDocNamespaces()以为它们等价。✅ 前者看当前元素上下文可选递归到后代后者看文档声明默认从根元素开始多一个$fromRoot参数。需要整篇文档的命名空间时用后者。❌ 认为默认命名空间可以像前缀一样直接写进 XPath//entry。✅ 无前缀名不匹配默认命名空间下的节点。先给它起个前缀并注册registerXPathNamespace(d, $uri)然后写//d:entry。❌ 用前缀字符串做白名单校验认为「只允许dc前缀」就安全了。✅ 前缀只是别名URI 才是命名空间的身份。校验必须基于 URI。❌ 期望getNamespaces()能自动帮你把前缀注册好直接开始xpath()。✅ 它只负责「报告有哪些命名空间」注册要另外调用registerXPathNamespace()。❌ 对空数组不做判断就取下标$ns[0]或假设一定存在某个前缀。✅ 用isset($ns[dc])判断或者遍历数组处理返回空数组是完全正常的情况文档没有声明任何命名空间。❌ 在大文档上无脑传recursive true只为拿根部的几个声明。✅ 递归会遍历整棵子树。默认false就够用时应保持默认。❌ 解析不可信 XML 时不加防护直接把外部 URL 的内容丢进new SimpleXMLElement()。✅ 加LIBXML_NONET禁止网络访问不要使用会替换展开实体的LIBXML_NOENT用libxml_use_internal_errors(true)收集错误并且只把错误摘要而不是完整路径返回给调用方。总结问题结论是否是全局函数不是是SimpleXMLElement::getNamespaces()签名public SimpleXMLElement::getNamespaces(bool $recursive false): array返回「前缀 URI」的数组可为空数组递归参数false只看当前元素true收进整个子树用到的命名空间与getDocNamespaces()前者看节点上下文后者看文档声明默认从根开始主要用途弄清文档有哪些命名空间再注册前缀供 XPath 使用校验方式基于 URI 白名单前缀不可信getNamespaces()解决的问题很具体在写 XPath 之前先问清楚「这个文档里有哪些命名空间」。把它和getDocNamespaces()的分工记清楚——一个看节点周围的上下文一个看整篇文档的声明——再配合registerXPathNamespace()带命名空间的 XML 查询就不会再莫名其妙地返回空数组了。