Учитывая некоторый 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>
Следующий код делает свое дело:
// 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
объявления в корневом элементе, но этот код обрабатывает их где угодно.
Мы также хотели удалить пространства имен (в нашем случае все пространства имен, а не только конкретное), но вышеприведенное решение работало только частично. Если префикс определяется несколько раз, но с другим 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 для каждого пространства имен по отдельности, он просто пытается удалить все пространства имен из первого элемента, найденного для первого пространства имен в документе, а затем повторно проверяет, остались ли еще пространства имен. Но если позже в документе есть пространства имен, он все равно удалит их. Если пространства имен более распространены в документе, может быть лучше другой подход.