XSLT Key、Select 与 Match、冲突解决,等等(转From MS)

来源:互联网 发布:content scripts.js 编辑:程序博客网 时间:2024/05/16 04:44

XSLT Key、Select 与 Match、冲突解决,等等

发布日期: 1/14/2005 | 更新日期: 1/14/2005

Aaron Skonnard

*

问:XSLT 中的 key 如何工作呢?

答:XSLT 中的 key 与文档类型定义 (DTD) 中的 ID 类似。如果您在 DTD 中定义了一个类型为 ID 的属性,则该属性值在文档中必须是唯一的。您也可以定义类型为 IDREF 的属性。这种情况下,其属性值必须引用文档中其他某个位置的唯一 ID 值。有了这种类型的 DTD 信息以后,要在运行时解析文档中的 ID 和 IDREF 的关系,就可以使用 XPath 的 id 函数。例如,我们考虑一个具有内部 DTD 的 XML 文档(请参见图 1)。

以下 XPath 表达式标识一个 ID 为 e104 的元素,在本例中指代名称为 Scott Bloom 的 employee 元素:

id('e104')

以下 XPath 表达式标识 ID 分别为 e101、e102 和 e104 的元素,在本例中,正好是名称分别为 Aaron Skonnard、Dan Sullivan 和 Scott Bloom 的 employee 元素:

id('e101 e103 e104')

最后,以下 XPath 表达式通过教师的 IDREF 属性标识教授“GWS.NET”课程的 employee(具体为 Aaron Skonnard、Simon Horrell 和 Bob Beauchemin):

id(/courses/course[2]/@instructors)

正如您所看到的,这种技术使得根据 ID 值在不同元素之间建立联系成为可能。ID/IDREF 技术的问题是它需要在运行时呈现 DTD 信息,而且只局限于对属性起作用。为了帮助消除这些限制,XSLT 引入了一个新的概念(称为 key),它提供的基本功能一样,但更加灵活。您可以使用 key 元素在 XSLT 文档中定义 key。它具有如下所示的结构:

<xsl:key name="qname" match="pattern" use="expression"/>

需要给 key 一个限定名,以便以后可以通过 key 函数(其行为类似于前面介绍的 XPath id 函数)引用它。您也可以指定一种模式,以便标识 key 要应用的节点。最后,使用“use”属性来指定一个 XPath 表达式,计算该表达式(相对于由 match 表达式标识的节点)可以生成实际的 key 值。例如,请考虑图 2 中的 XML 文档。

这种情况下,您可能想根据 employee 元素的 id 属性创建 key,如下所示:

<xsl:key name="employeeId" match="employee" use="@id"/>

它为文档中的所有 employee 元素创建一个 key;这个 key 值是通过 employee 元素的 id 属性计算得出的。定义了这个 key 之后,您就可以使用 key 函数来检索具有给定 key 的节点。例如,以下 XSLT 片段阐述了如何检索教授给定课程的教师:

<xsl:template match="course">  <h2><xsl:value-of select="name"/></h2>  <h3>Instructors</h3>  <ul>   <xsl:for-each select="key('employeeId', ./instructors/*)">     <li><xsl:value-of select="."/></li>   </xsl:for-each>  </ul></xsl:template>

假定输入文档如图 2 所示,则上面的片段会生成以下 HTML:

<h2>EWS.NET</h2><h3>Instructors</h3><ul>   <li>Aaron Skonnard </li></ul><h2>GWS.NET</h2><h3>Instructors</h3><ul>   <li>Aaron Skonnard</li>   <li>Simon Horrell</li>   <li>Dan Sullivan</li></ul>

正如您所看到的,key 并不需要使用 DTD 信息,而且不像 ID/IDREF 那样只限于对属性起作用。Key 可以指派给匹配任意模式的节点,这也为您带来了更大程度的灵活性。

问:XSLT 中的 match 和 select 属性有什么区别?它们的具体作用是什么?

