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

时间:2015-04-20 19:48:27

标签: php dom xpath domdocument

我正在尝试遍历文档,并删除节点(在我的情况下是所有div),但没有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);
        //}
        }
    }
}

?>

1 个答案:

答案 0 :(得分:4)

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

迭代所有子节点。此迭代首先将当前节点设置为第一个子节点(DOMNode::$firstChild)。然后你处理那个孩子,完成后你继续下一个孩子(那时是DOMNode::$nextSibling)。

但是,如果您现在从父

中删除当前节点
$object->parentNode->removeChild($object);

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

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

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

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

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

粗略地为您的代码更改以下行:

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

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

此代码仍然具有实际上不必要的递归,因为您可以按文档顺序迭代节点。为此,我在development branch中有一个 DOMNodeIterator 。该库在{{3}}中也有一个简单的 DOMElementFilter 。由于下一个兄弟的问题在这里是相同的,使用这两个也需要再次 CachingTerator

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