如何在PHP中删除重复的嵌套DOM元素?

时间:2011-11-01 20:03:42

标签: php html dom

假设你有一个带有嵌套标签的DOM树,我想通过删除重复来清理DOM对象。但是,这只适用于标签只有一个子标签的情况。相同的类型。例如,

修复<div><div>1</div></div>而非<div><div>1</div><div>2</div></div>

我正在试图弄清楚如何使用PHP's DOM extension来做到这一点。下面是起始代码,我正在寻找帮助,找出所需的逻辑。

<?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);

我收集了一些辅助函数,这些函数可以更容易地处理像JavaScript这样的DOM节点。

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 个答案:

答案 0 :(得分:7)

您可以使用DOMDocumentDOMXPath轻松完成此操作。 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);
}

在此textnode规范化之后,您不应该在这里的一条评论中遇到您所谈论的问题。

下一部分是查找与其父元素同名且所有元素唯一的子元素。这可以再次用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);
}

Full demo

正如您在演示中所看到的,这与文本节点和通信无关。如果您不想要,例如实际文本,计算子项的表达式需要遍历所有节点类型。但我不知道这是否是你的确切需要。如果是,则会计算所有节点类型中的子项数:

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

如果你没有预先规范化空文本节点(删除空文本节点),那么这个太严格了。选择你需要的工具集,我认为规范化加上这个严格的规则可能是最好的选择。

答案 1 :(得分:1)

好像你几乎拥有了你需要的一切。在哪里// 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

答案 2 :(得分:0)

以下是几乎正在运行的代码段。虽然它确实删除了重复的嵌套节点 - 但由于->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();