答:在 XSLT 中,有许多元素具有 match 属性(例如,template 和 key),而其他元素具有 select 属性(value-of、for-each、apply-templates 等)。乍一看,二者似乎都带有 XPath 表达式,但实际上它们差别很大。

Select 确实需要 XPath 表达式,它的作用是选择一个节点集以做进一步处理。在以下示例中,for-each 表达式标识出一组节点,以对其进行循环访问,而嵌套的 value-of 表达式则将每个节点的值写入输出树:

<xsl:for-each select="//item">  <xsl:value-of select="."/></xsl:for-each>

而另一方面,match 属性则采取所谓的模式。模式的形式很像 XPath 表达式,因为它们具有相同的语法,但 XSLT 处理器对它们的处理却是不同的。模式用于匹配树中符合指定条件的节点。例如,以下 template 匹配祖父节点名为 foo 的任何 element 节点:

<xsl:template match="foo/*/*">   •••</xsl:template>

换句话说,模式描述该节点与相关节点“是什么”关系(即该节点是名为 foo 的祖父节点的元素吗?)。下面是一个比较复杂的示例:

<xsl:template match="f:foo[@id > 323]//text()">   •••</xsl:template>

在本例中,模式标识具有由 f 标识的命名空间中的名为 foo 的上级元素、且具有其数值大于 323 的 id 属性的所有文本节点。

问:为什么 SelectNodes 总是按文档顺序返回节点,而无视查询中使用的轴方向(前向/反向)?

答:请考虑下面的 XML 文档,我将它命名为 foo.xml:

<foo depth='1'>  <foo depth='2'>    <foo depth='3'>      <bar/>    </foo>  </foo></foo>

现在请考虑以下代码,它针对加载的 foo.xml 文档计算略微不同的 XPath 表达式:

XmlDocument doc = new XmlDocument();doc.Load("foo.xml");XmlElement b, f1, f2;b =  (XmlElement)doc.SelectSingleNode("//bar");f1 = (XmlElement)b.SelectSingleNode("ancestor::foo[1]");f2 = (XmlElement)b.SelectNodes("ancestor::foo")[0];Console.WriteLine(f1.GetAttribute("depth"));Console.WriteLine(f2.GetAttribute("depth"));

您可能会认为这两种情况下控制台输出一模一样,但实际上却是不同的。第一个 Console.WriteLine 输出“3”,而第二个输出“1”。出现这种情况的原因是,轴方向(由 XPath 规范定义)只在 XPath 谓词下才有意义。事实上,XPath 节点集在定义时是没有顺序的,因此要由 XPath 实现来确定节点集的传递顺序。

然而,在调用 SelectSingleNode 时,我在一个谓词中请求获得上级轴的第一个节点。这就保证我获得的是从 bar 元素(深度为 3 的 foo 元素)开始的第一个上级节点(与文档顺序相反)。另一方面,在调用 SelectNodes 时,我请求获得所有上级 foo 元素和返回 XmlNodeList 集合的实现,且集合中的节点按文档顺序排列(根据规范完全合法)。然后,我的代码请求获得集合中的第一项 — 正好就是深度为 1 的 foo 元素。由于 XPath 没有指定节点的返回顺序,因此构建 XPath 的语言通常自己定义其他语义。例如,在 XSLT 示例中,节点集始终以文档顺序处理。

问:如何查询一段文本以确定它是否包含 <SCRIPT> 元素?

答:这个问题的答案取决于您问段落是否包含 <SCRIPT> 元素的真正含义。请考虑以下文档:

<item>   <name>Something</name>   <desc>This is a description that contains a <SCRIPT      language="Javascript" src="script.js"/> element.</desc></item>

如果文档看起来像是这样的,您就可以使用标准的 XPath 表达式,该表达式针对文档的逻辑数据模型进行查询。例如,以下 XPath 表达式标识文档树中包含 SCRIPT 子元素(在本例中为 <desc> 元素)的所有元素节点:

