【问题标题】:How to completely remove a namespace using DOMDocument如何使用 DOMDocument 完全删除命名空间
【发布时间】:2015-07-27 06:09:02
【问题描述】:

给定一些像下面这样的 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>

【问题讨论】:

    标签: php domdocument xml-namespaces


    【解决方案1】:

    下面的代码可以解决问题:

    // 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 声明,但这段代码可以在任何地方处理它们。

    【讨论】:

      【解决方案2】:

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

      在所有用例中都适用于我们的解决方案是使用 SimpleXMLElement 搜索命名空间并使用 SimpleXMLElement-&gt;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-&gt;getDocNamespaces(true, true) 的使用。

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

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 2014-12-11
        • 2023-02-07
        • 2012-10-24
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2018-02-11
        相关资源
        最近更新 更多