XML

简化 XML 文档转换的五个 XSLT 2.0 特性
作者 Jinyu Wang

了解如何利用这些 2.0 特性来克服 XSLT 1.0 的局限性

XSLT 是一种功能强大的语言,已经广泛用于转换 XML 文档。但是,XSLT 在其目前的 1.0 版本中具有局限性,可能使样式表的编写变得困难而复杂。在本篇技术文章中,我将描述 XSLT 2.0 中的五个新特性,它们将有助于克服这些局限性:

  • 分组: 允许进行简化而高效的内容分组
  • 多个输出: 在一次 XSLT 转换中创建多个输出文档
  • 临时树: 消除节点集的转换
  • 字符映射: 替代容易出错的字符换码
  • 数据类型绑定:允许根据数据类型来处理数据。

这些新的 XSLT 2.0 特性不仅丰富了 XML 转换的功能,而且优化了 XML 转换的性能。

设置 XSLT 命令行实用程序

您可以使用 Oracle XDK 10g(可从 Oracle OTN XML 中心下载)中提供的 oracle.xml.parser.v2.oraxsl 命令行实用程序来运行 XSLT 转换。

您可以从此处下载本文的示例代码。

要运行该命令行实用程序,您需要将 $XDK_HOME/lib 中的 xmlparserv2.jar 库包括在 Java CLASSPATH 中,其中 $XDK_HOME 是指您解压缩所下载的 XDK 的主目录。在设置 Java CLASSPATH 后,您可以按以下方式运行命令行实用程序:

>java oracle.xml.parser.v2.oraxl <XML document> <XSL document>

在后续部分中,将用该实用程序转换 XML 文档。

探究 XSLT 2.0 新特性

本页面中的所有 XSLT 示例将转换一个 CD 目录的 XML 文档,如下所示:

<CATALOG>
<CD>
<TITLE>Empire Burlesque</TITLE>
<ARTIST>Bob Dylan</ARTIST>
<COUNTRY>USA</COUNTRY>
<COMPANY>Columbia</COMPANY>
<PRICE>10.90</PRICE>
<YEAR>1985</YEAR>
</CD>
<CD>
<TITLE>Hide your heart</TITLE>
<ARTIST>Bonnie Tylor</ARTIST>
<COUNTRY>UK</COUNTRY>
<COMPANY>CBS Records</COMPANY>
<PRICE>9.90</PRICE>
<YEAR>1988</YEAR>
</CD>
<CD>
<TITLE>Still got the blues</TITLE>
<ARTIST>Gary More</ARTIST>
<COUNTRY>UK</COUNTRY>
<COMPANY>Virgin records</COMPANY>
<PRICE>10.20</PRICE>
<YEAR>1990</YEAR>
</CD>
<CD>
<TITLE>This is US</TITLE>
<ARTIST>Gary Lee</ARTIST>
<COUNTRY>UK</COUNTRY>
<COMPANY>Virgin records</COMPANY>
<PRICE>12.20</PRICE>
<YEAR>1990</YEAR>
</CD>
</CATALOG>

在处理数据库的数据时,这是一种很常用的格式。您将使用各种 Oracle 特性之一(如 XML SQL 实用程序或 SQL-XML),从以下的数据库表中获得该输出:

CREATE TABLE CD_TBL(
TITLE VARCHAR2(100),
ARTIST VARCHAR2(500),
COUNTRY VARCHAR(5),
COMPANY VARCHAR2(50),
PRICE FLOAT,
YEAR DATE);

下列 XSLT 示例将集中演示可以简化这些类型的 XML 文档转换的 XSLT 2.0 特性。但是应该注意,所讨论的这些技术也可以应用于其他类型的 XML 文档。

特性 1:分组

对数据进行分组是在 XSLT 中变换 XML 文档时的一种常用操作,尤其是当您管理由数据库表提供的表格化数据并希望在 Web 上发布这些文档的时候。

例如,假设我们在线发布 CD 信息,您希望将 CD 目录文档变换为根据其国家分组的一系列 CD。如果在根据国家对 CD 分组后,您又希望进一步根据年份对 CD 分组,应该如何处理:

<COUNTRY name="UK">
<YEAR year="1988">
<TITLE>Hide your heart</TITLE>
</YEAR>
<YEAR year="1990">
<TITLE>Still got the blues</TITLE>
<TITLE>This is US</TITLE>
</YEAR>
</COUNTRY>
<COUNTRY name="USA">
<YEAR year="1985">
<TITLE>Empire Burlesque</TITLE>
</YEAR>
</COUNTRY> 

由于 XSLT 1.0 不包含内建的分组支持,因此一种称为 Muenchian 方法(以 Oracle 的 Steve Muench 命名)的灵巧但却复杂并且耗费内存的方法被广泛用作解决该问题的变通方法,如下所示

