DomDocument removeChild in foreach 重新索引 dom

DomDocument removeChild in foreach reindexing the dom

我正在尝试删除具有 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 个项目我删除了第一个,它将重新索引它,第二个元素将成为第一个并且它不会删除它将转到第二个,现在是第三个元素。

我们可以这样使用:

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

DomNode::removeChild 文档的一些评论中提到了这一点,问题显然是 foreach 上的迭代器指针如何无法处理您正在从父项中删除项目的事实数组,同时循环遍历子列表(或其他内容)。

建议的解决方法是先遍历主节点并将要删除的子节点推送到它自己的数组,然后遍历该 "to-be-deleted" 数组并从其父节点中删除这些子节点。示例:

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

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

$elements = iterator_to_array($elements);

但是,如果我们谈论的是性能,更好的方法是仅 select 仅需要的节点。整洁的副作用,删除问题也消失了。

例如:

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

(假设 $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()' 调用那里。