IBM®
跳转到主要内容
    中国 [选择]    使用条款
 
 
Select a scope: Search for:    
    首页    产品    服务与解决方案     支持与下载    个性化服务    
跳转到主要内容

developerWorks 中国  >  XML  >

递归,而非拆分,以便得胜

为什么不在 XSLT 模板之间拆分 HTML 元素,替代方法是什么

developerWorks
文档选项

未显示需要 JavaScript 的文档选项


级别: 初级

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. 将产品清单按表显示

NameDescriptionPrice
WizzBang Ultra Word ProcessorMore words per minute than the competition.$799.99
Super WizzBang CalculatorCheap and reliable with power saving.$5.99
WizzBang Safest SafeChoose 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:descriptionps:priceps: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 开发人员)不在于递归。



参考资料



关于作者

Author photo

Benoît Marchal 是住在比利时那慕尔的顾问和作家。 他是 XML by Example Applied XML Solutions XML and the Enterprise 的作者。同时是 Gamelan 的专栏作家。 www.marchal.com 上有其最新项目的详细信息。




对本文的评价










回页首


IBM 公司保留在 developerWorks 网站上发表的内容的著作权。未经IBM公司或原始作者的书面明确许可,请勿转载。如果您希望转载,请通过 提交转载请求表单 联系我们的编辑团队。
    关于 IBM 隐私条约 联系 IBM 使用条款