//*[SCRIPT]

现在,请考虑以下文档,它对 元素中的小于号 (<) 和大于号 (>) 使用特殊的实体引用:

<item>   <name>Something</name>   <desc>This is a description that contains a &lt;SCRIPT       language="Javascript" src="script.js"/&gt; element.</desc></item>

当 XML 处理器分析这段文档时,它将 "&lt;" 替换为 < 字符,将 "&gt;" 替换为 > 字符。因此,当您使用 XPath 对 <desc> 元素进行查询时,该元素包含以下文本:

This is a description that contains a <SCRIPT       language="Javascript" src="script.js"/> element.

因此,如果要在文档中搜索包含“<SCRIPT”的所有元素,可以使用以下 XPath 表达式:

//*[contains(.,'<SCRIPT')]

问:当有多个 XSLT 模式匹配给定节点时,处理器如何确定使用哪一个呢?

答:当一个节点匹配在 XSLT 模板中建立的多个模式(也称为规则)时,处理器就会按照 XSLT 规范中描述的冲突解决指导原则来确定使用哪一个模式。这些指导原则表明,当发生冲突时,会调用优先级最高的模板。然而,确定模板实际优先级的算法还需要附带解释一下。

要确定哪个模板具有最高优先级,处理器首先会消除导入的所有模板(使用 xsl:import 元素);自动导入的模板比经过导入转换的模板优先级低。然后处理器确定其余模板的优先级值。

可以通过 priority 属性显式指定模板的优先级。例如,以下模板被赋予优先级 1:

<xsl:template match="/foo/bar" priority="1">   <!-- do something interesting --></xsl:template>

如果每个模板都赋予了优先级,则处理器可以使用这个值来确定哪个模板具有最高优先级。如果没有显式指定优先级,则处理器会为模板计算一个默认值。由处理器指定的默认优先级范围是从 -0.5 到 +0.5。基本上,模式越特殊,其默认优先级就越高。由于范围是从 -0.5 到 +0.5,因此如果显式指定一个模板的优先级为 1,就总会超过默认优先级。

图 3 详细列出了如何为现有的不同类型的模式指定默认优先级。只包含按类型的节点测试的模式(例如 *、节点、注释、文本等)是最一般的,因此它们的默认优先级为 -0.5。只包含命名空间通配符 (ns:*) 的模式比较具体,所以它们的默认优先级为 -0.25。只包含限定名测试或常量处理指令测试(例如 foo、ns:foo、@bar、处理指令 ('foo') 等等)的模式分配的默认优先级为 0。而比这些具体的其他模式所分配的默认优先级为 0.5。这意味着具有多个定位步骤 (Location Step),或具有谓词的任何模式都会自动得到默认优先级 0.5。

一旦所有模板的默认优先级都已算出,处理器就可以选择具有最高优先级的模板。例如,在以下 XSLT 片段中,有两个可能匹配给定 bar 元素的模板(假定 bar 元素的父元素为 foo)。当出现这种情况时,处理器会自动选择第二个模板,因为它的默认优先级较高:

<xsl:template match='bar' >  <!-- default priority = 0 --></xsl:template><xsl:template match='foo/bar' > <!-- default priority = .5 --></xsl:template>

仍然存在这样的情况:有多个具有相同优先级的模板匹配给定的节点。当出现这种情况时,处理器可能产生出错信号,也可能选择使用文档中的最后一个模板。这是通常的选择方式。例如,以下 XSLT 片段包含两个模板,它们可以匹配相同的 foo 元素(该元素具有一个 bar 子元素和一个 bar 父元素)。由于两个模板的默认优先级都为 0.5,因此 XSLT 处理器要么产生出错信号,要么选择文档中的最后一个模板 — 在本例中为匹配 bar/foo 元素的模板:

<xsl:template match='foo[bar]' >  <!-- default priority = .5 --></xsl:template><xsl:template match='bar/foo'>  <!-- default priority = .5 --></xsl:template>

