一、初步解决方案:
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。
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>
当应用于提供的 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>
产生了想要的正确结果:
<html>
<a href="http://example.com">good</a>
<br/>
<img src="http://example.com/logo.png"/>
</html>
说明:
身份规则“按原样”复制选择执行它的每个节点。
有一个模板,覆盖任何元素的标识模板(除了img
,input
和br
),其中任何
已删除的字符串值是空字符串。这个模板的主体是空的,它有效地“删除”了匹配的元素——匹配的元素不会被复制到输出中。
二、更新:
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])
]
)
]
)
]
基于 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>
当此转换应用于提供的(并且格式正确的)XML 文档(上图)时,所有节点都“按原样”复制,但我们的 XPath 表达式选择的节点除外:
<html>
<a href="http://example.com">good</a>
<br/>
<img src="http://example.com/logo.png"/>
</html>
说明:
$vAllEmpty
根据问题中“空”的定义,让我们用所有“空”的节点来表示。
$vAllEmpty
用以下 XPath 表达式表示:
//*[not(normalize-space((translate(., ' ', ''))))
and
not(descendant-or-self::*
[self::img or self::input or self::br])
]
要删除所有这些,我们只需要从$vAllEmpty
让我们将所有此类“顶级节点”的集合表示为:$vTopEmpty
。
$vTopEmpty
可以$vAllEmpty
使用以下 XPath 2.0 表达式表示:
$vAllEmpty[not(ancestor::* intersect $vAllEmpty)]
这会从中选择那些$vAllEmpty
没有任何祖先元素的节点$vAllEmpty
。
最后一个 XPath 表达式具有等效的 XPath 1.0 表达式:
$vAllEmpty[not(ancestor::*[count(.|$vAllEmpty) = count($vAllEmpty)])]
现在,我们用上面定义的扩展 XPath 表达式替换最后一个表达式$vAllEmpty
,这就是我们获得最终表达式的方式,它只选择“要删除的顶部节点”:
//*[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])
]
)
]
)
]
使用变量的基于 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>
此转换“按原样”复制每个节点,但属于 的任何节点除外$vTopEmpty
。结果是正确且预期的结果:
<html>
<a href="http://example.com">good</a>
<br/>
<img src="http://example.com/logo.png"/>
</html>
三、替代解决方案(可能需要“多次清理”):
另一种方法不是尝试指定要删除的节点,而是指定要保留的节点——那么要删除的节点就是所有节点与要保留的节点之间的集合差。
通过这个 XPath 表达式选择要保留的节点:
//node()
[self::input or self::img or self::br
or
self::text()[normalize-space(translate(.,' ',''))]
]
/ancestor-or-self::node()
然后要删除的节点是:
//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()
)
)
]
但是,请注意,这些都是要删除的节点,而不仅仅是“要删除的顶级节点”。可以只表示“要删除的顶部节点”,但结果表达式相当复杂。如果尝试删除所有要删除的节点,则会出现错误,因为“要删除的顶部节点”的后代按照文档顺序跟随它们。