级别: 初级 Benoit Marchal (bmarchal@pineapplesoft.com), 顾问, Pineapplesoft
2001 年 7 月 01 日 软件顾问和作者 Benoit Marchal 回答了 XSLT 学生的一个常见问题:如何在两个 XSLT 模板之间拆分 HTML 元素?诀窍是问正确的问题。本文演示如何将您的思路转至 XSLT 递归方法,如果您有过程语言(Java 及其类似语言)方面的背景知识,那么该方法对您特别有帮助。样本代码演示了使用想要按层次处理的平面 XML 或 XHTML 文件的正确方法(和错误方法)。
我愿意将 XSLT(XSL 变换)当成一种操纵 XML
文档的简单而有效的脚本语言。我曾经在包括出版和应用程序集成等范围广泛的应用程序中使用
XSLT。我已逐渐喜欢上 XSLT,但同时还了解到,它可能使那些正在学习
XSLT
的有经验开发人员不安,因为它具有截然不同的功能/递归特色(相对于如
Java 等过程编程语言)。正如本文所演示的那样,理解 XSLT
工作模型使开发与该语言有效配合的算法成为可能。
常见问题
学习 XSLT 的学生常常问我:如何在两个 XSLT 模板之间拆分 HTML(或
XML)标记。当开发人员试图向 XML
文档中添加一个层次级别时出现该问题。我认为,出于以下两个原因值得详细研究这个问题:
- 这是一个常见问题,并且很多开发人员将从答案受益。
- 更为重要的是,这是个错误的问题。
在本文中,我将建议哪个问题更有意义,并且,我将告诉您该问题的答案。
考虑
清单 1 中的
products.xml。该文档包含一个以 XML
标记的产品列表。有用于名称(
ps:name )、简短描述(
ps:description )和价格(
ps:price )的标记。所有标记都位于
http://www.psol.com/2001/07/dw
名称空间中。请记住,名称空间 URI 只用作标识;它不指向网站。
不幸的是,products.xml 有一个平面的结构。更确切地说,它缺乏将属于给定产品的所有数据分组的标记。可以推断出,新名称表示新产品的开始,但它在标记中并不是显式的。虽然该文档有正确的格式,但是由于缺乏层次信息,以至于很难处理它。然而这样的平面结构相当普遍。平面结构的另一示例是使用
<h1> 标记、而不用任何显式节分组来标记新节开始的 XHTML。
清单 1.
products.xml:XML 格式的平面产品清单
<?xml version="1.0"?>
<ps:products xmlns:ps="http://www.psol.com/2001/07/dw">
<ps:name>WizzBang Ultra Word Processor</ps:name>
<ps:description>More words per minute than the competition.</ps:description>
<ps:price>$799.99</ps:price>
<ps:name>Super WizzBang Calculator</ps:name>
<ps:description>Cheap and reliable with power saving.</ps:description>
<ps:price>$5.99</ps:price>
<ps:name>WizzBang Safest Safe</ps:name>
<ps:description>Choose the authentic WizzBang Safest Safe.</ps:description>
<ps:price>$1,999.00</ps:price>
</ps:products> |
假设您的任务是创建一个 XSLT 样式表,以在如
图
1 所示的 HTML 表中显示产品清单。大多数样式表很容易,但
<tr> 元素 - 新表行的 HTML 代码如何呢?
图 1.
将产品清单按表显示
| Name | Description | Price | | WizzBang Ultra Word Processor | More words per minute than the competition. | $799.99 | | Super WizzBang Calculator | Cheap and reliable with power saving. | $5.99 | | WizzBang Safest Safe | Choose the authentic WizzBang Safest Safe. | $1999.00 |
乍一看,解决方案是在
<ps:name>
的模板中发出开始标记(
<tr> ),当到达
<ps:price>
时发出相应的结束标记(
</tr> )。然而,这种方法不可行,我们将在稍后解释原因。
清单 2 中的以下样式表 table-bad.xsl
演示了为什么它不是格式正确的 XML
文档。处理器报告一条类似这样的错误:“此文件格式不正确:期望 tr
结束元素名称”。事实上,XSLT 样式表本身就是 XML
文档,因此它必须遵守 XML 规则。
清单 2.
table-bad.xsl:在两个模板之间拆分标记不可行
<?xml version="1.0"?>
<xsl:stylesheet version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:ps="http://www.psol.com/2001/07/dw">
<xsl:output method="html"/>
<xsl:template match="ps:products">
<html>
<head><title>Product List</title></head>
<body>
<table>
<tr><td>Name</td><td>Description</td><td>Price</td></tr>
<xsl:apply-templates/>
</table>
</body>
</html>
</xsl:template>
<xsl:template match="ps:name">
<tr>
<td><xsl:apply-templates/></td>
</xsl:template>
<xsl:template match="ps:description">
<td><xsl:apply-templates/></td>
</xsl:template>
<xsl:template match="ps:price">
<td><xsl:apply-templates/></td>
</tr>
</xsl:template>
</xsl:stylesheet> |
 |
