如何在XSLT中存储转换后XML并构建分类树结构?
XSLT实现分类树生成:去重与ID/父节点属性处理
问题说明
关于XSLT中存储转换后XML的问题:在XSLT 1.0中,无法直接将动态生成的XML节点树存储到变量中(XSLT 2.0及以上支持节点序列变量),但可以通过键(Key)、递归模板等方式直接基于原数据完成多阶段处理,无需显式存储中间XML。
业务需求:从包含层级分类文本的商品XML中,生成无重复的分类树结构,每个分类节点需要分配唯一递增ID,并正确关联父节点ID。
初始XML
<shop> <product> <name /> <categorytxt> Garden > Tables > Blue </categorytxt> </product> <product> <name/> <categorytxt> Garden > Tables > Green </categorytxt> </product> <product> <name /> <categorytxt> Home > Tables > Green </categorytxt> </product> <product> <name /> <categorytxt> Home > Chairs > White </categorytxt> </product> </shop>
期望输出XML
<!-- category treeview structure --> <categories> <category id="1" parent="root">Garden</category> <category id="2" parent="1">Tables</category> <category id="3" parent="2">Blue</category> <category id="4" parent="2">Green</category> <category id="5" parent="root">Home</category> <category id="6" parent="5">Tables</category> <category id="7" parent="6">Green</category> <category id="8" parent="5">Chairs</category> <category id="9" parent="8">White</category> </categories>
现有代码(待优化)
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="1.0"> <xsl:output indent="no" method="xml" /> <xsl:template match="/"> <categories> <xsl:for-each select="//categorytxt[not(.=../../preceding-sibling::categorytxt/product)]"> <xsl:sort select="." /> <xsl:call-template name="output-tokens"> <xsl:with-param name="num" select="1" /> <xsl:with-param name="list" select="concat(. ,' > ')" /> </xsl:call-template> </xsl:for-each> </categories> </xsl:template> <xsl:template name="output-tokens"> <xsl:param name="num" /> <xsl:param name="list" /> <xsl:variable name="newlist" select="concat(normalize-space($list), ' ')" /> <xsl:variable name="first" select="substring-before($newlist, '>')" /> <xsl:variable name="remaining" select="substring-after($newlist, '>')" /> <xsl:if test="$first"> <category id="{$num}"> <xsl:copy-of select="normalize-space($first)" /> </category> </xsl:if> <xsl:if test="$remaining"> <xsl:call-template name="output-tokens"> <xsl:with-param name="num" select="$num + 1"/> <xsl:with-param name="list" select="$remaining" /> </xsl:call-template> </xsl:if> </xsl:template> </xsl:stylesheet>
解决方案与完整代码
核心思路:
- 用
<xsl:key>定义分类路径的唯一性,确保每个完整路径(如Garden、Garden > Tables)只生成一次节点 - 递归处理分类层级,为每个唯一路径分配递增ID,并通过父路径查找对应的父ID
- 按层级顺序生成节点,保证父节点先于子节点被创建
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="1.0"> <xsl:output indent="yes" method="xml"/> <!-- 定义键:以分类的完整路径为键,确保每个唯一路径只匹配一次 --> <xsl:key name="category-path" match="category-path" use="."/> <!-- 预处理所有分类文本,生成完整路径列表 --> <xsl:variable name="all-category-paths"> <xsl:for-each select="//categorytxt"> <xsl:call-template name="generate-paths"> <xsl:with-param name="full-path" select="normalize-space(.)"/> </xsl:call-template> </xsl:for-each> </xsl:variable> <!-- 主模板 --> <xsl:template match="/"> <categories> <!-- 提取所有唯一的分类路径,按路径排序 --> <xsl:for-each select="msxsl:node-set($all-category-paths)/category-path[generate-id() = generate-id(key('category-path', .)[1])]"> <xsl:sort select="."/> <xsl:call-template name="create-category-node"> <xsl:with-param name="path" select="."/> </xsl:call-template> </xsl:for-each> </categories> </xsl:template> <!-- 递归生成完整路径片段(如Garden > Tables会生成Garden、Garden > Tables) --> <xsl:template name="generate-paths"> <xsl:param name="full-path"/> <xsl:if test="$full-path"> <category-path><xsl:value-of select="$full-path"/></category-path> <xsl:call-template name="generate-paths"> <xsl:with-param name="full-path" select="substring-before($full-path, ' > ')"/> </xsl:call-template> </xsl:if> </xsl:template> <!-- 为分类路径生成节点,分配ID和父ID --> <xsl:template name="create-category-node"> <xsl:param name="path"/> <xsl:variable name="parent-path" select="substring-before($path, ' > ')"/> <xsl:variable name="category-name" select="substring-after($path, ' > ')"/> <!-- 计算当前分类的ID:统计所有路径长度小于等于当前路径的唯一节点数量 --> <xsl:variable name="current-id"> <xsl:for-each select="msxsl:node-set($all-category-paths)/category-path[generate-id() = generate-id(key('category-path', .)[1])]"> <xsl:if test="string-length(.) <= string-length($path) and contains(concat(' > ', $path, ' > '), concat(' > ', ., ' > '))"> <xsl:value-of select="position()"/> </xsl:if> </xsl:for-each> </xsl:variable> <!-- 生成节点 --> <category id="{$current-id}"> <!-- 设置父节点属性 --> <xsl:if test="$parent-path"> <xsl:attribute name="parent"> <xsl:for-each select="msxsl:node-set($all-category-paths)/category-path[generate-id() = generate-id(key('category-path', .)[1])]"> <xsl:if test=". = $parent-path"> <xsl:value-of select="position()"/> </xsl:if> </xsl:for-each> </xsl:attribute> </xsl:if> <xsl:if test="not($parent-path)"> <xsl:attribute name="parent">root</xsl:attribute> </xsl:if> <!-- 分类名称:如果是根分类则直接取路径,否则取最后一段 --> <xsl:value-of select="if($parent-path) then $category-name else $path"/> </category> </xsl:template> </xsl:stylesheet>
关键逻辑说明
- 路径预处理:
generate-paths模板将每个完整分类路径拆分为所有层级的片段(如Garden > Tables > Blue会生成Garden > Tables > Blue、Garden > Tables、Garden),存储到变量中。 - 去重处理:通过
<xsl:key>和generate-id()筛选出所有唯一的路径片段,避免重复生成分类节点。 - ID与父ID分配:通过统计路径在唯一列表中的位置得到ID;父ID则通过查找父路径在列表中的位置获取,根分类的父节点设为
root。
内容的提问来源于stack exchange,提问作者Simon Zeman
相关产品推荐
相关产品推荐