当然,您最好是避免出现这样的情况。如果您想让处理器在出现冲突时始终使用第一个模板,则只需要将它的优先级设为 1,如下面的代码所示:

<xsl:template match='foo[bar]' priority='1'>  <!-- would have a default priority = .5 --></xsl:template><xsl:template match='foo/bar' >  <!-- default priority = .5 --></xsl:template>

正如您所看到的,当出现冲突时,XSLT 需要经过大量处理才能确定调用哪个模板。当大量使用 XSLT 的声明性编程模型时,理解这些规则是很有必要的。

问:当没有模板匹配待处理的节点时,会出现什么情况呢?

XSLT 定义了几个模板,它们默认生成到每个转换中(不管您是否显式提供)。这些模板被称为 XSLT 的内置模板。可能在 XML 文档中处理的每种类型的节点(元素、属性、处理指令、注释等等)都有一个内置模板。然而,这些内置模板的优先级可能最低,因此当用户提供的模板也匹配相同节点时就从不会使用它们(请参见上一个关于冲突解决的问题)。只有当不存在匹配特定节点的其他模板时才使用它们。

内置模板的功能可以用以下模板定义来表述:

<!-- built-in templates --><xsl:template match="*|/">   <xsl:apply-templates select="node()"/></xsl:template><xsl:template match="text()|@*">   <xsl:value-of select="."/></xsl:template><xsl:template match="processing-instruction()|comment()"/>

对于根节点或元素节点,处理器会自动将模板应用到当前节点的子节点(但不会应用到其属性,因为该调用使用 child::node())。对于文本节点或属性节点,处理器会自动将节点值输出到结果树中。对于处理指令或注释,处理器不做任何处理 — 只是忽略它们。

对于由 XSLT 提供的声明性编程模型,这个默认功能会显得十分有用。它基本上可以使您以递归方式遍历整个树,直到有一个节点匹配所提供的模板之一。当处理器遇到文本或属性节点时,它会将节点值打印到结果树的正确位置。

为了阐述这个过程,我们试着运行以下针对任何 XML 输入文档的转换:

<xsl:transform version="1.0"   xmlns:xsl="http://www.w3.org/1999/XSL/Transform" />

这个转换没有包含任何用户提供的模板,因此会改为使用内置模板。例如,试着针对以下文档运行它:

<foo bar="hi">  <bar>hello</bar>  <bar>     <baz>world</baz>  </bar></foo>

在这种情况下,XSLT 处理器会输出“helloworld”。由于有了内置模板,因此它会递归遍历树并输出文本节点值。用于元素的内置模板调用 xsl:apply-templates 的方式注定从不会遇到属性节点。如果转换输出的文本总是比想要的多,原因可能是处理器递归遍历树并在无意间触及文本节点。为了避免这种情况,应该始终为文本节点重写内置模板。例如,以下代码是上一个转换的新版本,它有一个用于文本节点的新模板:

<xsl:transform version="1.0"   xmlns:xsl="http://www.w3.org/1999/XSL/Transform" >   <!-- override built-in template to do nothing -->   <xsl:template match="text()|@*" /></xsl:transform>

通过这样修改之后,文本节点就从不会输出,除非您通过 xsl:value-of 显式使它输出。试着通过这个转换运行示例文档,您会发现什么都没有输出。另外,如果您希望处理器甚至不遍历树,可以重写用于元素的内置模板,使它什么都不做,如下所示:

<xsl:transform version="1.0"   xmlns:xsl="http://www.w3.org/1999/XSL/Transform" >   <!-- override built-in template to do nothing -->   <xsl:template match="*" /></xsl:transform>

这样做之后,处理器就不会遍历树,自然也就不会触及文本节点。如果您通过这个转换运行示例文档,您仍然会发现它什么都没输出。如果您重写这个模板,意味着您必须对调用 xsl:apply-templates 时要处理哪些节点了如指掌。

问:xsl:include 和 xsl:import 之间有什么区别呢?

