如何使用DOMDocument完全删除命名空间

时间:2015-05-15 10:40:19

标签: php domdocument xml-namespaces

考虑到以下一些XML,如何从每个元素中完全删除特定的命名空间,包括其声明?

<?xml version="1.0" encoding="UTF-8"?>
<document xmlns:my-co="http://www.example.com/2015/co">
  <my-namespace:first xmlns:my-namespace="http://www.example.com/2015/ns">
    <element my-namespace:id="1">
    </element>
  </my-namespace:first>
  <second>
    <my-namespace:element xmlns:my-namespace="http://www.example.com/2015/ns" my-co:id="2">
    </my-namespace:element>
  </second>
</document>

请注意,根级别没有xmlns:my-namespace声明,并且这两个声明位于XML结构的不同部分和级别。

如何在不必检查代码中的每个节点的情况下,如何有效地删除命名空间my-namespace

这就是XML之后的样子:

<?xml version="1.0" encoding="UTF-8"?>
<document xmlns:my-co="http://www.example.com/2015/co">
  <first>
    <element id="1">
    </element>
  </first>
  <second>
    <element my-co:id="2">
    </element>
  </second>
</document>

2 个答案:

答案 0 :(得分:2)

以下代码可以解决问题:

// Removes the namespace $ns from all elements in the DOMDocument $doc
function remove_dom_namespace($doc, $ns) {
  $finder = new DOMXPath($doc);
  $nodes = $finder->query("//*[namespace::{$ns} and not(../namespace::{$ns})]");
  foreach ($nodes as $n) {
    $ns_uri = $n->lookupNamespaceURI($ns);
    $n->removeAttributeNS($ns_uri, $ns);
  }
}

// Usage:
$mydoc = new DOMDocument();
$mydoc->load('test.xml'); // Load "before" XML
remove_dom_namespace($mydoc, 'my-namespace');

// Prints the above "after" XML
echo $mydoc->saveXML(null, LIBXML_NOEMPTYTAG);

XPath查询查找具有名为$ns的命名空间节点的所有节点,其父节点也不具有相同的命名空间。这会找到/document/my-namespace:first/document/second/my-namespace:element但不会找到/document/my-namespace:first/element,因为它的父级也有my-namespace这个名称空间。然后代码从找到的每个元素中删除指定的命名空间。从元素中删除命名空间会自动将其从所有子元素中删除。

许多真实的XML文档在根元素上都有xmlns个声明,但是这个代码可以在任何地方处理它们。

答案 1 :(得分:0)

我们也希望删除命名空间(在我们的例子中是所有命名空间,而不仅仅是特定的命名空间),但上述解决方案只能部分工作。如果多次定义前缀但使用不同的URI,则第一个答案不会全部删除它们。

在所有用例中适用于我们的解决方案是使用SimpleXMLElement搜索命名空间并使用SimpleXMLElement->xpath()搜索该命名空间的节点,然后转换为DOMElement删除命名空间。对我们来说,内存管理更好地使用该方法,而不是在DOM中加载XML并使用DOMXPath

要测试的示例XML:

<xml xmlns="http://foo" xmlns:bar="http://bar" xmlns:baz="http://baz">
    <foo bam="hoi">Hello World</foo>
    <foo baz:bam="hoi">Hello World</foo>
    <bar:foo bam="hoi">Hello World</bar:foo>
    <bar:foo bar:bam="hoi">Hello World</bar:foo>
    <bar:foo baz:bam="hoi">Hello World</bar:foo>
    <baz:foo bar:bam="hoi">Hello World</baz:foo>
    <plop:foo xmlns:plop="http://plop" xmlns:bar="http://baasdr">
        <bar:foo>
            <bar:foo xmlns:plop="http://plop">
                <plop:foo>
                    <plop:foo>
                        <plop:foo xmlns:bar="http://bar">
                            <bar:baz>Hello World</bar:baz>
                        </plop:foo>
                    </plop:foo>
                </plop:foo>
            </bar:foo>
        </bar:foo>
    </plop:foo>
</xml>

删除命名空间的示例代码:

function removeNamespaces(SimpleXMLElement $xml) {

    while($namespaces = $xml->getDocNamespaces(true, true)) {

        $uri    = reset($namespaces);
        $prefix = key($namespaces);

        $elements = $xml->xpath("//*[namespace::*[name() = '{$prefix}' and . = '{$uri}'] and not (../namespace::*[name() = '{$prefix}' and . = '{$uri}'])]");
        $element  = dom_import_simplexml($elements[0]);

        foreach($namespaces as $prefix => $uri) {
            $element->removeAttributeNS($uri, $prefix);
        }

        $xml = new SimpleXMLElement($xml->asXML());
    }

    return $xml;
}

重新创建SimpleXMLElement,因为在某些情况下,如果在使用DOM删除命名空间后尝试访问或操作SimpleXMLElement,PHP(5.6)会因分段错误而崩溃。幸运的是,虽然asXML()保持运行以允许这种解决方法,但是新创建的对象不会导致崩溃。

如果要删除特定的命名空间,可以以仅搜索特定命名空间的方式重写函数和/或xpath。请注意,您还必须更改SimpleXMLElement->getDocNamespaces(true, true)的使用。

另外请注意,我们只查找第一个命名空间的第一个节点,然后出于性能原因尝试从该节点删除所有命名空间。我们有时必须处理可能包含100多个不同命名空间的可怕XML,并且可能是几个MB的大。在这些文档上为每个命名空间执行xpath非常慢。此解决方案大大提高了性能,因为它假设大多数(如果不是全部)命名空间在同一元素(通常是根元素)中声明。因此,它不是单独循环并为每个命名空间执行xpath,而只是尝试从为文档中第一个命名空间找到的第一个元素中删除所有命名空间,然后重新检查是否还有命名空间。但是如果稍后在文档中存在名称空间,它仍然会删除它们。如果名称空间通过文档更加分散,那么不同的方法可能会更好。