XPath для рекурсивного удаления пустых узлов DOM?

Я пытаюсь найти способ очистки пустых элементов DOM из источника HTML, например:

<div class="empty"> <div>&nbsp;</div> <div></div> </div> <a href="http://example.com">good</a> <div> <p></p> </div> <br> <img src="http://example.com/logo.png" /> <div></div> 

Однако я не хочу наносить ущерб действительным элементам или разрыву строк. Поэтому результат должен быть примерно таким:

 <a href="http://example.com">good</a> <br> <img src="http://example.com/logo.png" /> 

До сих пор я пробовал некоторые XPaths следующим образом:

 $xpath = new DOMXPath($dom); //$x = '//*[not(*) and not(normalize-space(.))]'; //$x = '//*[not(text() or node() or self::br)]'; //$x = 'not(normalize-space(.) or self::br)'; $x = '//*[not(text() or node() or self::br)]'; while(($nodeList = $xpath->query($x)) && $nodeList->length > 0) { foreach ($nodeList as $node) { $node->parentNode->removeChild($node); } } 

Может ли кто-нибудь показать мне правильный XPath для удаления пустых узлов DOM, которые не имеют никакой цели, если они пусты? (img, br и input служат цели, даже если они пусты)

Токовый выход:

 <div> <div>&nbsp;</div> </div> <a href="http://example.com">good</a> <div> </div> <br> 

Обновить

Чтобы уточнить, я ищу запрос XPath, который либо:

  • Рекурсивный поиск совпадающих пустых узлов до тех пор, пока все не будут найдены (включая родителей пустых узлов)
  • Можно успешно запускать несколько раз после каждой очистки (как показано в моем примере)

I. Исходное решение:

XPath – это язык запросов для документов XML. Таким образом, оценка выражения XPath только выбирает узлы или извлекает не-узловые данные из документа XML, но никогда не изменяет XML-документ. Таким образом, оценка выражения XPath никогда не удаляет или не вставляет узлы – документ XML остается тем же.

Вы хотите «очистить кучу пустых элементов DOM из источника HTML» и не могут быть выполнены только с XPath .

Это подтверждается наиболее достоверным и единственным официальным (мы говорим, нормативным ) источником на XPath – W3C XPath 1.0 Рекомендация :

« Основной целью XPath является обращение к частям XML [XML] документа. В поддержку этой основной цели он также предоставляет базовые возможности для манипулирования строками, числами и логическими значениями. XPath использует компактный синтаксис, отличный от XML, для облегчения использование XPath в пределах URI и значений атрибутов XML. XPath работает с абстрактной логической структурой документа XML, а не с его поверхностным синтаксисом. XPath получает свое имя от использования нотации пути, как в URL-адресах, для навигации по иерархической структуре XML-документ ".

Поэтому для реализации требуемой функциональности необходимо использовать некоторый дополнительный язык в сочетании с XPath .

XSLT – это язык, специально предназначенный для преобразования XML.

Вот пример XSLT – краткое и простое преобразование XSLT, которое выполняет запрошенную очистку :

 <xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform"> <xsl:output method="xml" omit-xml-declaration="yes" indent="yes"/> <xsl:strip-space elements="*"/> <xsl:template match="node()|@*"> <xsl:copy> <xsl:apply-templates select="node()|@*"/> </xsl:copy> </xsl:template> <xsl:template match= "*[not(string(translate(., '&#xA0;', ''))) and not(descendant-or-self::* [self::img or self::input or self::br])]"/> </xsl:stylesheet> 

При применении к предоставленному XML (скорректированный, чтобы стать хорошо сформированным XML-документом):

 <html> <div class="empty"> <div>&#xA0;</div> <div></div> </div> <a href="http://example.com">good</a> <div> <p></p> </div> <br /> <img src="http://example.com/logo.png" /> <div></div> </html> 

получается желаемый, правильный результат :

 <html> <a href="http://example.com">good</a> <br/> <img src="http://example.com/logo.png"/> </html> 

Объяснение :

  1. Правило удостоверения копирует «как есть» каждый узел, для которого он выбран для выполнения.

  2. Существует один шаблон, переопределяющий шаблон идентификатора для любого элемента (за исключением img , input и br ), строковое значение которого, из которого any &nbsp; был удален, это пустая строка. Тело этого шаблона пуст, что эффективно «удаляет» сопоставленный элемент – согласованный элемент не копируется на выход.


II. Обновление :

