Как полностью удалить пространство имен с помощью DOMDocument

Учитывая некоторый 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>

0

Решение

Следующий код делает свое дело:

// 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->xpath() искать узлы этого пространства имен, а затем преобразовать в DOMElement удалить пространство имен. Для нас управление памятью было лучше при использовании этого подхода, а не при загрузке XML в DOM и использовании 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 воссоздан, потому что в некоторых случаях, если вы пытаетесь получить доступ или манипулировать SimpleXMLElement после использования DOM для удаления пространств имен PHP (5.6) потерпел крах с ошибкой сегментации. К счастью, хотя asXML() продолжал функционировать, чтобы учесть этот обходной путь, поскольку вновь созданный объект не вызывал сбоев.

Если вы хотите удалить определенные пространства имен, вы можете переписать функцию и / или xpath таким образом, чтобы она осуществляла поиск только в определенных пространствах имен. Обратите внимание, что вам также придется изменить использование SimpleXMLElement->getDocNamespaces(true, true),

Дополнительное примечание: мы ищем только первый узел первого пространства имен и затем пытаемся удалить все пространства имен из этого узла по соображениям производительности. Иногда нам приходится работать с ужасными XML-файлами, которые могут содержать более 100 различных пространств имен и могут занимать несколько МБ. Выполнение xpath для каждого пространства имен было очень медленным для этих документов. Это решение значительно повышает производительность, поскольку оно работает в предположении, что большинство, если не все, пространства имен объявлены в одном и том же элементе (обычно это корневой элемент). Таким образом, вместо циклического прохождения и выполнения xpath для каждого пространства имен по отдельности, он просто пытается удалить все пространства имен из первого элемента, найденного для первого пространства имен в документе, а затем повторно проверяет, остались ли еще пространства имен. Но если позже в документе есть пространства имен, он все равно удалит их. Если пространства имен более распространены в документе, может быть лучше другой подход.

0

По вопросам рекламы [email protected]