Как удалить дубликаты вложенных элементов DOM в PHP? - PullRequest
1 голос
/ 02 ноября 2011

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

Fix <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);
    }
}

Ответы [ 3 ]

7 голосов
/ 06 ноября 2011

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

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

$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);
}

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

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

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

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

1 голос
/ 02 ноября 2011

Похоже, у вас есть почти все, что вам нужно здесь.Там, где у вас есть // 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

0 голосов
/ 03 ноября 2011

Ниже приведен почти рабочий фрагмент кода.Хотя он удаляет дубликаты вложенных узлов - он меняет исходный порядок из-за ->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();
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...