Как удалить дубликаты, вложенные элементы DOM в PHP?

Предполагая, что у вас есть дерево DOM с вложенными тегами, я хотел бы очистить объект DOM, удалив дубликаты. Однако это должно применяться только в том случае, если тег имеет только один дочерний тег того же типа. Например,

Исправить <div><div>1</div></div> а не <div><div>1</div><div>2</div></div> .

Я пытаюсь выяснить, как это сделать, используя расширение DOM PHP . Ниже приведен стартовый код, и я ищу помощь в определении необходимой логики.

 <?php libxml_use_internal_errors(TRUE); $html = '<div><div><div><p>Some text here</p></div></div></div>'; $dom = new DOMDocument; $dom->preserveWhiteSpace = false; $dom->formatOutput = true; $dom->loadHTML($html); function dom_remove_duplicate_nodes($node) { var_dump($node); if($node->hasChildNodes()) { for($i = 0; $i < $node->childNodes->length; $i++) { $child = $node->childNodes->item($i); dom_remove_duplicate_nodes($child); } } else { // Process here? } } dom_remove_duplicate_nodes($dom); 

Я собрал некоторые вспомогательные функции, которые могли бы облегчить работу с узлами DOM, такими как JavaScript.

 function DOM_delete_node($node) { DOM_delete_children($node); return $node->parentNode->removeChild($node); } function DOM_delete_children($node) { while (isset($node->firstChild)) { DOM_delete_children($node->firstChild); $node->removeChild($node->firstChild); } } function DOM_dump_child_nodes($node) { $output = ''; $owner_document = $node->ownerDocument; foreach ($node->childNodes as $el) { $output .= $owner_document->saveHTML($el); } return $output; } function DOM_dump_node($node) { if($node->ownerDocument) { return $node->ownerDocument->saveHTML($node); } } 

Solutions Collecting From Web of "Как удалить дубликаты, вложенные элементы DOM в PHP?"

Вы можете сделать это довольно легко с DOMDocument и DOMXPath . XPath особенно полезен в вашем случае, потому что вы легко разделяете логику, чтобы выбрать, какие элементы удалить и как удалить элементы.

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

 $xp = new DOMXPath($dom); //remove empty textnodes - if necessary at all // (in case remove WS: [normalize-space()=""]) foreach($xp->query('//text()[""]') as $i => $tn) { $tn->parentNode->removeChild($tn); } - $xp = new DOMXPath($dom); //remove empty textnodes - if necessary at all // (in case remove WS: [normalize-space()=""]) foreach($xp->query('//text()[""]') as $i => $tn) { $tn->parentNode->removeChild($tn); } 

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

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

 // all child elements with same name as parent element and being // the only child element. $r = $xp->query('body//*/child::*[name(.)=name(..) and count(../child::*)=1]'); foreach($r as $i => $dupe) { while($dupe->childNodes->length) { $child = $dupe->firstChild; $dupe->removeChild($child); $dupe->parentNode->appendChild($child); } $dupe->parentNode->removeChild($dupe); } в // all child elements with same name as parent element and being // the only child element. $r = $xp->query('body//*/child::*[name(.)=name(..) and count(../child::*)=1]'); foreach($r as $i => $dupe) { while($dupe->childNodes->length) { $child = $dupe->firstChild; $dupe->removeChild($child); $dupe->parentNode->appendChild($child); } $dupe->parentNode->removeChild($dupe); } 

Полная демоверсия .

Как вы можете видеть в демонстрационной версии, это не зависит от текстовых полей и сообщений. Если вы этого не хотите, например, фактические тексты, выражение для подсчета детей должно растягиваться по всем типам узлов. Но я не знаю, является ли это вашей конкретной потребностью. Если это так, это делает число детей по всем типам узлов:

 body//*/child::*[name(.)=name(..) and count(../child::node())=1] 

Если вы не нормализуете пустые текстовые поля upfront (удалите пустые), то это слишком строгое. Выберите набор инструментов, которые вам нужны, я думаю, что нормализация плюс это строгое правило может быть лучшим выбором.

Похоже, у вас есть почти все, что вам нужно здесь. Где у вас есть // Process here? сделайте что-то вроде этого:

 if ($node->parentNode->nodeName == $node->nodeName && $node->parentNode->childNodes->length == 1) { $node->parentNode->removeChild($node); } 

Кроме того, в настоящее время вы используете рекурсию в dom_remove_duplicate_notes() которая может быть дорогостоящей. Можно выполнить итерацию по каждому узлу документа без рекурсии, используя такой подход: https://github.com/elazar/domquery/blob/master/trunk/DOMQuery.php#L73

Ниже приведен почти рабочий фрагмент. Хотя он удаляет дубликаты, вложенные узлы – он изменяет порядок источника из-за ->appendChild() .

 <?php header('Content-Type: text/plain'); libxml_use_internal_errors(TRUE); $html = "<div>\n<div>\n<div>\n<p>Some text here</p>\n</div>\n</div>\n</div>"; $dom = new DOMDocument; $dom->preserveWhiteSpace = false; $dom->formatOutput = true; $dom->loadHTML($html); function dom_remove_duplicate_nodes($node) { //var_dump($node); if($node->hasChildNodes()) { $newNode = NULL; for($i = 0; $i < $node->childNodes->length; $i++) { $child = $node->childNodes->item($i); dom_remove_duplicate_nodes($child); if($newNode === FALSE) continue; // If there is a parent to check against if($child->nodeName == $node->nodeName) { // Did we already find the same child? if($newNode OR $newNode === FALSE) { $newNode = FALSE; } else { $newNode = $child; } } elseif($child->nodeName == '#text') { // Something other than whitespace? if(trim($child->nodeValue)) { $newNode = FALSE; } } else { $newNode = FALSE; } } if($newNode) { // Does not transfer $newNode children!!!! //$node->parentNode->replaceChild($newNode, $node); // Works, but appends in reverse!! $node->parentNode->appendChild($newNode); $node->parentNode->removeChild($node); } } } print $dom->saveHTML(). "\n\n\n"; dom_remove_duplicate_nodes($dom); print $dom->saveHTML();