XSLT 处理器
本文中的代码是标准的 XSLT,因此您需要一个与标准兼容的 XSLT
处理器,例如 Xalan 或 XT。
注:如果计划使用 Internet Explorer,首先必须升级到 MSXML
3.0(请参阅
参考资料)。
|
|
越来越糟
此时,XSLT 新手通常会问:如何在两个或更多个模板之间拆分 HTML
元素。不幸的是,没有好的解决方案。
table-bad.xsl 违反了基本的 XSLT
规则,唯一正确的解决方案是改变方式,采用另一种策略,我将在下一节
正确的问题中介绍这种策略。
为了便于讨论,让我们考虑一下:如果坚持不正确的方法将发生什么。请花片刻查看
清单 3 中的
table-worse.xsl。该样式表使用一种可怕的拆分方法来在两个模板之间拆分
<tr> 。我再强调一下,
这种技术本质上就是错误的。请将
table-worse.xsl
当作对不正确编码的警告,而不是解决方案。
这种拆分方法使用
<xsl:text> 元素和 CDATA 节的组合来强迫 XSLT 处理器接受拆分:
<xsl:text disable-output-escaping="yes"><![CDATA[<tr>]]></xsl:text> |
请注意
disable-output-escaping
属性;它全盘告诉处理器将其内容盲目地复制到结果文档中。CDATA
部分转义容易出问题的
<
字符,然后处理器进行编译。然而,这种拆分方法肯定会引起麻烦,因为处理器无法确保文档的格式是正确的。您将失去一种重要的防护措施,并且可能以难以跟踪的错误告终。
清单 3.
table-worse.xsl:创建 HTML 格式的表的“错误”方式
<?xml version="1.0"?>
<xsl:stylesheet version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:ps="http://www.psol.com/2001/07/dw">
<xsl:output method="html"/>
<xsl:template match="ps:products">
<html>
<head><title>Product List</title></head>
<body>
<table>
<tr><td>Name</td><td>Description</td><td>Price</td></tr>
<xsl:apply-templates/>
</table>
</body>
</html>
</xsl:template>
<xsl:template match="ps:name">
<xsl:text disable-output-escaping="yes"><![CDATA[<tr>]]></xsl:text>
<td><xsl:apply-templates/></td>
</xsl:template>
<xsl:template match="ps:description">
<td><xsl:apply-templates/></td>
</xsl:template>
<xsl:template match="ps:price">
<td><xsl:apply-templates/></td>
<xsl:text disable-output-escaping="yes"><![CDATA[</tr>]]></xsl:text>
</xsl:template>
</xsl:stylesheet> |
 |
