我试图找到一种方法从HTML源清理一堆空DOM元素,如下所示:
<div class="empty">
<div> </div>
<div></div>
</div>
<a href="http://example.com">good</a>
<div>
<p></p>
</div>
<br>
<img src="http://example.com/logo.png" />
<div></div>
Run Code Online (Sandbox Code Playgroud)
但是,我不想损害有效元素或换行符.所以结果应该是这样的:
<a href="http://example.com">good</a>
<br>
<img src="http://example.com/logo.png" />
Run Code Online (Sandbox Code Playgroud)
到目前为止,我尝试了一些像这样的XPath:
$xpath = new DOMXPath($dom);
//$x = '//*[not(*) and not(normalize-space(.))]';
//$x = '//*[not(text() or node() or self::br)]';
//$x = 'not(normalize-space(.) or self::br)';
$x = '//*[not(text() or node() or self::br)]';
while(($nodeList = $xpath->query($x)) && $nodeList->length > 0) {
foreach ($nodeList as $node) {
$node->parentNode->removeChild($node);
}
}
Run Code Online (Sandbox Code Playgroud)
有人能告诉我正确的XPath来删除那些空的DOM节点吗?(即使是空的,img,br和输入也有用)
当前输出:
<div>
<div> </div>
</div>
<a href="http://example.com">good</a>
<div>
</div>
<br>
Run Code Online (Sandbox Code Playgroud)
为了澄清,我正在寻找一个XPath查询,它是:
I.初步解决方案:
XPath是XML文档的查询语言.因此,对XPath表达式的评估仅选择节点或从XML文档中提取非节点信息,但从不改变XML文档.因此,评估XPath表达式永远不会删除或插入节点 - XML文档保持不变.
你想要的是"从HTML源清理一堆空DOM元素",而不能单独使用XPath.
这是由XPath上最可靠和唯一的官方(我们说的规范)来源证实的--W3C XPath 1.0建议:
" XPath的主要目的是解决一个XML [XML]文档的部分.为了支持这一主要目的,还提供了基本设施为字符串,数字和布尔值的操纵.XPath使用小型的非XML语法,以促进URI和XML中使用XPath属性值.XPath的对XML文档的摘要,逻辑结构进行操作,而不是其表面语法.XPath的其使用的路径的符号如URL的得名通过的层次结构进行导航一个XML文档. "
因此,必须使用一些其他语言与XPath结合才能实现require功能.
XSLT是一种专门用于XML转换的语言.
这是一个基于XSLT的示例 - 一个执行请求清理的简短XSLT转换:
<xsl:stylesheet version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="xml" omit-xml-declaration="yes" indent="yes"/>
<xsl:strip-space elements="*"/>
<xsl:template match="node()|@*">
<xsl:copy>
<xsl:apply-templates select="node()|@*"/>
</xsl:copy>
</xsl:template>
<xsl:template match=
"*[not(string(translate(., ' ', '')))
and
not(descendant-or-self::*
[self::img or self::input or self::br])]"/>
</xsl:stylesheet>
Run Code Online (Sandbox Code Playgroud)
当应用于提供的XML(更正为成为良好的XML文档)时:
<html>
<div class="empty">
<div> </div>
<div></div>
</div>
<a href="http://example.com">good</a>
<div>
<p></p>
</div>
<br />
<img src="http://example.com/logo.png" />
<div></div>
</html>
Run Code Online (Sandbox Code Playgroud)
产生了想要的正确结果:
<html>
<a href="http://example.com">good</a>
<br/>
<img src="http://example.com/logo.png"/>
</html>
Run Code Online (Sandbox Code Playgroud)
说明:
身份规则"按原样"复制为其执行选择的每个节点.
有一个单一的模板,覆盖对任何元素的身份模板(的除外img,input和br),其字符串值从任何 已被去除,是空字符串.此模板的主体为空,有效地"删除"匹配的元素 - 匹配的元素不会复制到输出.
II.更新:
OP澄清他想要一个或多个XPath表达式:
" 每次清理后都可以成功运行多次. "
有趣的是,存在一个XPath表达式,它准确地选择了所有需要删除的节点 - 因此完全避免了"多次清理":
//*[not(normalize-space((translate(., ' ', ''))))
and
not(descendant-or-self::*[self::img or self::input or self::br])
]
[not(ancestor::*
[count(.| //*[not(normalize-space((translate(., ' ', ''))))
and
not(descendant-or-self::*
[self::img or self::input or self::br])
]
)
=
count(//*[not(normalize-space((translate(., ' ', ''))))
and
not(descendant-or-self::*
[self::img or self::input or self::br])
]
)
]
)
]
Run Code Online (Sandbox Code Playgroud)
基于XSLT的验证:
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="xml" omit-xml-declaration="yes" indent="yes"/>
<xsl:template match="node()|@*">
<xsl:copy>
<xsl:apply-templates select="node()|@*"/>
</xsl:copy>
</xsl:template>
<xsl:template match=
"//*[not(normalize-space((translate(., ' ', ''))))
and
not(descendant-or-self::*[self::img or self::input or self::br])
]
[not(ancestor::*
[count(.| //*[not(normalize-space((translate(., ' ', ''))))
and
not(descendant-or-self::*
[self::img or self::input or self::br])
]
)
=
count(//*[not(normalize-space((translate(., ' ', ''))))
and
not(descendant-or-self::*
[self::img or self::input or self::br])
]
)
]
)
]
"/>
</xsl:stylesheet>
Run Code Online (Sandbox Code Playgroud)
当这个转换应用于提供的(并且形成良好的)XML文档(上面)时,除了我们的XPath表达式选择的节点之外,所有节点都"按原样"复制:
<html>
<a href="http://example.com">good</a>
<br/>
<img src="http://example.com/logo.png"/>
</html>
Run Code Online (Sandbox Code Playgroud)
说明:
让我们$vAllEmpty根据问题中"空"的定义来表示所有"空"的节点.
$vAllEmpty 用以下XPath表达式表示:
//*[not(normalize-space((translate(., ' ', ''))))
and
not(descendant-or-self::*
[self::img or self::input or self::br])
]
Run Code Online (Sandbox Code Playgroud)
要删除所有这些内容,我们只需删除"顶级节点" $vAllEmpty
让我们将所有这些"顶级节点"的集合表示为:$vTopEmpty.
$vTopEmpty可以$vAllEmpty使用以下XPath 2.0表达式表示:
$vAllEmpty[not(ancestor::* intersect $vAllEmpty)]
Run Code Online (Sandbox Code Playgroud)
这将选择那些$vAllEmpty没有任何祖先元素的节点$vAllEmpty.
最后一个XPath表达式具有等效的XPath 1.0表达式:
$vAllEmpty[not(ancestor::*[count(.|$vAllEmpty) = count($vAllEmpty)])]
Run Code Online (Sandbox Code Playgroud)
现在,我们用$vAllEmpty上面定义的扩展XPath表达式替换最后一个表达式,这就是我们获取最终表达式的方式,它只选择"要删除的顶级节点":
//*[not(normalize-space((translate(., ' ', ''))))
and
not(descendant-or-self::*[self::img or self::input or self::br])
]
[not(ancestor::*
[count(.| //*[not(normalize-space((translate(., ' ', ''))))
and
not(descendant-or-self::*
[self::img or self::input or self::br])
]
)
=
count(//*[not(normalize-space((translate(., ' ', ''))))
and
not(descendant-or-self::*
[self::img or self::input or self::br])
]
)
]
)
]
Run Code Online (Sandbox Code Playgroud)
使用变量进行基于XSLT-2.0的简短验证:
<xsl:stylesheet version="2.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="xml" omit-xml-declaration="yes" indent="yes"/>
<xsl:strip-space elements="*"/>
<xsl:variable name="vAllEmpty" select=
"//*[not(normalize-space((translate(., ' ', ''))))
and
not(descendant-or-self::*
[self::img or self::input or self::br])
]"/>
<xsl:variable name="vTopEmpty" select=
"$vAllEmpty[not(ancestor::* intersect $vAllEmpty)]"/>
<xsl:template match="node()|@*">
<xsl:copy>
<xsl:apply-templates select="node()|@*"/>
</xsl:copy>
</xsl:template>
<xsl:template match="*[. intersect $vTopEmpty]"/>
</xsl:stylesheet>
Run Code Online (Sandbox Code Playgroud)
此转换将"按原样"复制每个节点,但属于任何节点除外$vTopEmpty.结果是正确的和预期的:
<html>
<a href="http://example.com">good</a>
<br/>
<img src="http://example.com/logo.png"/>
</html>
Run Code Online (Sandbox Code Playgroud)
III.替代解决方案(可能需要"多次清理"):
另一种方法是不尝试指定要删除的节点,而是指定要保留的节点 - 然后要删除的节点是所有节点和要保留的节点之间的集合差异.
要保留的节点由此XPath表达式选择:
//node()
[self::input or self::img or self::br
or
self::text()[normalize-space(translate(.,' ',''))]
]
/ancestor-or-self::node()
Run Code Online (Sandbox Code Playgroud)
然后要删除的节点是:
//node()
[not(count(.
|
//node()
[self::input or self::img or self::br
or
self::text()[normalize-space(translate(.,' ',''))]
]
/ancestor-or-self::node()
)
=
count(//node()
[self::input or self::img or self::br
or
self::text()[normalize-space(translate(.,' ',''))]
]
/ancestor-or-self::node()
)
)
]
Run Code Online (Sandbox Code Playgroud)
但是,请注意,这些都是所有节点删除,而不是只有"顶级节点删除".可以仅表达"要删除的顶级节点",但结果表达式相当复杂.如果有人试图删除要删除的所有节点,则会出现错误,因为"要删除的顶级节点"的后代按文档顺序跟随它们.