ОП разъясняет, что ему требуется одно или несколько выражений XPath, которые:

« Можно успешно запускать несколько раз после каждой очистки ».

Интересно, что существует одно выражение XPath, которое выбирает точно все узлы, которые необходимо удалить, поэтому полностью исключаются «множественные очистки» :

 //*[not(normalize-space((translate(., '&#xA0;', '')))) and not(descendant-or-self::*[self::img or self::input or self::br]) ] [not(ancestor::* [count(.| //*[not(normalize-space((translate(., '&#xA0;', '')))) and not(descendant-or-self::* [self::img or self::input or self::br]) ] ) = count(//*[not(normalize-space((translate(., '&#xA0;', '')))) and not(descendant-or-self::* [self::img or self::input or self::br]) ] ) ] ) ] 

Проверка на основе XSLT :

 <xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform"> <xsl:output method="xml" omit-xml-declaration="yes" indent="yes"/> <xsl:template match="node()|@*"> <xsl:copy> <xsl:apply-templates select="node()|@*"/> </xsl:copy> </xsl:template> <xsl:template match= "//*[not(normalize-space((translate(., '&#xA0;', '')))) and not(descendant-or-self::*[self::img or self::input or self::br]) ] [not(ancestor::* [count(.| //*[not(normalize-space((translate(., '&#xA0;', '')))) and not(descendant-or-self::* [self::img or self::input or self::br]) ] ) = count(//*[not(normalize-space((translate(., '&#xA0;', '')))) and not(descendant-or-self::* [self::img or self::input or self::br]) ] ) ] ) ] "/> </xsl:stylesheet> 

Когда это преобразование применяется к предоставленному (и сделанному хорошо сформированному) XML-документу (выше), все узлы копируются «как есть», за исключением узлов, выбранных нашим выражением XPath :

 <html> <a href="http://example.com">good</a> <br/> <img src="http://example.com/logo.png"/> </html> 

Объяснение :

Обозначим через $vAllEmpty все узлы, которые являются «пустыми» в соответствии с определением «пусто» в вопросе.

$vAllEmpty выражается следующим выражением XPath:

  //*[not(normalize-space((translate(., '&#xA0;', '')))) and not(descendant-or-self::* [self::img or self::input or self::br]) ] 

Чтобы все они были удалены, нам нужно удалить только «верхние узлы» из $vAllEmpty

Обозначим множество всех таких «верхних узлов» как: $vTopEmpty .

$vTopEmpty может быть выражен из $vAllEmpty используя следующее выражение XPath 2.0:

 $vAllEmpty[not(ancestor::* intersect $vAllEmpty)] 

это выбирает эти узлы из $vAllEmpty , у которых нет элемента предка, который также находится в $vAllEmpty .

Последнее выражение XPath имеет эквивалентное выражение XPath 1.0:

 $vAllEmpty[not(ancestor::*[count(.|$vAllEmpty) = count($vAllEmpty)])] 

Теперь мы заменим в последнем выражении $vAllEmpty с его расширенным выражением XPath, как определено выше, и таким образом мы получаем окончательное выражение, которое выбирает только «верхние узлы для удаления»:

 //*[not(normalize-space((translate(., '&#xA0;', '')))) and not(descendant-or-self::*[self::img or self::input or self::br]) ] [not(ancestor::* [count(.| //*[not(normalize-space((translate(., '&#xA0;', '')))) and not(descendant-or-self::* [self::img or self::input or self::br]) ] ) = count(//*[not(normalize-space((translate(., '&#xA0;', '')))) and not(descendant-or-self::* [self::img or self::input or self::br]) ] ) ] ) ] 

Короткая проверка на основе XSLT-2.0 с использованием переменных :

 <xsl:stylesheet version="2.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform"> <xsl:output method="xml" omit-xml-declaration="yes" indent="yes"/> <xsl:strip-space elements="*"/> <xsl:variable name="vAllEmpty" select= "//*[not(normalize-space((translate(., '&#xA0;', '')))) and not(descendant-or-self::* [self::img or self::input or self::br]) ]"/> <xsl:variable name="vTopEmpty" select= "$vAllEmpty[not(ancestor::* intersect $vAllEmpty)]"/> <xsl:template match="node()|@*"> <xsl:copy> <xsl:apply-templates select="node()|@*"/> </xsl:copy> </xsl:template> <xsl:template match="*[. intersect $vTopEmpty]"/> </xsl:stylesheet> 

