【问题标题】:How do you remove duplicate, nested DOM elements in PHP?如何在 PHP 中删除重复的嵌套 DOM 元素?
【发布时间】:2011-11-06 23:07:19
【问题描述】:

假设您有一个带有嵌套标签的 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);

我收集了一些帮助函数,这些函数可能会使处理 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);
    }
}

【问题讨论】:

  • 我没有时间提供更详细的答案,但我认为使用正则表达式会更快更容易!
  • @janoliver,我无法想象一个正则表达式复杂到足以将节点级别考虑在内。
  • 看在上帝的份上,除非绝对没有其他选择,否则不要使用正则表达式来解析 XML。如果您有格式错误的标记,请尝试使用 tidy 扩展来清理它,然后使用 DOM、SimpleXML 或 XMLReader 扩展来解析它。

标签: php html dom


【解决方案1】:

您可以使用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]

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

【讨论】:

  • 非常好,解决了问题,并且可以选择删除 cmets 和空格!没想到小xpath类这么强大!
【解决方案2】:

似乎您在这里几乎拥有所需的一切。在你有// 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

【讨论】:

  • 这是一个好的开始,但问题是你很少有一个只有 1 个子元素的元素。大多数时候可能只有一个孩子 - 但childNodes-&gt;length 报告 3 因为节点前后的空白。所以,#textdiv#text.
  • @Xeoncross 真的。我严格按照前面的例子进行,但这并不适用。要执行您所描述的操作,只需遍历childNodes 并检查每个节点的-&gt;nodeType 属性以查看它是否与常量XML_ELEMENT_NODE 匹配。有关更多信息,请参阅php.net/manual/en/dom.constants.php。只需使用一个计数器变量来计算元素节点的数量,如果只有 1 个,则按上述进行。
  • 我以您的建议为基础,并发布了我迄今为止的进展。这是一个几乎可以工作的版本。
【解决方案3】:

以下是一个几乎可以工作的sn-p。虽然它确实删除了重复的嵌套节点 - 它会因为 -&gt;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();

【讨论】:

    猜你喜欢
    • 2022-01-15
    • 2013-05-19
    • 1970-01-01
    • 2021-01-20
    • 2011-10-24
    • 2019-12-17
    • 2018-02-18
    • 2019-09-13
    • 1970-01-01
    相关资源
    最近更新 更多