分组在XSLT处理中是一个很常见的问题,怎样才能将一系列XML节点元素组织到不同的分组里?最常见的情况是处理从数据库中得到的XML输出,数据库通常是根据数据表中的记录生成结果。例如,一个地址簿可能输出下面的内容:
< records >
< contact id ="0001" >
< title > Mr </ title >
< forename > John </ forename >
< surname > Smith </ surname >
</ contact >
< contact id ="0002" >
< title > Dr </ title >
< forename > Amy </ forename >
< surname > Jones </ surname >
</ contact >
...
</ records >
现在的问题是怎样把这段文本转换成多个按照surname来分组的列表,例如:
Jones, < br />
Amy (Dr) < br />
Brian (Mr) < br />
Smith, < br />
Fiona (Ms) < br />
John (Mr) < br />
通过以下两个步骤可实现这个需求:
- 找出都有哪些surname。
- 获取具有相同surname值的所有contact节点。
要找出都有哪些surname就要先找出每个surname对应的第一个contact节点,一个方法就是找出那些surname与先前节点的surname都不同的的contact节点。
contact[not(surname = preceding-sibling::contact/surname)]
一旦这些contact节点被找出,就很容易找出它们对应的surname了,然后在此基础上再找出具有相同surname的所有contact节点。
<xsl:apply-templates select="/records/contact[surname = current()/surname]" />
但是对于那些比较大的XML数据(比如海量数据库),这种方法也存在缺点,它使用了两个XPath查询进行了大量的处理。使用preceding-siblings遍历所有前驱兄弟节点会花费大量的时间,尤其是当前节点处于尾部的时候。同样地,查找所有具有某个指定surname的contact节点也会遍历所有contact节点。这种做法效率是很低的。
Muenchian是由Steve Muench开发的一种分组方法,它通过keys更高效的实现这种需求。Keys通过分配一个key值给一个节点,然后就可以简单的通过key值来访问节点。如果存在多个节点有相同的key值,那么通过这个key值就可以取到所有这些节点。这意味着,如果你想根据节点的某个属性对一组节点进行高效分组时,可以使用keys。
再回到上面的例子,我们想通过surname来对所有的contact节点分组,于是就创建一个key,并且给每个contact节点一个key值(也就是surname值)。需要分组的节点匹配match属性指定的表达式,key值通过use属性的表达式指定,如下:
< xsl:key name ="contacts-by-surname" match ="contact" use ="surname" />
在key定义好以后,如果知道了surname,我们便可快速查找到相应的contact节点,例如下面的表达式将给出所有surname为Smith的contact节点。
key('contacts-by-surname', 'Smith')
然后这就可以很容易实现上面提到的第二个步骤。
< xsl:apply-templates select ="key('contacts-by-surname', surname)" />
不过,我们首先需要做的是找出都有哪些surname,同时找出具有某个surname的第一个contact节点。在这里我们可以再次使用keys。当我们把surname作为key的时候,contact节点就已经是这个集合的一部分了,但它是否是集合中的第一个元素呢(按照XML文档顺序)?还是在排在后面?我们仅仅关心集合中的第一个元素。
要找出一个contact节点是否为由key筛选出的结果中的第一个元素,就要把contact和由key筛选出的结果中的第一个元素作比较。有很多常用的方法可以用来判断两个节点是不是同一个节点:
1. 比较通过generate-id()函数生成的唯一标识符。
contact[generate-id() = generate-id(key('contacts-by-surname', surname)[1])]
2. 检查由这两个节点组成的新集合内部有一个还是两个节点——因为同一个节点在节点集合中不可能出现两次,所以如果这个集合中只有一个元素,那么这之前提到的两个节点肯定就是同一个节点。
contact[count(. | key('contacts-by-surname', surname)[1]) = 1]
在找出所有的组以后,可以按照需求对它们排序。同样,组内的节点也可以进行排序。最终,利用下面的这个template,我们就可以分组输出从数据中取到的XML信息。
< xsl:key name ="contacts-by-surname" match ="contact" use ="surname" />
< xsl:template match ="records" >
< xsl:for-each select ="contact[count(. | key('contacts-by-surname', surname)[1]) = 1]" >
< xsl:sort select ="surname" />
< xsl:value-of select ="surname" /> , < br />
< xsl:for-each select ="key('contacts-by-surname', surname)" >
< xsl:sort select ="forename" />
< xsl:value-of select ="forename" /> ( < xsl:value-of select ="title" /> ) < br />
</ xsl:for-each >
</ xsl:for-each >
</ xsl:template >
通常,Muenchian是最好的节点分组方法,因为它不需要遍历大量的节点,所以更高效。这种方法特别适用于处理从数据库中获取到的表结构数据,例如以某种层级关系重新组织这些从数据库中得到的数据。它也适用于任何情况下利用XPath可访问到的节点属性对节点集合进行分组。
Muenchian有一个缺点,它只能在支持keys的XSLT处理器下使用。这就排除了James Clark的xt还有2000年5月以前发布的MSXML。另外,使用keys也是相当消耗内存资源的,因为所有的节点和key值都必须保存在内存中。最后,在多个XML文档之间使用keys进行分组也是一项比较复杂的工作。
相关链接:
XSLT Recommendation: generate-id()
[原文地址:http://www.jenitennison.com/xslt/grouping/muenchian.html]