PHPDom 遍历文档并删除没有 XPath 的节点

PHPDom iterate through document and remove nodes without XPath

我正在尝试遍历文档并删除节点(在我的例子中是所有 divs),但没有 xpath(我已经可以使用 xpath 执行此操作)。出于某种原因,只有第一个 div 被删除。有什么建议吗?

<?php

//my totally random html        
$html = '<p> Great <div> dont want this</div> </p><p> some more</p><div>more crap here</div>';

$doc = new DOMDocument();
$doc->loadHTML($html);  

iterate_children($doc );
print $doc->saveHTML();


function iterate_children(&$object){
    //print_r($object);

    if ($object->tagName == "div") {
        $object->parentNode->removeChild($object);
        iterate_children($object->parentNode);
    }
    else {
        //if($object->hasChildNodes()) {
        foreach($object->childNodes as $child) {
            //
            iterate_children($child);
        //}
        }
    }
}

?>

只有第一个 div 被删除的原因可能是最简单的解释方式:

您遍历所有子节点。此迭代首先将当前节点设置为第一个子节点 (DOMNode::$firstChild). Then you process that child and when done you continue to the next child (that is then DOMNode::$nextSibling)。

但是如果你现在从父节点中删除当前节点

$object->parentNode->removeChild($object);

迭代中的当前节点不再有任何下一个兄弟节点(因为它已从其父节点中删除)。因此,foreach 迭代在您删除第一个 div 元素后立即结束。

有多种方法可以解决这个问题。使用纯 PHP 并且不使用任何 xpath,您可以先将要删除的所有节点存储在一个数组中,然后再删除它们。在这种情况下,函数 iterator_to_array 非常方便:

$divs = iterator_to_array($doc->getElementsByTagName('div'));
foreach ($divs as $div) {
    $div->parentNode->removeChild($div);
}

这四行代码确实替换了您的(不起作用的)函数 (!) 的所有迭代和递归逻辑。

您还可以通过使用 CachingIterator 来修复您的函数,它在迭代当前元素(当前元素已缓存)时内部已经有下一个元素。它不会失效,因为当您从父节点中删除当前节点时,下一个节点已经被获取。

您的代码大概会更改以下行:

foreach($object->childNodes as $child) {            
    iterate_children($child);
}

至:

$children = $object->childNodes;
$children = new IteratorIterator($children);
$children = new CachingIterator($children, CachingIterator::TOSTRING_USE_KEY);
foreach ($children as $child) {
    iterate_children($child);
}

但请注意,此代码仅用于演示目的。如果您将其复制并粘贴到您的示例中,它会崩溃,因为您的代码中存在一些其他问题,这些问题会因此类更改而变得严重。

此代码仍将具有实际上不必要的递归,因为您可以按文档顺序迭代节点。为此,我在 Iterator Garden. That library also has a simple DOMElementFilter in the development branch 中有一个 DOMNodeIterator。由于下一个兄弟的问题在这里是一样的,使用这两个需要再次使用 CachingITerator

$divs = new CachingIterator(new DOMElementFilter(new DOMNodeIterator($doc), 'div'), CachingIterator::TOSTRING_USE_KEY);
foreach ($divs as $div) {
    $div->parentNode->removeChild($div);
}

此代码再次与 iterator_to_array 示例非常相似。由于迭代器的装饰特性,迭代器通常使您能够创建更多可重用的代码。

我希望这能帮助您理解为什么会发生这种情况,并展示一些处理方法。


出于完整性原因,此处您的代码具有更好的错误处理和遍历逻辑:

function iterate_children(DOMNode $node)
{
    if ($node instanceof DOMElement and $node->tagName == "div") {
        $parent = $node->parentNode;
        $parent->removeChild($node);
        return;
    }

    $children = $node->childNodes;
    if (!$children) {
        return;
    }

    $children = new IteratorIterator($children);
    $children = new CachingIterator($children, CachingIterator::TOSTRING_USE_KEY);
    foreach ($children as $child) {
        iterate_children_old($child);
    }
}

这里是没有递归和数组的实现:

<?php
/**
 * PHPDom iterate through document and remove nodes without XPath
 */

/my totally random html
$html = '<p> Great <div> dont want this</div> </p><p> some more</p><div>more crap here</div>';

$doc          = new DOMDocument();
$doc->recover = true;
$saved        = libxml_use_internal_errors(true);
$doc->loadHTML($html);
libxml_use_internal_errors($saved);

$divs = iterator_to_array($doc->getElementsByTagName('div'));
foreach ($divs as $div) {
    $div->parentNode->removeChild($div);
}

echo $doc->saveHTML();