foreach 中的 DomDocument removeChild 重新索引 dom

Posted

技术标签:

【中文标题】foreach 中的 DomDocument removeChild 重新索引 dom【英文标题】:DomDocument removeChild in foreach reindexing the dom 【发布时间】:2016-08-22 23:24:14 【问题描述】:

我正在尝试删除带有data-spotid 属性的p 标签

        $dom = new DOMDocument();
        @$dom->loadhtml($description);
        $pTag = $dom->getElementsByTagName('p');

        foreach ($pTag as $value) 
            /** @var DOMElement $value */
            $id = $value->getAttribute('data-spotid');
            if ($id) 
                $value->parentNode->removeChild($value);
            
        

但是当我删除孩子时,它正在重新索引 dom。假设我有 8 个项目,我删除了第一个,它将重新索引它,第二个元素将成为第一个,它不会删除它将转到第二个,现在是第三个元素。

【问题讨论】:

我认为“重新索引”这个词不太合适。如果循环被操纵,听起来 foreach 迭代器无法“倒带”自身(可以这么说)以始终位于循环中的最新项目上。因此,这可能是一个比 DomDocument 特定的问题更普遍的引用问题。 看来我的预感有一些优点:php.net/manual/en/domnode.removechild.php#90292 您可以使用iterator_to_array($pTag)。演示:3v4l.org/ieN3X @Yoshi - 这是一个非常简洁的沙盒网站。与使用 iterator_to_array 的原始演示或类似代码相比,运行两个循环(一个在其中构建子元素数组,另一个在其中从其父元素中删除子元素)似乎在不同版本中始终具有更好的性能结果首先创建一个数组变量,然后将该变量传递到 foreach 循环中。 @Yoshi - 使用iterator_to_array 但高于foreach : 3v4l.org/5ug9c ;使用两个 foreach 循环来获取孩子,然后将其删除:3v4l.org/dJiPA 【参考方案1】:

DomNode::removeChild 文档的几个 cmets 中提到了这一点,问题显然是 foreach 上的迭代器指针无法处理您在循环时从父数组中删除项目的事实孩子(或其他东西)的名单。

建议的解决方法是先循环遍历主节点,然后将要删除的子节点推送到其自己的数组中,然后循环遍历该“待删除”数组并从其父节点中删除这些子节点。示例:

$dom = new DOMDocument();
@$dom->loadHTML($description);
$pTag = $dom->getElementsByTagName('p');

$spotid_children = array();

foreach ($pTag as $value) 
    /** @var DOMElement $value */
    $id = $value->getAttribute('data-spotid');
    if ($id) 
        $spotid_children[] = $value; 
    


foreach ($spotid_children as $spotid_child) 
    $spotid_child->parentNode->removeChild($spotid_child); 

【讨论】:

【参考方案2】:

我们可以这样使用:

        $dom = new DOMDocument();
        @$dom->loadHTML($description);
        $pTag = $dom->getElementsByTagName('p');
        $count = count($pTag)
        for($i = 0; $i < $count; $i++) 
            /** @var DOMElement $value */
            $value = $pTag[$i];
            $id = $value->getAttribute('data-spotid');
            if ($id) 
                $i--;$count--;
                $value->parentNode->removeChild($value);
            
        

【讨论】:

【参考方案3】:

就像我评论的那样,easy 解决方案是将迭代器cast 转换为数组。例如:

$elements = iterator_to_array($elements);

但是,如果我们谈论性能,更好的方法是仅选择所需的节点。很好的副作用,移除问题也消失了。

例如:

<?php
$doc = new DOMDocument('1.0', 'UTF-8');
$doc->loadXML(<<<__XML
<?xml version="1.0" encoding="UTF-8"?>
<root>
    <element>1</element>
    <element attr="a">2</element>
    <element>3</element>
    <element>4</element>
    <element attr="a">5</element>
    <element attr="a">6</element>
    <element>7</element>
    <element>8</element>
</root>
__XML
);

$xpath = new DOMXPath($doc);
$elements = $xpath->query('//element[@attr]');

foreach ($elements as $element) 
    $element->parentNode->removeChild($element);


echo $doc->saveXML();

演示:https://3v4l.org/CM9Fv

【讨论】:

这是一个可靠的解决方案,但需要两件事可能是不可能的:1)可以通过 XPath 选择目标元素(如果需要删除的元素是基于滑稽的逻辑,例如“如果子元素属性 1='x' 和属性 2>=7”或更糟),以及 2)确定 XPath 是否能找到合适的 XPath 的工作量(如果有是一个)与其他解决方案相比是现实的。【参考方案4】:

(假设 $dom 包含您需要过滤掉的(DOM)段落)。 让我们尝试一些好的旧 javascript

$ptag = $dom.all.tags("p");
$ptag = [].slice.call($ptag);
$i = 0; 
while($ptag[$i])
'data-spotid' in $ptag[$i].attributes ? $ptag[$i++].outerHTML = "" : 0

注意: 我正在使用 outerHTML 来销毁不需要的元素,以避免调用其父级并重新定位我们已经拥有的感兴趣的节点。最近的 Firefox 版本终于支持它(11+)。MDN ref

为了简洁起见,我还使用了简短的 all.tags() 语法; Firefox 可能还不支持它,所以你可能想回退到 'getElementsByTagName()' 调用那里。

【讨论】:

为了彻底揭开您的问题的神秘面纱,我们必须揭示您正在制作 Live 收藏的事实。循环遍历,同时从实时 dom 集合中删除元素,同时具有线性增长的索引,无疑会导致它跳过元素。这就是您需要将其($ptag 实时集合)转换为静态的原因。这就是我在那里所做的。

以上是关于foreach 中的 DomDocument removeChild 重新索引 dom的主要内容,如果未能解决你的问题,请参考以下文章

这是 PHP 的 DOMDocument 库中的错误吗?

PHP 4 中的新 DOMDocument()

DOMDocument::loadHTML 错误

如何使用 DOMDocument 替换节点的文本

PHP通过DOMDocument对象来抓取网页中的指定class的内容

使用PHP中的DOMDocument在h3标记集之间包装所有HTML标记