<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output indent="yes"/>
<xsl:key name="cd-by-country" match="CD" use="COUNTRY" />
<xsl:key name="cd-by-year" match="CD" use="YEAR" />
<xsl:template match="CD">
<xsl:for-each 
select="current()[generate-id()=generate-id(key('cd-by-country',COUNTRY)[1])]">
<xsl:sort select="COUNTRY" />
<COUNTRY name="{COUNTRY}">
<xsl:for-each select="key('cd-by-country', COUNTRY)">
<xsl:for-each 
select="current()[generate-id()=generate-id(key('cd-by-year', YEAR)[1])]">
<xsl:sort select="YEAR" />
<YEAR name="{YEAR}">
	 <xsl:for-each select="key('cd-by-year', YEAR)">
	   <xsl:copy-of select="current()/TITLE"/>
</xsl:for-each>
</YEAR>
</xsl:for-each>
</xsl:for-each>
</COUNTRY>
</xsl:for-each>
</xsl:template>
</xsl:stylesheet>

由于所生成索引的嵌套问题,这种方法非常复杂并且成本很高。但是,如果使用 XSLT 2.0,您就不必担心这个问题。在 XSLT 2.0 中,您可以使用新的 <xsl:for-each-group> 元素对 XML 元素方便地进行分组:

<xsl:stylesheet version="2.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output indent="yes"/>
<xsl:template match="/">
<xsl:for-each-group select="/CATALOG//CD" group-by="COUNTRY">
<xsl:sort select="COUNTRY"/>
<COUNTRY name="{COUNTRY}">
<xsl:for-each-group select="current-group()" group-by="YEAR">
<YEAR year="{YEAR}">
<xsl:copy-of select="current-group()/TITLE"/>
</YEAR>
</xsl:for-each-group>
</COUNTRY>
</xsl:for-each-group>
</xsl:template>
</xsl:stylesheet>

在本示例中,您首先需要在 <xsl:for-each-group> 的选择属性中指定 XPATH 表达式,以选择要进行分组的元素。然后,您需要提供 <xsl:for-each-group> 元素的四个属性之一,以确定如何对所选项目进行分组:

  • group-by: 通过评估所选项目的表达式,并忽略项目在所选序列中出现的顺序,对所选项目进行分组
  • group-adjacent: 只是将具有相同值的相邻项目分在一组
  • group-starting-with: 确定一个模式,该模式将与每组中的第一个节点相匹配
  • group-ending-with: 确定一个模式,该模式将与每组中的最后一个节点相匹配

在本示例中,无论所选择的顺序如何,均对所有选定的 <CD> 元素根据其 <COUNTRY> 子元素中的内容进行分组。

<xsl:for-each-group> 元素中,您可以添加一个 <xsl:sort> 元素,以便对各组进行排序。在本示例中,<xsl:sort select="COUNTRY"> 确保各 CD 组按其国家名称的字母顺序排序。这就是来自 UK 的 CD 组排在来自 USA 的 CD 组之前的原因。

在 XSLT 2.0 中,您可以使用 current-group() 函数引用当前的组节点集。在以下示例中,使用该函数依据 CD 的发行年份对每个国家的 CD 进行分组,如下所示:

<xsl:for-each-group select="current-group()" group-by="YEAR">

新的 XSL 样式表更容易编写和理解。

特性 2:多个输出

极大地简化 XSL 样式表的编写的是 XSLT 2.0 的第二个特性,它支持创建多个 XSLT 输出。该特性允许您在一个 XSL 样式表中定义多个 XSLT 转换输出。

在许多现实世界的应用程序中,您需要从一个 XML 文档创建多个输出。例如,在生成 Java 文档时,您可能希望创建多个 HTML 页面以使用框架。您可能还需要创建用于主输出文档引用的辅助文件。例如,在创建 HTML 报表时,您可能需要为报表创建一些可缩放矢量图形 (SVG)、层叠样式表 (CSS) 或者元数据文件。

由于 XSLT 1.0 没有在一个 XSL 样式表中定义多个输出的能力,所以您必须为每个输出分别进行 XSLT 转换。虽然您可以编写一个 XSL 样式表,依靠 XSLT 参数来显示所要创建的页面,但是这个 XSL 样式表需要多次执行,并且由于需要管理这些参数,因此该样式表可能相当复杂。

为简化这些类型的操作,XSLT 1.0 处理器为解决该问题提供了各种扩展。例如,Oracle XDK 提供了一种 <ora:output> 扩展函数。但是,这些 XSL 样式表中的扩展函数阻止了它们在处理器之间的移植。

在 XSLT 2.0 中,可以使用一种新的 <xsl:result-document> 元素来定义多个输出。例如,在转换 CD 目录时,可以将每个国家发行的 <CD/> 元素创建为单独的结果文档,如下所示。