Это преобразование копирует каждый узел «как есть», за исключением любого узла, принадлежащего $vTopEmpty . Результат – правильный и ожидаемый:

 <html> <a href="http://example.com">good</a> <br/> <img src="http://example.com/logo.png"/> </html> 

III. Альтернативное решение (может потребоваться «множественная очистка») :

Альтернативный подход – не пытаться указывать удаляемые узлы, а указывать узлы для хранения – тогда удаляемые узлы являются заданной разницей между всеми узлами и узлами, которые нужно сохранить.

Узлы, которые нужно сохранить, выбираются с помощью выражения XPath :

  //node() [self::input or self::img or self::br or self::text()[normalize-space(translate(.,'&#xA0;',''))] ] /ancestor-or-self::node() 

Затем удаляемые узлы :

  //node() [not(count(. | //node() [self::input or self::img or self::br or self::text()[normalize-space(translate(.,'&#xA0;',''))] ] /ancestor-or-self::node() ) = count(//node() [self::input or self::img or self::br or self::text()[normalize-space(translate(.,'&#xA0;',''))] ] /ancestor-or-self::node() ) ) ] -  //node() [not(count(. | //node() [self::input or self::img or self::br or self::text()[normalize-space(translate(.,'&#xA0;',''))] ] /ancestor-or-self::node() ) = count(//node() [self::input or self::img or self::br or self::text()[normalize-space(translate(.,'&#xA0;',''))] ] /ancestor-or-self::node() ) ) ] -  //node() [not(count(. | //node() [self::input or self::img or self::br or self::text()[normalize-space(translate(.,'&#xA0;',''))] ] /ancestor-or-self::node() ) = count(//node() [self::input or self::img or self::br or self::text()[normalize-space(translate(.,'&#xA0;',''))] ] /ancestor-or-self::node() ) ) ] 

Однако имейте в виду, что это все узлы для удаления, а не только «верхние узлы для удаления». Можно выразить только «верхние узлы для удаления», но полученное выражение довольно сложно. Если вы попытаетесь удалить все узлы – для удаления, будут ошибки из-за того, что потомки «верхних узлов для удаления» следуют за ними в порядке документа.

Вам нужны текстовые узлы, и <img> , а также их предки?

Вы можете получить все br и img с //br и //img .

Вы можете получить все текстовые узлы с помощью //text() и всех непустых текстовых узлов с помощью //text()[normalize-space()] . (хотя вам может понадобиться что-то вроде //text()[normalize-space(translate(., '&nbsp;', ''))] чтобы фильтровать текстовые узлы, если ваш XML-анализатор еще этого не делает)

И вы можете получить всех родителей с ancestor-or-self::* .

Таким образом, полученное выражение

 //br/ancestor-or-self::* | //img/ancestor-or-self::* | //text()[normalize-space()]/ancestor-or-self::* 

И короче в XPath 2:

 (//br | //img | //text()[normalize-space()])/ancestor-or-self::* 

Самый простой способ добиться необходимого результата – использование регулярных выражений с вашим текстом. С замечанием: вы должны использовать это выражение несколько раз, так как он не жадный, он удаляет только младшие пустые дочерние узлы, поэтому для удаления всех пустых узлов мы должны вызывать регулярное выражение несколько раз.

Вот решение:

 <? $text = '<div class="empty"> <div>&nbsp;</div> <div></div> </div> <a href="http://example.com">good</a> <div> <p></p> </div> <br> <img src="http://example.com/logo.png" /> <div></div>'; // recursive function function recreplace($text) { $restext = preg_replace("/<div(.*)?>((\s|&nbsp;)*|(\s|&nbsp;)*<p>(\s|&nbsp;)*<\/p>(\s|&nbsp;)*)*<\/div>/U", '', $text); if ($text != $restext) { recreplace($restext); } else { return $restext; } } print recreplace($text); ?> 

Этот код печатает желаемый результат. Если вам нужно отредактировать регулярное выражение, в него могут быть добавлены любые другие теги, которые должны считаться пустыми (как <p> </p> ).

В данном примере эта функция будет вызывать себя два раза в результате и в третий раз без какой-либо замены – и это будет результат.

Вы пробовали XPath, подобный этому?

 *[not(*) and not(text()[normalize-space()])] 

С

  • not(*) = нет дочернего элемента
  • text()[normalize-space()] = Включить узлы с небелым текстом (не инвертирует это)