XSLT分组主要有两种方式:XSLT 2.0+使用for-each-group指令,通过group-by等属性实现直观高效的分组;XSLT 1.0则依赖Muenchian Grouping,利用key()和generate-id()筛选每组首个节点,虽复杂但有效。
XSLT对节点进行分组操作,核心上来说,主要有两种主流方式:对于XSLT 2.0及更高版本,我们有强大的
for-each-group
指令,它极大地简化了分组逻辑;而在XSLT 1.0时代,则需要依赖一种被称为Muenchian Grouping(门兴分组)的技巧,通过
key()
函数和
generate-id()
的组合来实现。
解决方案
要对XSLT中的节点进行分组,我们通常会根据某个节点的特定属性值、子节点内容或者计算出的某个键值来组织相关的节点集合。这在数据转换中非常常见,比如把扁平化的数据转换成带有层级结构的报告。
1. XSLT 2.0+ 中的
for-each-group
指令
这是现代XSLT处理分组的首选方式,它直观且强大。
for-each-group
允许你指定一个节点集合,然后根据一个表达式对这些节点进行分组。
<xsl:stylesheet version="2.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform"> <xsl:output method="xml" indent="yes"/> <xsl:template match="/root"> <grouped-data> <!-- 根据'category'属性进行分组 --> <xsl:for-each-group select="item" group-by="@category"> <category-group name="{current-grouping-key()}"> <xsl:for-each select="current-group()"> <item-detail id="{@id}"> <name><xsl:value-of select="name"/></name> <price><xsl:value-of select="price"/></price> </item-detail> </xsl:for-each> </category-group> </xsl:for-each-group> </grouped-data> </xsl:template> </xsl:stylesheet>
对应这个XML输入:
<root> <item id="A001" category="Electronics"> <name>Laptop</name> <price>1200</price> </item> <item id="A002" category="Books"> <name>XSLT Cookbook</name> <price>45</price> </item> <item id="A003" category="Electronics"> <name>Mouse</name> <price>25</price> </item> <item id="A004" category="Books"> <name>XML Basics</name> <price>30</price> </item> </root>
输出会是:
<grouped-data> <category-group name="Electronics"> <item-detail id="A001"> <name>Laptop</name> <price>1200</price> </item-detail> <item-detail id="A003"> <name>Mouse</name> <price>25</price> </item-detail> </category-group> <category-group name="Books"> <item-detail id="A002"> <name>XSLT Cookbook</name> <price>45</price> </item-detail> <item-detail id="A004"> <name>XML Basics</name> <price>30</price> </item-detail> </category-group> </grouped-data>
group-by
属性定义了分组的依据。
current-grouping-key()
用于获取当前组的键值,而
current-group()
则返回当前组中的所有节点。
2. XSLT 1.0 中的 Muenchian Grouping(键控分组)
在没有
for-each-group
的日子里,Muenchian Grouping是实现分组的“黑魔法”。它利用了
key()
函数能够高效查找节点,以及
generate-id()
函数为每个节点生成唯一ID的特性。其核心思想是:只处理每个分组的“第一个”节点。
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform"> <xsl:output method="xml" indent="yes"/> <!-- 定义一个键,用于按'category'属性查找item节点 --> <xsl:key name="items-by-category" match="item" use="@category"/> <xsl:template match="/root"> <grouped-data> <!-- 遍历所有item节点,但只选择每个分组的第一个节点 --> <xsl:for-each select="item[count(. | key('items-by-category', @category)[1]) = 1]"> <xsl:variable name="currentCategory" select="@category"/> <category-group name="{$currentCategory}"> <!-- 遍历当前分组的所有节点 --> <xsl:for-each select="key('items-by-category', $currentCategory)"> <item-detail id="{@id}"> <name><xsl:value-of select="name"/></name> <price><xsl:value-of select="price"/></price> </item-detail> </xsl:for-each> </category-group> </xsl:for-each> </grouped-data> </xsl:template> </xsl:stylesheet>
使用与上面相同的XML输入,输出结果会是一致的。这里的
item[count(. | key('items-by-category', @category)[1]) = 1]
是Muenchian Grouping的精髓,它巧妙地筛选出每个分组的“领导者”节点。
XSLT 2.0+中如何使用
for-each-group
for-each-group
进行高效分组?
老实说,自从XSLT 2.0引入
for-each-group
,分组操作简直是鸟枪换炮,变得异常直观和强大。我个人觉得,这玩意儿是XSLT 2.0最令人拍案叫绝的特性之一,它把之前那些需要绞尽脑汁才能实现的复杂逻辑,一下子拉到了“所见即所得”的层面。
for-each-group
指令的核心在于它的几个关键属性:
-
select
select="item"
就是选择所有名为
item
的节点。
-
group-by
-
group-starting-with
<h3>
后面跟着若干个
<p>
,直到下一个
<h3>
出现。
-
group-ending-with
group-starting-with
,但它定义的是一个组的结束条件。
-
group-adjacent
group-by
有点像,但更强调“相邻”这个概念,如果中间隔了其他键值的节点,即使后面有相同键值的节点,也不会归为同一个组。
在
for-each-group
内部,有两个非常重要的函数:
-
current-group()
-
current-grouping-key()
举个更复杂的例子,我们想分组销售订单,先按年份,再按月份:
XML输入:
<sales> <order id="1" date="2023-01-15" amount="100"/> <order id="2" date="2023-02-20" amount="150"/> <order id="3" date="2023-01-25" amount="120"/> <order id="4" date="2022-11-01" amount="80"/> <order id="5" date="2023-02-10" amount="200"/> </sales>
XSLT:
<xsl:stylesheet version="2.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform"> <xsl:output method="xml" indent="yes"/> <xsl:template match="/sales"> <yearly-sales> <!-- 第一层分组:按年份 --> <xsl:for-each-group select="order" group-by="substring(@date, 1, 4)"> <year-group year="{current-grouping-key()}"> <!-- 第二层分组:在当前年份组内,再按月份分组 --> <xsl:for-each-group select="current-group()" group-by="substring(@date, 6, 2)"> <month-group month="{current-grouping-key()}"> <total-amount><xsl:value-of select="sum(current-group()/@amount)"/></total-amount> <orders-in-month> <xsl:for-each select="current-group()"> <order-summary id="{@id}" date="{@date}" amount="{@amount}"/> </xsl:for-each> </orders-in-month> </month-group> </xsl:for-each-group> </year-group> </xsl:for-each-group> </yearly-sales> </xsl:template> </xsl:stylesheet>
这个例子展示了嵌套分组的强大。我们先按年份
substring(@date, 1, 4)
分组,然后在每个年份组内,又对
current-group()
(即当前年份的所有订单)进行月份
substring(@date, 6, 2)
分组。这种层层递进的逻辑,用
for-each-group
来表达简直是水到渠成,写起来也相当顺手。
XSLT 1.0环境下,Muenchian分组模式的实现与局限性
Muenchian Grouping,这个名字听起来有点酷,但它的实现方式,对于初学者来说,绝对是XSLT 1.0时代的一个“智力挑战”。它不是一个内置指令,而是一种巧妙地利用XSLT 1.0固有功能的模式。我记得刚开始学XSLT 1.0的时候,理解这个模式花了我不少时间,因为它确实有点反直觉。
核心思想:
- 定义键(
xsl:key
)
: 使用xsl:key
来创建一个索引,将所有需要分组的节点与它们的“分组键”关联起来。
- 找到每个组的“头”节点: 这是最关键的一步。我们遍历所有节点,但只选择那些在
key()
函数返回的节点集中,它自己就是第一个节点的。
count(. | key('your-key', your-criteria)[1]) = 1
就是这个魔法表达式。
-
key('your-key', your-criteria)
:返回所有符合
your-criteria
的节点。
-
key('your-key', your-criteria)[1]
:返回这些节点中的第一个。
-
count(. | ...)
:这是一个集合运算,计算当前节点和第一个节点合并后的节点数量。如果当前节点就是第一个节点,那么合并后节点数量是1;如果不是,合并后节点数量是2。所以,
= 1
就筛选出了每个组的第一个节点。
-
- 遍历组内成员: 找到组头后,再次使用
key()
函数,传入组头的键值,就能获取该组的所有成员。
我们还是用之前的商品分类XML来演示Muenchian Grouping:
<root> <item id="A001" category="Electronics"> <name>Laptop</name> <price>1200</price> </item> <item id="A002" category="Books"> <name>XSLT Cookbook</name> <price>45</price> </item> <item id="A003" category="Electronics"> <name>Mouse</name> <price>25</price> </item> <item id="A004" category="Books"> <name>XML Basics</name> <price>30</price> </item> </root>
XSLT 1.0 (Muenchian Grouping):
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform"> <xsl:output method="xml" indent="yes"/> <!-- 定义一个键,用于按'category'属性查找item节点 --> <xsl:key name="items-by-category" match="item" use="@category"/> <xsl:template match="/root"> <grouped-data> <!-- 遍历所有item节点,但只选择每个分组的第一个节点 --> <xsl:for-each select="item[count(. | key('items-by-category', @category)[1]) = 1]"> <xsl:variable name="currentCategory" select="@category"/> <category-group name="{$currentCategory}"> <!-- 遍历当前分组的所有节点 --> <xsl:for-each select="key('items-by-category', $currentCategory)"> <item-detail id="{@id}"> <name><xsl:value-of select="name"/></name> <price><xsl:value-of select="price"/></price> </item-detail> </xsl:for-each> </category-group> </xsl:for-each> </grouped-data> </xsl:template> </xsl:stylesheet>
局限性:
- 复杂性高: 表达式
item[count(. | key('items-by-category', @category)[1]) = 1]
对于不熟悉XSLT 1.0技巧的人来说,确实难以理解和记忆。维护起来也容易出错。
- 可读性差: 相比
for-each-group
的语义化,Muenchian Grouping的代码看起来更像是一种“黑客行为”,而不是清晰的意图表达。
- 性能考量: 虽然
key()
函数本身是优化的,但在处理超大型文档时,反复调用
key()
可能会带来一定的性能开销。
- 不支持复杂分组条件: Muenchian Grouping主要适用于基于单一键值的分组。像
group-starting-with
那种基于节点位置和上下文的分组,用Muenchian Grouping实现起来会异常困难,甚至不可能。
- 嵌套分组的挑战: 虽然可以实现,但嵌套的Muenchian Grouping会使得表达式更加复杂,代码更难维护。
尽管有这些局限性,但对于那些仍然运行在XSLT 1.0环境下的系统来说,Muenchian Grouping依然是不可或缺的技能。它证明了即使在语言特性有限的情况下,开发者也能通过巧妙的组合实现复杂的功能。
除了基本分组,XSLT还能实现哪些复杂的节点分组场景?
XSLT的分组能力远不止是简单地按一个字段值来归类。在实际项目中,我遇到过各种稀奇古怪的分组需求,有些真的需要跳出常规思维去解决。这正是XSLT的魅力所在,它提供了一套工具集,让你能像搭积木一样,构建出满足特定业务逻辑的转换。
-
连续兄弟节点分组(
group-starting-with
的妙用) 这是我个人觉得
for-each-group
最出彩的地方之一,尤其是在处理半结构化文档(比如HTML)时。想象一下,你有一段HTML,里面有标题
<h2>
,后面跟着几个段落
<p>
,然后又是另一个
<h2>
。你希望把每个
<h2>
和它后面的所有
<p>
(直到下一个
<h2>
出现)作为一个组。
XML输入 (模拟HTML片段):
<document> <h2>Section A</h2> <p>Content for A, paragraph 1.</p> <p>Content for A, paragraph 2.</p> <h2>Section B</h2> <p>Content for B, paragraph 1.</p> <ul><li>List item 1</li><li>List item 2</li></ul> <p>Content for B, paragraph 2.</p> <h2>Section C</h2> <p>Content for C.</p> </document>
XSLT (使用
group-starting-with
):
<xsl:stylesheet version="2.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform"> <xsl:output method="xml" indent="yes"/> <xsl:template match="/document"> <sections> <!-- 以h2节点作为新组的开始 --> <xsl:for-each-group select="*" group-starting-with="h2"> <section title="{current-group()[1]}"> <!-- current-group()[1]是h2节点 --> <content> <xsl:copy-of select="current-group()[position() > 1]"/> <!-- 复制h2后面的所有内容 --> </content> </section> </xsl:for-each-group> </sections> </xsl:template> </xsl:stylesheet>
这里,
group-starting-with="h2"
告诉XSLT,每当遇到一个
<h2>
节点,就开启一个新的组。这个组会包含
<h2>
本身,以及它后面所有的兄弟节点,直到遇到下一个
<h2>
为止。这对于将扁平的HTML结构转换为逻辑上的章节结构非常有效。
-
基于动态或计算值的分组 有时候,分组的依据不是一个简单的属性值,而是一个需要计算出来的结果。比如,我们想把商品按照价格区间(0-50, 51-100, 101-200等)来分组。
XSLT (计算价格区间):
<xsl:stylesheet version="2.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform"> <xsl:output method="xml" indent="yes"/> <xsl:template match="/root"> <price-ranges> <xsl:for-each-group select="item" group-by="floor(