<xsl:stylesheet version="2.0"      
xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="xml" indent="yes" name="cd-format"/>
<xsl:template match="/">
<xsl:for-each-group select="/CATALOG//CD" group-by="COUNTRY">
<xsl:result-document 
href="../output/CD{position()}_{current-group()/COUNTRY}.xml"
format="cd-format">
<CD_LIST country="{current-group()/COUNTRY}">
<xsl:copy-of select="current-group()"/>
</CD_LIST>
</xsl:result-document>
</xsl:for-each-group>
</xsl:template>
</xsl:stylesheet>

一般来说,在定义多个输出时需要两个步骤:

  • 设置输出格式
  • 指定输出文件名。

由于每个输出可能具有不同的格式,您需要使用 <xsl:output> 在 XSL 样式表的顶部定义特定的输出格式,然后使用这些格式的名称来引用它们。随后,每个 <xsl:result-document> 元素生成输出格式(根据其格式属性命名)。在以下示例中,cd-format 输出格式被定义为具有元素输出缩进的 XML 输出格式:

<xsl:output method="xml" indent="yes" name="cd-format"/>

该格式在后文中由 <xsl:result-document> 用于为来自每个国家的 CD 创建一个 XML 文档。

除了指定输出格式之外,您还可以指定用于定义输出文件名的表达式。在以下示例中,使用当前组的顺序(调用 position() 函数而获得)以及当前组的国家名称(使用 current_group()/COUNTRY 表达式选择)命名输出文件,如下所示:

<xsl:result-document 
href="../output/CD{position()}_{current-group()/COUNTRY}.xml"
format="cd-format">

由于只对 XSLT 样式表进行一次处理,因此其处理效率要高得多。

特性 3:临时树

临时树是在 XSLT 2.0 中推出的另一项新结构。与 XSLT 1.0 将 XSL 转换的中间结果和 XSL 变量表示为字符串不同,XSLT 2.0 将由 <xsl:variable>、<xsl:param> <xsl:with-param> 元素构造的中间结果和 XSL 变量存储为一组称为临时树的文档节点。

使用临时树,您可以使用 XPath 表达式来计算变量或参数的内容,并把 XSL 处理模块化。当使用模板或者从 XSL 变量或模板参数中提取数据时,这种方法提供了很大的灵活性。例如,如下所示,将目录变量设为选择 1988 年以后发行的所有 CD。

<xsl:stylesheet version="2.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output indent="yes"/> 
<xsl:variable name="catalog" select="//CD[number(YEAR)>=1988]"/>
  
<xsl:template match="/">
<Expensive>
<xsl:apply-templates select="$catalog[number(PRICE)>10]"/>
</Expensive>
<Cheap>
<xsl:apply-templates select="$catalog[number(PRICE) < 10]"/>
</Cheap>
</xsl:template>
  
<xsl:template match="*">
<xsl:copy-of select="."/>
</xsl:template>
</xsl:stylesheet>

随后可以对所选节点作进一步的分类,将价格等于或高于 10 美元的 CD 归类为高价 CD,将价格低于 10 美元的 CD 归类为廉价 CD。如果没有临时树特性,则 <xsl:apply-templates select="$catalog[number(PRICE) < 10]"/> 是无效的,并且无法访问您先前在创建 $catalog 变量时所选择的数据。

使用这种新特性,您可以方便地将复杂的转换分成几个模块,并对 XML 文档进行迭代处理。

特性 4:字符映射

当您希望在 XSLT 输出中生成带有保留的或无效的 XML 字符(如 <、> 和 &)的文件时,XSLT 2.0 中的字符映射特性很有用处。当您希望生成包含标记的文档(如 XML 模式、XSL 样式表或 JSP 文件)时,该特性非常有用。

在 XSLT 1.0 中,您必须使用 <xsl:text><xsl:value-of> 元素中的 disable-output-escaping 属性来指定字符换码。但是这种需求可能很复杂并容易出错。

