如何用XSLT 2/3将注释转换为包裹式输出元素?
解决HTML注释区块转XML元素的XSLT分组问题
问题背景
需要将非规范HTML转换为XML,以便通过XSLT批量处理数千份文档生成指定格式JSON。现有HTML使用<!-- TAG_NAME -->与<!-- /TAG_NAME -->形式的注释标记内容区块,目标是将这类注释替换为对应XML元素,包裹两个注释之间的所有内容。当前尝试的XSLT代码仅能匹配结束注释前的第一个同级节点,无法正确分组包裹所有中间节点,需修正分组逻辑。
待处理HTML示例
<!-- IMAGECOUNT --> <table width="100%"> <tr> <td colspan="1" rowspan="1"> <strong> 2 Images </strong> </td> </tr> </table> <!-- /IMAGECOUNT --> <p> <strong> <!-- SECTION --> The section <!-- /SECTION --> <!-- COUNTRY --> The country <!-- /COUNTRY --> </strong> </p> <!-- DATE --> <p> <font size="-1"> <b>Date Posted: 09-Dec-2019</b> </font> </p> <!-- /DATE --> <!-- TEXT --> <center> <p> <font size="-1"> Just some text </font> </p> </center> <!-- /TEXT --> <!-- TOP THUMBNAILS --> <table class="tabletopbottom" width="100%"> <tr> <td colspan="1" rowspan="1"> <img src="images/s1353556.jpg" alt="Cat"/> <img src="images/s1164352.jpg" alt="Dog"/> </td> </tr> </table> <!-- /TOP THUMBNAILS -->
尝试的XSLT代码(存在问题)
<?xml version="1.0" encoding="UTF-8"?> <xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="3.0"> <!-- Identity transform template --> <xsl:template match="@* | node()"> <xsl:copy> <xsl:apply-templates select="@* | node()"/> </xsl:copy> </xsl:template> <!-- Match opening comments and create elements --> <xsl:template match="comment()" priority="10"> <xsl:variable name="commentContent" select="normalize-space(.)"/> <xsl:message>Processing comment: <xsl:value-of select="$commentContent"/></xsl:message> <xsl:choose> <xsl:when test="starts-with($commentContent, '/')"> <xsl:message> Ignoring closing comment: <xsl:value-of select="$commentContent"/></xsl:message> </xsl:when> <xsl:otherwise> <xsl:message>Creating element for comment: <xsl:value-of select="$commentContent"/></xsl:message> <xsl:element name="{replace($commentContent, ' ', '')}"> <xsl:apply-templates select="following-sibling::node()[1][not(self::comment())]"/> </xsl:element> </xsl:otherwise> </xsl:choose> </xsl:template> </xsl:stylesheet>
问题分析
原代码仅通过following-sibling::node()[1]选择当前注释后的第一个非注释节点,无法覆盖到结束注释前的所有节点。同时,未处理结束注释的跳过逻辑,会导致结束注释被保留或重复处理。
解决方案
使用XSLT 3.0的for-each-group分组功能,通过group-starting-with匹配开头注释,group-ending-with匹配对应的结束注释,将两个注释之间的所有节点作为一组,再用对应名称的XML元素包裹该组内容。
修正后的XSLT代码
<?xml version="1.0" encoding="UTF-8"?> <xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="3.0"> <!-- Identity transform: 默认复制所有节点,后续模板覆盖特定情况 --> <xsl:template match="@* | node()"> <xsl:copy> <xsl:apply-templates select="@* | node()"/> </xsl:copy> </xsl:template> <!-- 对同级节点按注释区块分组处理 --> <xsl:template match="*"> <xsl:copy> <xsl:apply-templates select="@*"/> <xsl:for-each-group select="node()" group-starting-with="comment()[normalize-space(.) != '' and not(starts-with(normalize-space(.), '/'))]"> <!-- 处理分组:如果是注释开头的组 --> <xsl:choose> <xsl:when test="self::comment()"> <xsl:variable name="tagName" select="normalize-space(.)"/> <xsl:variable name="closingTag" select="concat('/', $tagName)"/> <!-- 检查是否有对应的结束注释 --> <xsl:if test="current-group()[last()][self::comment() and normalize-space(.) = $closingTag]"> <!-- 创建替换的XML元素,移除标签名中的空格 --> <xsl:element name="{replace($tagName, ' ', '')}"> <!-- 处理组内除开头和结尾注释外的所有节点 --> <xsl:apply-templates select="current-group()[position() > 1 and position() < last()]"/> </xsl:element> </xsl:if> <!-- 无对应结束注释的情况:直接复制开头注释及后续节点 --> <xsl:if test="not(current-group()[last()][self::comment() and normalize-space(.) = $closingTag])"> <xsl:apply-templates select="current-group()"/> </xsl:if> </xsl:when> <!-- 非注释开头的组:直接复制所有节点 --> <xsl:otherwise> <xsl:apply-templates select="current-group()"/> </xsl:otherwise> </xsl:choose> </xsl:for-each-group> </xsl:copy> </xsl:template> <!-- 单独匹配结束注释,直接跳过不输出 --> <xsl:template match="comment()[starts-with(normalize-space(.), '/')]"/> </xsl:stylesheet>
代码说明
- 分组逻辑:通过
for-each-group将节点按开头注释分组,每组从<!-- TAG -->开始,到对应的<!-- /TAG -->结束。 - 元素创建:提取注释中的标签名,替换空格后作为XML元素名,包裹组内除首尾注释外的所有节点。
- 异常处理:如果开头注释没有对应的结束注释,会按原结构保留节点,避免数据丢失。
- 结束注释处理:单独匹配结束注释,直接跳过不输出,避免残留无用注释。
注意事项
- 确保使用支持XSLT 3.0的处理器(如Saxon HE/PE/EE)。
- 需保证HTML中的注释区块无嵌套结构,否则分组逻辑可能失效。
- 标签名中的空格会被移除(如
TOP THUMBNAILS转为TOPTHUMBNAILS),若需保留空格可调整replace逻辑。
内容的提问来源于stack exchange,提问作者Binsky
相关产品推荐
相关产品推荐

