如何删除DOM元素标签,但保留其内容?

4

我有一段PHP代码,它可以删除所有至少有一个属性的节点。这里是我的代码:

<?php

$data = <<<DATA
<div>
    <p>These line shall stay</p>
    <p class="myclass">Remove this one</p>
    <p>But keep this</p>
    <div style="color: red">and this</div>
</div>
DATA;

$dom = new DOMDOcument();
$dom->loadHTML($data, LIBXML_HTML_NOIMPLIED);
$dom->removeChild($dom->doctype);

$xpath = new DOMXPath($dom);

$lines_to_be_removed = $xpath->query("//*[count(@*)>0]");

foreach ($lines_to_be_removed as $line) {
    $line->parentNode->removeChild($line);
}

// just to check
echo $dom->saveHTML();
?>

如您在fiddle中所见,这是上述代码的当前输出:

<div>
    <p>These line shall stay</p>

    <p>But keep this</p>

</div>

虽然这是期望的结果:

<div>
    <p>These line shall stay</p>
    Remove this one
    <p>But keep this</p>
    and this
</div>

我该如何做到这一点?


你是想保留文本节点但删除包围它的<p>容器吗? - Scuzzy
@Scuzzy 是的...我正在尝试删除至少具有一个属性的HTML标签,但是我需要保留其中的内容。换句话说,我只需要删除包围它的<tag attribuve ..容器。 - Martin AJ
3个回答

7

在删除元素之前,您需要拿出它们的子节点并将它们附加在后面。

示例:

$data = <<<DATA
<div>
    <p>These line shall stay</p>
    <p class="myclass">Remove this one</p>
    <p>But keep this</p>
    <div style="color: red">and this</div>
    <div style="color: red">and <p>also</p> this</div>
    <div style="color: red">and this <div style="color: red">too</div></div>
</div>
DATA;

$dom = new DOMDocument();
$dom->loadHTML($data, LIBXML_HTML_NOIMPLIED | LIBXML_HTML_NODEFDTD);
$xpath = new DOMXPath($dom);

foreach ($xpath->query("//*[@*]") as $node) {
    $parent = $node->parentNode;
    while ($node->hasChildNodes()) {
        $parent->insertBefore($node->lastChild, $node->nextSibling);
    }
    $parent->removeChild($node);
}

echo $dom->saveHTML();

输出:

<div>
    <p>These line shall stay</p>
    Remove this one
    <p>But keep this</p>
    and this
    and <p>also</p> this
    and this too
</div>

https://3v4l.org/9qHRM

(我添加了一些嵌套元素来证明这种方法的安全性)


几点说明:

  • 如果使用额外的LIBXML_HTML_NODEFDTD标志加载,则不需要$dom->removeChild($dom->doctype).
  • 你的xpath表达式可以简化为//*[@*].

干净清晰,比我的啰嗦实现要好。 - Scuzzy
我的网站语言不是英语,而是波斯语。嗯,你的方法在波斯字符上不起作用。另外,正如你所看到的,我已经使用了 mb_convert_encoding($data, 'HTML-ENTITIES', 'UTF-8') 来使其正确 *(基于 这个答案)*,但仍然不起作用。有什么建议吗? - stack
@PaulCrovella 哦,好的,谢谢..也许你想在这个问题下写一个答案。 - stack

2
您可以使用replaceChild()函数来替换该节点的文本内容:
foreach ($lines_to_be_removed as $line) {
  $line->parentNode->replaceChild($dom->createTextNode($line->textContent),$line);
}

// <div>
//   <p>These line shall stay</p>
//   Remove this one
//   <p>But keep this</p>
//   and this
// </div>

然而,使用//符号的xpath选择器和递归可能会出现问题。


采用更加手动的方法将目标节点的子内容复制到父节点中。

$data = '
<div>
  <div>1A</div>
  <div class="foo">1B
    <div>2C</div>
    <div class="foo">2D</div>
    <div>2E</div>
    <div class="foo">2F
      <div>3G</div>
      <div class="foo">3H</div>
    </div>
  </div>
</div>';

$dom = new DOMDOcument();
$dom->loadHTML($data, LIBXML_HTML_NOIMPLIED);
$dom->removeChild($dom->doctype);

SomeFunctionName( $dom->documentElement );

$html = $dom->saveHTML();

function SomeFunctionName( $parent )
{
  $nodesToDelete = array();
  if( $parent->hasChildNodes() )
  {
    foreach( $parent->childNodes as $node )
    {
      SomeFunctionName( $node );
      if( $node->hasAttributes() and count( $node->attributes ) > 0 )
      {
        foreach( $node->childNodes as $childNode )
        {
          $node->parentNode->insertBefore( clone $childNode, $node );
        }
        $nodesToDelete[] = $node;
      }
    }
  }
  foreach( $nodesToDelete as $delete)
  {
    $delete->parentNode->removeChild( $delete );
  }
}

// <div>
//   <div>1A</div>
//   1B
//     <div>2C</div>
//     2D
//     <div>2E</div>
//     2F
//       <div>3G</div>
//       3H
//       <div>3I</div>
//       3J
// </div>

如果您想将子元素嵌套在新的“div”容器中,请更改以下代码部分。
    foreach( $parent->childNodes as $node )
    {
      SomeFunctionName( $node );
      if( $node->hasAttributes() and count( $node->attributes ) > 0 )
      {
        $newNode = $node->ownerDocument->createElement('div');
        foreach( $node->childNodes as $childNode )
        {
          $newNode->appendChild( clone $childNode );
        }
        $node->parentNode->insertBefore( $newNode, $node );
        $nodesToDelete[] = $node;
      }
    }

// <div>
//   <div>1A</div>
//   <div>1B
//     <div>2C</div>
//     <div>2D</div>
//     <div>2E</div>
//     <div>2F
//       <div>3G</div>
//       <div>3H</div>
//       <div>3I</div>
//       <div>3J</div>
//     </div>
//   </div>
// </div>

实际上,你上一句话让我对使用你的方法感到担忧。("然而,这可能会对您的Xpath选择器中的//符号和递归造成问题。")我能相信你的方法吗? - Martin AJ
看一下我的修改,我采用了手动DOM迭代的方法。我的担忧是,如果这些元素作为子元素的容器,处理嵌套的div将会变得有趣。 - Scuzzy
这种方法实际上是将要销毁的节点的内容复制到父节点中,我的测试数据包含嵌套深度作为数字以及一个唯一的字母字符来显示所有内容都保持在原处。 - Scuzzy

1
这将删除所有具有classstyle属性的标签,因此它并不是百分之百可靠的:
<?php

$data = <<<DATA
<div>
    <p>These line shall stay</p>
    <p class="myclass">Remove this one</p>
    <p>But keep this</p>
    <div style="color: red">and this</div>
</div>
DATA;

$dom = new DOMDOcument();
$dom->loadHTML($data, LIBXML_HTML_NOIMPLIED);
$dom->removeChild($dom->doctype);

$xpath = new DOMXPath($dom);

$lines_to_be_removed = $xpath->query("//*[count(@class)>0 or count(@style)>0]");

foreach ($lines_to_be_removed as $line) {
    $line->parentNode->removeChild($line);
}

// just to check
echo $dom->saveHTML();
?>

请注意这一行:


 $lines_to_be_removed = $xpath->query("//*[count(@class)>0] or count(@style)>0]");

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接