following-sibling 轴线
缺省情况下,XPaths
遵循父/子关系。例如,
ps:products/ps:name 选择
ps:name 元素,后者是
ps:products
的子代。
轴线可以让您选择另一种关系,并且,如其名称所暗示,
following-sibling
选择当前节点下的元素,只要它们是兄弟即可:即,它忽略子孙。
例如,
ps:name/following-sibling::ps:price[1]
选择每个名称之后的第一个
ps:price
元素。换句话说,它选择产品价格。
|
|
正确的问题
存在更好的解决方案,并且所要做的全部只是稍稍改变态度。目前为止,示例采用的是过程性方法:这种算法无非是在整个文档上进行循环。
然而,那不是处理器的工作机制。处理器实现了一种递归算法。缺省情况下,它执行所谓的
树中深度优先遍历。我知道那听起来象一门算法课程的题目,实际上正是这样。
深度优先遍历意味着,处理器递归访问某一给定节点的所有子代,直到处理了整个文档为止。
您所要作的全部是稍微改变这一算法,以便处理器在访问
ps:name 时也访问
ps:description 和
ps:price (
ps:name 后面的元素)。如
清单 4 中的 table-good.xsl
所示,这并不困难。
为效率起见,
ps:products 模板选择
ps:name 。如果不是由于这个小小的优化,我们将遍历每个元素两次。
有趣之处在
ps:name 模板中开始,该模板包含两条
<xsl:apply-templates>
指令。第一条是处理元素内容的常规调用。第二条分支到树的特殊遍历中(有关详细信息,请参阅
following-sibling 轴线侧栏)。它选择
ps:name
后的第一个元素。换句话说,它没有选择深度优先(到达子代),而是遍历相邻元素。它还使用特殊的
"within" 方式(有关详细信息,请参阅
mode 属性侧栏)。
 |
mode 属性
mode
属性让您将多个模板集与每一元素关联,并从中选择一个或另一个来应用。XSLT
处理器只应用那些
mode 属性与相应的
<xsl:apply-templates> 指令中引入的
mode 相匹配的模板。
|
|
请注意,两条
<xsl:apply-templates>
指令都括在现在出了名的
<tr> 元素中 -
在两个模板之间不再有拆分!
在
"within"
方式中还有两个模板。第一个捕获所有节点(
*
XPath),然后,再使用两条
<xsl:apply-templates>
指令来继续对文档进行特殊遍历。
"within"
方式中的最后一个模板是
ps:name ,它停止特殊遍历。其指导思想是,当您在处理另一个
ps:name 的过程中遇到
ps:name
时,就已经到达了当前产品的结尾。这是递归的停止条件。
清单 4.
table-good.xsl:创建 HTML 格式的表的“正确”方式
<?xml version="1.0"?>
<xsl:stylesheet version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:ps="http://www.psol.com/2001/07/dw">
<xsl:output method="html"/>
<xsl:template match="ps:products">
<html>
<head><title>Product List</title></head>
<body>
<table>
<tr><td>Name</td><td>Description</td><td>Price</td></tr>
<xsl:apply-templates select="ps:name"/>
</table>
</body>
</html>
</xsl:template>
<xsl:template match="ps:name">
<tr>
<td><xsl:apply-templates/></td>
<xsl:apply-templates select="following-sibling::*[1]" mode="within"/>
</tr>
</xsl:template>
<xsl:template match="*" mode="within">
<xsl:apply-templates select="."/>
<xsl:apply-templates select="following-sibling::*[1]" mode="within"/>
</xsl:template>
<xsl:template match="ps:name" mode="within"/>
<xsl:template match="ps:description">
<td><xsl:apply-templates/></td>
</xsl:template>
<xsl:template match="ps:price">
<td><xsl:apply-templates/></td>
</xsl:template>
</xsl:stylesheet> |
递归并得胜
当编写 XSLT 样式表时,记住 XSLT
处理器遵循递归算法是很有帮助的。您对递归有许多控制,并且,在很多情况下,发现一种递归算法是值得的
- 即使您的本意(例如,作为一名 Java 开发人员)不在于递归。
参考资料
关于作者
对本文的评价
|