XSLT是处理XML文档转换的经典技术,但很多使用者在入门阶段接触的往往是XSLT 1.0版本,习惯了它简陋的表达能力和繁琐的变通写法。实际上,XSLT 2.0早在多年前就已经成熟,配合XPath 2.0引擎(如Saxon、Altova XMLSpy内置处理器),可以极大简化样式表的编写。本文将围绕XSLT 2.0的主要新特性展开,并重点讲解最实用的xsl:for-each-group与group-by分组用法。

XSLT 2.0带来了哪些核心新特性
首先是数据模型层面的升级。XSLT 2.0基于XPath 2.0,引入了序列(sequence)的概念,节点集被泛化为可以包含原子值的有序序列,这带来了一系列实用的新函数,例如avg、min、max、distinct-values、tokenize等。在1.0时代求平均值需要手工累加再除以数量,现在一行表达式即可完成。
其次是强类型支持。2.0允许在样式表中声明参数和变量的数据类型,也可以导入XML Schema并利用schema-element、element(*, TypeName)等结构做类型校验,提前在编译阶段发现错误,这对大型样式表的维护非常有帮助。
再者,正则表达式的引入让文本处理能力大幅增强。xsl:analyze-string指令配合regex属性可以对字符串进行模式匹配,通过xsl:matching-substring和xsl:non-matching-substring分别处理命中与未命中的片段,常用于从非结构化文本中抽取数据。此外,多结果文档输出(xsl:result-document)、用户自定义函数(xsl:function)、临时树(xsl:variable直接构造节点)、字符串与日期时间函数的丰富,也都显著提升了开发效率。
xsl:for-each-group与group-by的基本语法
分组是XSLT 1.0最令人头疼的场景之一,通常要依赖xsl:key配合generate-id之类的技巧模拟。XSLT 2.0提供了专门的分组指令xsl:for-each-group,其select属性指定待分组的节点集合,group-by属性指定分组键表达式。处理器会为每个不同的分组键值生成一个组,组内可以通过current-group()访问所有成员,通过current-grouping-key()访问当前的键值。
下面用一个典型示例说明。假设有如下XML数据,记录了多个城市所属的省份:
<cities> <city name="杭州" province="浙江"/> <city name="宁波" province="浙江"/> <city name="南京" province="江苏"/> <city name="苏州" province="江苏"/> <city name="广州" province="广东"/> </cities>
现在希望按省份分组输出,每个省份下列出所有城市。使用group-by的写法如下:
<xsl:template match="cities">
<provinces>
<xsl:for-each-group select="city" group-by="@province">
<province name="{current-grouping-key()}">
<xsl:for-each select="current-group()">
<city><xsl:value-of select="@name"/></city>
</xsl:for-each>
</province>
</xsl:for-each-group>
</provinces>
</xsl:template>这段样式表的逻辑非常直观:外层xsl:for-each-group遍历每一个分组,内层用current-group()遍历组内成员。输出结果会按照省份聚合出三个<province>元素,每个元素下包含对应的城市列表。需要注意的是,group-by的分组键可以是任意XPath表达式,不限于属性,也可以是子元素文本、拼接字符串甚至计算值。
group-by的进阶用法与常见注意事项
除了基本分组,group-by还可以组合聚合统计。例如统计每个省份的城市数量,或者对组内数据求和:
<xsl:for-each-group select="city" group-by="@province">
<province name="{current-grouping-key()}"
count="{count(current-group())}"/>
</xsl:for-each-group>排序方面,xsl:for-each-group本身支持子级xsl:sort指令,可以按分组键排序输出,也可以在组内对current-group()排序。需要注意的是xsl:sort必须写在xsl:for-each-group内部第一个位置,作用于分组结果本身,这与对组内成员排序是两个不同层次的操作。
另一个容易踩坑的点是分组键的数据类型。如果分组键是数值类型,2.0处理器会比较其数值相等性;而字符串类型则严格区分大小写。此外,group-by的表达式返回的是序列,如果返回多个值,一个节点可能同时落入多个分组,这在处理多标签、多分类数据时反而是个强大的特性。最后提醒一点,xsl:for-each-group还有group-adjacent、group-starting-with、group-ending-with三种兄弟分组方式,分别适用于相邻分组和按起始元素切分的场景,它们与group-by互斥使用,掌握之后几乎可以覆盖所有XML分组需求。处理器方面建议使用Saxon-HE,它对XSLT 2.0规范的支持最为完整,且可免费商用。