假设你有一个带有嵌套标签的DOM树,我想通过删除重复来清理DOM对象。但是,这只适用于标签只有一个子标签的情况。相同的类型。例如,
修复<div><div>1</div></div>
而非<div><div>1</div><div>2</div></div>
。
我正在试图弄清楚如何使用PHP's DOM extension来做到这一点。下面是起始代码,我正在寻找帮助,找出所需的逻辑。
<?php
libxml_use_internal_errors(TRUE);
$html = '<div><div><div><p>Some text here</p></div></div></div>';
$dom = new DOMDocument;
$dom->preserveWhiteSpace = false;
$dom->formatOutput = true;
$dom->loadHTML($html);
function dom_remove_duplicate_nodes($node)
{
var_dump($node);
if($node->hasChildNodes())
{
for($i = 0; $i < $node->childNodes->length; $i++)
{
$child = $node->childNodes->item($i);
dom_remove_duplicate_nodes($child);
}
}
else
{
// Process here?
}
}
dom_remove_duplicate_nodes($dom);
我收集了一些辅助函数,这些函数可以更容易地处理像JavaScript这样的DOM节点。
function DOM_delete_node($node)
{
DOM_delete_children($node);
return $node->parentNode->removeChild($node);
}
function DOM_delete_children($node)
{
while (isset($node->firstChild))
{
DOM_delete_children($node->firstChild);
$node->removeChild($node->firstChild);
}
}
function DOM_dump_child_nodes($node)
{
$output = '';
$owner_document = $node->ownerDocument;
foreach ($node->childNodes as $el)
{
$output .= $owner_document->saveHTML($el);
}
return $output;
}
function DOM_dump_node($node)
{
if($node->ownerDocument)
{
return $node->ownerDocument->saveHTML($node);
}
}
答案 0 :(得分:7)
您可以使用DOMDocument
和DOMXPath
轻松完成此操作。 XPath特别适用于您的情况,因为您可以轻松划分逻辑以选择要删除的元素以及删除元素的方式。
首先,规范化输入。我并不完全清楚你对空白空间的意思,我认为它可能是空文本节点(可能已被删除,因为preserveWhiteSpace
是FALSE
但是我不确定)或者它们是否已标准化空白是空的。我选择了第一个(如果有必要的话),以防它是另一个变种我留下了一个评论用什么代替:
$xp = new DOMXPath($dom);
//remove empty textnodes - if necessary at all
// (in case remove WS: [normalize-space()=""])
foreach($xp->query('//text()[""]') as $i => $tn)
{
$tn->parentNode->removeChild($tn);
}
在此textnode规范化之后,您不应该在这里的一条评论中遇到您所谈论的问题。
下一部分是查找与其父元素同名且所有元素唯一的子元素。这可以再次用xpath表示。如果找到这样的元素,他们的所有子元素都将被移动到父元素,然后元素也将被删除:
// all child elements with same name as parent element and being
// the only child element.
$r = $xp->query('body//*/child::*[name(.)=name(..) and count(../child::*)=1]');
foreach($r as $i => $dupe)
{
while($dupe->childNodes->length)
{
$child = $dupe->firstChild;
$dupe->removeChild($child);
$dupe->parentNode->appendChild($child);
}
$dupe->parentNode->removeChild($dupe);
}
正如您在演示中所看到的,这与文本节点和通信无关。如果您不想要,例如实际文本,计算子项的表达式需要遍历所有节点类型。但我不知道这是否是你的确切需要。如果是,则会计算所有节点类型中的子项数:
body//*/child::*[name(.)=name(..) and count(../child::node())=1]
如果你没有预先规范化空文本节点(删除空文本节点),那么这个太严格了。选择你需要的工具集,我认为规范化加上这个严格的规则可能是最好的选择。
答案 1 :(得分:1)
好像你几乎拥有了你需要的一切。在哪里// Process here?
执行以下操作:
if ($node->parentNode->nodeName == $node->nodeName
&& $node->parentNode->childNodes->length == 1) {
$node->parentNode->removeChild($node);
}
此外,您目前正在使用dom_remove_duplicate_notes()
中的递归,这可能在计算上很昂贵。可以使用如下方法迭代文档中的每个节点而不进行递归:https://github.com/elazar/domquery/blob/master/trunk/DOMQuery.php#L73
答案 2 :(得分:0)
以下是几乎正在运行的代码段。虽然它确实删除了重复的嵌套节点 - 但由于->appendChild()
。
<?php
header('Content-Type: text/plain');
libxml_use_internal_errors(TRUE);
$html = "<div>\n<div>\n<div>\n<p>Some text here</p>\n</div>\n</div>\n</div>";
$dom = new DOMDocument;
$dom->preserveWhiteSpace = false;
$dom->formatOutput = true;
$dom->loadHTML($html);
function dom_remove_duplicate_nodes($node)
{
//var_dump($node);
if($node->hasChildNodes())
{
$newNode = NULL;
for($i = 0; $i < $node->childNodes->length; $i++)
{
$child = $node->childNodes->item($i);
dom_remove_duplicate_nodes($child);
if($newNode === FALSE) continue;
// If there is a parent to check against
if($child->nodeName == $node->nodeName)
{
// Did we already find the same child?
if($newNode OR $newNode === FALSE)
{
$newNode = FALSE;
}
else
{
$newNode = $child;
}
}
elseif($child->nodeName == '#text')
{
// Something other than whitespace?
if(trim($child->nodeValue))
{
$newNode = FALSE;
}
}
else
{
$newNode = FALSE;
}
}
if($newNode)
{
// Does not transfer $newNode children!!!!
//$node->parentNode->replaceChild($newNode, $node);
// Works, but appends in reverse!!
$node->parentNode->appendChild($newNode);
$node->parentNode->removeChild($node);
}
}
}
print $dom->saveHTML(). "\n\n\n";
dom_remove_duplicate_nodes($dom);
print $dom->saveHTML();