在 XSLT 2.0 中,允许您利用 <xsl:character-map> 元素将映射字符声明为顶级样式表元素来解决这一问题。在 <xsl:character-map> 元素中,您需要指定输出字符与其在 XSL 样式表中的表示符之间的映射。例如,如果您需要从 Unicode 专用区域进行字符表示(该区域的 Unicode 数值在 #xE000#xF8FF 之间),则您需要使用 use-character-maps 属性将字符映射定义关联到每个 <xsl:output>。 例如,下面的 XSL 样式表为生成 JSP 文件定义了字符映射:

<xsl:stylesheet version="2.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output use-character-maps="jsp"/>

<xsl:character-map name="jsp">
<xsl:output-character character="" string="<%"/>
<xsl:output-character character="" string="%>"/>
</xsl:character-map>
<xsl:template match="/">
@ page language="java" 
<HTML>
<BODY> myvariable String; </BODY>
</HTML> 
</xsl:template>
</xsl:stylesheet>

在本示例中,字符映射被设置为将 #xE001 映射到 <%,将 #xE002 映射到 %>。在生成 XSLT 输出时,所有的 #xE001 字符被 <% 替代。同样,所有 #xE002 字符被 %> 替代:

<?xml version = '1.0' encoding = 'UTF-8'?>
<%@ page language="java" %>
<HTML><BODY><% myvariable String; %></BODY></HTML>

由于即使在临时树中复制文本节点或属性时,未换码的字符也保证会保留在串行化输出中,因此在 XSLT 2.0 中的字符映射提供了比 XSLT 1.0 中的 disable-output-escaping 支持更加强健的字符串行化功能。此外,XSL 处理器更有可能利用字符映射来产生一致的结果。

特性 5:数据类型绑定

另一种简化特性是数据类型绑定,当您需要处理不同类型的数据(如日期、持续时间、数字和其他 XML 模式数据类型)时,这种特性特别有用。(目前在 XDK 10g 产品中不支持该特性,但在将来的版本中将会支持它。)

在 XST 1.0 中,数据操作被限制在字符串、数字和布尔值处理方面。在 XSLT 2.0 中,您的样式表可以使用 44 种内建的 XML 模式数据类型以及与这些数据类型关联的构造函数。您还可以使用 AS 属性为所有由 <xsl:variable>、<xsl:param><xsl:with-param> 定义的变量或参数指定 datatypeAS 属性通知 XSLT 处理器检查变量或参数的数据类型,并将它们转换为指定的数据类型。这种早期的运行时检查防止了对错误数据的进一步处理,并防止出现可能难以调试的意外结果。

要使用这些数据类型,您必须将 xmlns:xs="http://www.w3.org/2001/XMLSchema" URL 以及其他命名空间声明包括在 <xsl:stylesheet> 的开始标记中。例如,下面的 XML 文档列出了 CD 定购情况:

<?xml version = '1.0' encoding = 'windows-1252'?>
<Order>
<Item name="Empire Burlesque" number="18"/>
<Item name="Hide your heart" number="7"/>
<Item name="Still got the blues" number="10"/>
<Item name="This is US" number="5"/>
</Order>

在这里,XSL 样式表读入 CD 目录数据,将每张 CD 的价格乘以相应的定购数量,计算出总的定购价格:

<?xml version = '1.0' encoding = 'windows-1252'?>
<xsl:stylesheet version="2.0" 
xmlns:xsi="http://www.w3.org/2001/XMLSchema"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
   
<!-- Root template -->
<xsl:variable name="CDs">
<xsl:apply-templates select="/" mode="phase1"/>
</xsl:variable>

<xsl:template match="/">
<xsl:apply-templates select="$CDs" mode="phase1"/>
<xsl:value-of select="sum($CDs//item)"/>
</xsl:template>
   
<xsl:template match="/" mode="phase1">
<xsl:apply-templates mode="phase1"/>
</xsl:template>
  
<xsl:template match="CD" mode="phase1">
<xsl:variable name="title" select="TITLE"/>
<xsl:variable name="num" select="document('CDOrder.xml')/Order/Item[@name=$title]/@number" as="xs:integer"/>  
<xsl:variable name="price" select="PRICE/text() "/>  
<xsl:element name="item">
<xsl:value-of select="$price*$num"/>
</xsl:element>
</xsl:template>
</xsl:stylesheet>

在将定购数量从 CDOrder.xml 读入到 $num 变量时,使用 as="xs:integer" 将数据转换为 xs:integer 数据类型。这种方法确保数据是有效的数字。例如,如果如下所示将 CDOrder.xml 更新为定购 10.5 张 "Still got the blues" CD,则在创建变量时会立即发出一个数据类型转换错误。

<?xml version = '1.0' encoding = 'windows-1252'?>
<Order>
<Item name="Empire Burlesque" number="18"/>
<Item name="Hide your heart" number="7"/>
<Item name="Still got the blues" number="10.5"/>
<Item name="This is US" number="5"/>
</Order>

这仅仅是一个演示数据类型绑定如何极大地丰富 XSL 样式表中的数据操作的简单示例。

总结

如果您要编写 XSL 样式表,则本文中讨论的特性不仅为您节省时间,而且会提高您的 XSLT 转换的性能。


Jinyu Wang ( Jinyu.Wang@oracle.com) 是 Oracle XML 产品管理的高级产品经理,并且是一位 Oracle 认证专家。她与其他人合作编写了 Oracle Database 10g XML & SQL(Oracle 出版社)。

请为本文评定等级:

优秀 良好 一般 低于一般水平 较差


把您的意见发送给我们

寄送此页面
Printer View 打印机视图