答:xsl:include 元素使得将来自一个转换的模板包含到另一个转换成为可能。当您使用 xsl:include 时,它必须是一个顶级元素,也就是说它必须是 xsl:transform 的直接子节点。接下来您只需使用 href 属性来指定您想要包含的转换的位置,如下所示:

<xsl:transform version="1.0"   xmlns:xsl="http://www.w3.org/1999/XSL/Transform" >   <!-- include templates from foo.xsl here -->   <xsl:include href="foo.xsl"/>   ...</xsl:transform>

使用 xsl:include 对所包含的模板的优先级没有任何影响。包含到新转换中的模板被当作以内联方式定义的模板一样看待。这就是 xsl:import 的不同之处。

xsl:import 元素也允许您将模板从一个转换包含到另一个转换。但是当您这样做时,导入的模板会自动获得比包含它们的转换中的模板低的优先级,即,更低的默认优先级(请参见前面关于冲突解决指导原则的问题)。这就是 xsl:include 和 xsl:import 之间的主要区别之一。

与 xsl:include 一样,xsl:import 也必须是一个顶级元素,也需要您使用 href 属性指定要导入的转换的位置:

<xsl:transform version="1.0"   xmlns:xsl="http://www.w3.org/1999/XSL/Transform" >   <!-- import templates from foo.xsl here -->   <!-- imported templates have lower priority -->   <xsl:import href="foo.xsl"/>   ...</xsl:transform>

XSLT 还提供了一个 xsl:apply-imports 元素(类似于 xsl:apply-templates),它允许您只针对导入的模板处理一组节点。这方便于重写模板,如同在面向对象的代码中重写方法一样。例如,请考虑图 4 中的示例,它它重写 employee 模板,增加其他一些详细信息。

在这个示例中,employee.xsl 导入 global.xsl,并重写 employee 模板。在 employee 模板内,它首先调用 xsl:apply-imports 来处理导入的 employee 模板(在 global.xsl 中显示),然后继续处理 employee 元素,输出其 title 元素。运行 employee.xsl 所产生的输出如下所示:

Name:  Bob SmithTitle: President

问:XSLT 提供 if/else 语句吗?

答:XSLT 提供 if 语句 (xsl:if),但没有 else 子句。xsl:if 语句在 test 属性中有一个布尔表达式,如下所示:

<xsl:if test="spouse and count(child) > 0">   Married with children</xsl:if>

如果 test 表达式计算为真,则会对 xsl:if 元素中的内容进行实例化和处理。如果您需要一个 else 子句,就必须使用一个不同的构造 — 称为 xsl:choose,它与 switch 语句相似。xsl:choose 语句允许您指定多个 test 表达式,(从顶部起)第一个计算为真的语句的内容会被实例化和处理:

<xsl:choose>   <xsl:when test="spouse and count(child) = 0">      Just married   </xsl:when>   <xsl:when test="not(spouse) and count(child) > 0">      Just kids   </xsl:when>   <xsl:when test="spouse and count(child) > 0">      Married with children   </xsl:when></xsl:choose>

xsl:choose 还允许您使用 xsl:otherwise 语句,如果没有 test 表达式计算为真,则对该语句的内容进行实例化。这就是要实现传统的 if/else 语句所要使用的,如下所示:

<xsl:choose>   <xsl:when test="spouse and count(child) > 0">      Married with children   </xsl:when>   <xsl:otherwise>      You must have plenty of free time   </xsl:otherwise></xsl:choose>

我已经数不清有多少次我已经开始写 xsl:if 语句后才意识到我还需要一个 else 子句,它强迫我重新使用 xsl:choose。因此每次要使用 xsl:if 时,要问一下自己是否还需要一个 else。

问:XSLT 是否提供一种方式来控制如何序列化转换输出?如果有,它提供了哪些选项呢?

