XSLT排序重复元素的最优实现方案及问题解决
基于子树元素名称串联的XSLT 3.0排序实现
针对你提出的XML排序需求(属性按字母序、子元素按名称字母序、同名子元素基于子树元素名称串联键排序),以下是改进后的解决方案,可解决原代码的边缘问题:
原代码问题分析
原代码中使用<xsl:sort select="."/>作为同名元素的排序键存在两个缺陷:
- 仅取当前元素的文本内容,未递归遍历子树结构,导致含嵌套元素的同名元素无法正确排序,需多次执行才生效
- 空子元素的
.为空字符串,无法区分不同的空元素节点
改进思路
通过定义递归函数,遍历元素的所有子元素(按文档顺序),将所有子元素的名称串联成一个字符串作为排序键。这样同名元素可通过子树的元素结构差异生成唯一且可比较的排序键,实现一致明确的排序。
完整XSLT代码
<?xml version="1.0" encoding="UTF-8"?> <xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:local="http://example.com/local" version="3.0"> <xsl:output method="xml" indent="yes"/> <!-- 递归函数:生成子树所有元素名称的串联字符串 --> <xsl:function name="local:generate-subtree-name-key" as="xs:string"> <xsl:param name="node" as="node()"/> <xsl:sequence> <!-- 拼接当前元素的名称(如果是元素节点) --> <xsl:if test="$node[self::element()]"> <xsl:value-of select="name($node)"/> </xsl:if> <!-- 递归处理所有子元素节点(按文档顺序) --> <xsl:for-each select="$node/child::element()"> <xsl:value-of select="local:generate-subtree-name-key(.)"/> </xsl:for-each> </xsl:sequence> </xsl:function> <!-- 核心复制模板 --> <xsl:template match="node()|@*"> <xsl:copy> <!-- 属性按名称字母序排序 --> <xsl:apply-templates select="@*"> <xsl:sort select="name()"/> </xsl:apply-templates> <!-- 子元素排序:先按名称,再按子树元素名称串联键 --> <xsl:apply-templates select="node()"> <xsl:sort select="name()"/> <xsl:sort select="local:generate-subtree-name-key(.)"/> <!-- 可选:如果子树键相同,再按文本内容排序 --> <xsl:sort select="normalize-space(.)"/> </xsl:apply-templates> </xsl:copy> </xsl:template> </xsl:stylesheet>
代码说明
递归函数
local:generate-subtree-name-key:- 接收一个节点参数,递归遍历其所有子元素
- 按文档顺序拼接当前元素和所有子元素的名称,生成唯一的排序键
- 忽略文本、注释等非元素节点(若需包含文本可自行扩展)
排序逻辑优化:
- 子元素先按名称字母序排序
- 同名元素按子树元素名称串联键排序,解决嵌套元素的排序问题
- 可选添加文本内容排序作为 fallback,处理子树结构完全相同但文本不同的元素
测试示例验证
针对你提供的示例XML:
<colours> <aa> <cc> <rr/> </cc> <ff/> </aa> <aa> <cc> <ee/> </cc> <zz/> </aa> <violet/> </colours>
- 第一个
<aa>的子树键为:aaccrrff(注:函数会包含当前元素名称,若需仅子元素名称可修改函数去掉当前元素的名称输出) - 第二个
<aa>的子树键为:aacceezz - 按字母序比较,
aacceezz<aaccrrff,因此第二个<aa>会排在第一个<aa>之前,最终输出排序后的XML:
<?xml version="1.0" encoding="UTF-8"?> <colours> <aa> <cc> <ee/> </cc> <zz/> </aa> <aa> <cc> <rr/> </cc> <ff/> </aa> <violet/> </colours>
(若你希望排序键仅包含子元素名称而非当前元素,可修改函数中的<xsl:if test="$node[self::element()]">逻辑,去掉当前元素名称的拼接)
内容的提问来源于stack exchange,提问作者Justin Weller
相关产品推荐
相关产品推荐