答:是的,XSLT 提供的 xsl:output 元素正是为了达到这个目的。它是一个顶级元素,应该是 xsl:transformation 的直接子节点。xsl:output 元素提供了大量选项来控制如何序列化结果树。以下显示了 xsl:output 的语法:

<xsl:output   method = "xml" | "html" | "text" | qname   version = nmtoken   encoding = string   omit-xml-declaration = "yes" | "no"  standalone = "yes" | "no"  doctype-public = string   doctype-system = string   cdata-section-elements = qnames   indent = "yes" | "no"  media-type = string/> 

当 XSLT 处理器运行转换时,它通过模板中的静态文本或者显式调用 xsl:value-of

当 XSLT 处理器运行转换时,它通过模板中的静态文本或者显式调用 xsl:value-of 来构建一个临时结果树,该树包含写到输出的内容。然后处理器将结果树序列化为输出流。您可以使用 xsl:output 来影响 XSLT 处理器执行这个最后步骤的方式。

您可以使用方法属性来指示您试图输出的是 XML、HTML,或者只是纯文本。XML 输出方法始终输出格式良好的 XML。另一方面,HTML 输出方法允许处理器对输出进行多次调整,以便生成较友好的 HTML 文档。例如,HTML 中的空元素通常没有 end 标记,而 script 和 style 标记不需要有转义符。这些调整使得生成能让大多数浏览器轻松处理的 HTML 成为可能。文本输出方法只是输出结果文档中的所有文本节点,周围没有任何标记。

如果您没有指定输出方法,处理器会自动根据临时结果树选择一个。如果根元素命名为“html”(不区分大小写),则它选择 HTML 输出方法;否则默认选择 XML 输出方法。要更改默认输出方法,需要通过 xsl:output 显式指定要使用哪种输出方法,如下所示:

<xsl:transform version="1.0"   xmlns:xsl="http://www.w3.org/1999/XSL/Transform" >   <xsl:output method="text"/>   ...

您可以指定所使用的输出方法的版本。XML 的默认版本是 1.0,HTML 的默认版本是 4.0(版本控制不适用于文本模式)。例如,以下 xsl:output 元素指定输出应该使用 HTML 3.0:

<xsl:transform version="1.0"   xmlns:xsl="http://www.w3.org/1999/XSL/Transform" >   <xsl:output method="html" version="3.0"/>   

您也可以指定想让处理器在序列化时使用的字符编码形式。这个设置指示处理器应该如何将字符流转换成字节流。虽然 XSLT 处理器通常会遵循这个指示,但它们不一定要这样做。在转换中指出的编码形式可以被处理器或某些 API 重写,这通常会导致混乱。

除了这些设置以外,也可以指定是否在输出中包含一个 XML 声明,以及独立的标识符和 DTD 标识符。您还可以指定应该置于 CDATA 节中的任何元素的名称,以防它们包含很多不安全的 XML 字符。甚至可以告诉处理器使用缩进来整齐打印结果,从而使得输出更易于人们阅读。这里是最后一个示例:

<xsl:transform   xmlns:xsl='http://www.w3.org/1999/XSL/Transform'  version='1.0'>  <xsl:output method="xml"     version="1.0"    omit-xml-declaration="yes"    indents="yes"    encoding="iso-8859-1"    cdata-section-elements="codefrag syntax"/>•••</xsl:transform>

这个示例表明输出应该序列化为 XML 1.0,并且在顶部有一个 XML 声明。它还指示处理器应该使用 iso-8859-1 字符编码形式和缩进,并将所有的代码片段和语法元素放在 CDATA 节中。

请将您给 Aaron的问题和建议发送到 xmlfiles@microsoft.com

Aaron Skonnard 是 DevelopMentor 的一名讲师兼研究员,在那里他制定了 XML 和 Web 服务的相关课程。Aaron 与人合著了 Essential XML Quick Reference (Addison-Wesley, 2001) 和 Essential XML (Addison-Wesley, 2000)。他的联系方式为 http://staff.develop.com/aarons

转到原英文页面