使用XSLT转换XML

SLT1.0 是W3C标准,主要用于对XML文档的转换,包括将XML转换成HMTL,TEXT或者另外格式的XML文件.XSLT1.0可以与XPATH1.0标准一起使用,XPATH会告诉你要转换的节点而XSLT则提供了一种机制来说明如何实现这种转换。为了将源文档转换成想要的格式, 一个XSLT样式文件往往包含一系列的规则,而要解释这些规则, 需要依赖XSLT处理器,这个处理器实际上就是对XSLT1.0标准的实现,所以大家可以按照这个标准提供自己的实现,处理器可以根据标准元宝的所有特性来来执行规则,最终完成转换的工作。

XSLT的处理机制

XSLT样式表用于定义转换逻辑,这些逻辑会应用于源XML文档的树状形式的节点集上,然后生成树状结构的节点集做为输出结果。

下面的XML文档比较简单,我们就以此做为源文件:



   
        7839
        KING
   

   
        7788
        SCOTT
   

每个XML文档必须有一个代表该文档的根结点, 并且这个根结点的子节点可以包含文档节点(具有唯一性,在上面的例子中就是,大家要清楚根节点跟文档节点的区别),注释节点和指令结点。文档节点可以包含任意数量的文本节点和元素节点,同时,这些子节点仍可以包含其他任意数量的子节点,这些节点以相互嵌套的方式形成了一棵树。

所以,要实现文档转换, 必须要有两个组成部分:

1 源XML文档, 在内存中,它是以树状的节点集形式表现,了解DOM的人应该容易理解。

2 XSLT文档,其中包含一系列的转换规则。

XSLT本身也是XML格式的文档,不同的是XSLT文档支持相应的指令标签,以实现转换的功能, XSLT的文档节点是,该节点下面包含所有的转换规则,每个规则一般都与XPATH关联,XPATH表明,这个规则适用于哪个节点,当XSLT处理器在解释源文档的某个节点的时候,会查找匹配这个节点的规则。当然,这种规则也被称做Template, 在XSLT中是用标签表示,该标签有个match属性来关联XPATH表达式。比如,像下面的这个规则,它应用于源文档的根节点,”/” 是XPATH表达式,说明这个规则适用于根节点。


    

类似,像下面的模板:


就仅作用于源文档中的节点集,一个节点下面还含有子节点,并且这个必须是节点的直接子节点,才能应用这个规则。

为什么要用TEMPATE来表示一个规则呢?因为在应用这个规则的时候,包含在规则里面的文本和元素就像是个模板,每次XSLT处理程序在调用同个规则的时候,输出的结果都是以这个模板为基础的,模板保证了输出的结构是一致。

让我们看下,以下的规则会输出什么:


    
          
    

XSLT处理程序在解释 节点时,会查找匹配的规则,这个模板的match属性值是“ROWSET/ROW[ENAME]”,刚好符合要求, 最后程序会调用这个模板,输出结果。

当匹配的模板被实例化后,接下来还会做三件事情:

1) 模板中的文本和属性直接输出。任何不是以XSL命名空间开头的都被认为是文本,像上面例子里的 和 id 属性,它们会以文本形式直接输出。

2) 以大括号表示的元素,像{XPathExpr}, XSLT会计算它的值并将值以文本形式返回到当前的位置,我们可以理解为是一个表达式点位符。

3) XSLT命名空间下的任何元素,都以文档中的先后顺序被执行。比如执行元素,处理程序会根据select属性中的XPATH表达式获取到值,并以文本节点的形式替换元素。

基本的逻辑可以归纳为,当源文档中的节点与XSLT中的某个规则匹配的时候,规则中的内容就会输出到结果树中。一旦你掌握了这个过程, 那整个XSLT处理模型就容易理解了。 给你一个源树(XML文档的树状表示)和XSLT样式表,XSLT处理程序

依照样式表中的规则说明的那样,一步步的执行这些规则,最终将源文档转化成结果树。

在执行源树节点集的过程中, 会产生结果树的一部分或称做“片段”,当然,到最后这些片段会被整合在一起。当前正在被执行的节点,称做当前节点, XSLT处理器会选择所有适用于当前节点的模板,如果适用的模板有多个,处理器会根据内置的规则(这个后面再细说),从中选取一个最匹配的,因为针对一个节点只能使用一个模板。

执行的时候,XSLT处理器从根节点开始,它搜索匹配根节点的模板,一般来说, 匹配根节点的模板,它的match属性等于"/",找到,处理器实例化该模板,并将内容输出到结果树, 通常都要执行这三个步骤来完成工作。

如果模板还包含需要处理其他节点的XSLT指令, 那么处理器会重复上面的步骤,搜索模板,应用模板,输出结果,这是个循环的过程,直到所有的XSLT指令都被执行完成。执行完成后,转化过程产生的结果树,就是我们想要的目标文档。

单模板实现

理论上说,只要定义一个模板就可以实现转化的过程,接下来,我们会创建这样的一个模板来进行说明,当然,在一个XSLT样式表中定义多个模板,会给我们带来更多好处,这个在后面详细介绍。

像下面的样式表,主要作用是将XML文档转换成HTML格式的文本。










或者另外一种更简洁的表示:





当看到XSL命名空间的申明语句:xmlns:xsl="http://www.w3.org/1999/XSL/Transform" ,你很自然的就会想到,当执行这个样式表的时候,XSLT处理器会访问这个URL地址。然而,这个申明的作用仅仅是做为唯一的字符串来标记XSLT的命名空间, 命名空间的作用是为了区别各自的标签,因为XML是允许自定义标签的,这会导致出现相同的标签名的可能性大大增加,为了避免冲突,突显自定义标签的唯一性,引入了命名空间的概念。XSLT用XSL作用命名空间的别名, , , 这些标签就表明他们是XSLT相关的标签,XSLT处理器会根据XSL前辍来识别它们,如果你移除了XSL前辍,XSLT处理器就不可能识别出它们。

 

考虑一下以下的样式表,它只包含一个模板,这个例子的目的就是将EMP.XML转化成HTML。


















输出结果:

image

 

上面的模板混合了HTML的标签, ,

, , and 元素到结果树,同时里面包含一个class属性,根据当前行是奇数或偶数,它的值在tr0和 tr1之间变化。CSS文件的定义如下:

,和 and ,当XSLT处理器实例化这个模板的时候,根节点就是当前节点,

标签 :

1) 选择源XML树中所有的"ROWSET"节点集。

2) 将选中的节点集做为当前正在处理的节点集。

3) 开始执行这些节点集。

针对节点集中的每个节点,里的内容都被实例化到结果树中,如果这个实例化后的片段,含有其他的XSLT元素,需要解释执行,碰到元素的话,会用XPATH表达式计算后的结果替换当前位置。

生成的HTML:












7839KING
7788SCOTT


在这个例子中, XSLT处理器仅仅执行与根节点匹配的模板, 所有的子节点处理都依靠执行来完成。

如果模式表只用一个模板,那么我们可以用更简洁的方式来实现,像都可以不需要。这种情况下, 文字元素在模板中是做为第一个元素。但是你必须包含XSLT的命名空间,并且要加上xsl:version="1.0"属性。














与前一个相比,写法稍有不同,但它们输出的结果是完全一样的,而且后一种看起来更简洁明了。

理解输入和输出选项

在之前的XSLT转化过程,我们都有谈到节点树这个概念,节点的树状结构其实并不存在,它只是为了便于我们理解的一种逻辑形式,从XSLT处理器的角度来说,它就是一堆连续的符号,是其在内部执行源XML文档和输出转换结果的过程中的逻辑表现形式,实际情况是:

1) 源文档是以可读的文本形式存在。

2) 转化的结果需要以其他可读的方式输出,比如,以文本形式保存到文件中或以流的方式输出到浏览器。

转换的输入信息必须是节点树,这可以通过解析源XML文档或者手动编程的方式构造出一个树(通过DOM 或SAX提供的API)。

所有的XSLT转换都是通过解析源节点树,生成结果节点树,当你的应用中有多个顺序执行的转化过程,那么一个转化过程产生的节点树会做了下个转化过程的输入,直到所有的转化过程都结束为止,所有的节点集做为一个整体以字符流的方式输出。这个过程称做序列化结果树。

根据XSLT 1.0规范的描述, 利用默认的序列化规则,在通用情况下可以使我们的XSLT文件看起来更简洁。XSLT1.0用UTF-8做为转化输出结果默认的编码集 同时还支持以下几种输出格式:

  • 支持缩进的,格式规范的HTML , 它的类型是text/html
  • 无缩进的XML, 没有DOCTYPE属性,它的类型是text/xml

抛开这些默认的选项, 标准的XSLT语法需要以开头,然后包含子元素,通过这个子元素来控制输出序列化过程。

要想明白如何控制序列化,最主要的就是理解output元素中的method属性,这个属性决定XSLT处理器如何将结果集序列化到输出流。XSLT 1.0支持三种不同的输出选项:

  • , 这个是默认项,输出XML格式。
  • , 如果结果树的文档结点的,这个就是默认项。要注意的是,它的输出并不是格式良好的XML。
  • , 这种方式只会将结果树中的文本结点顺序输出。一般应用于,将XML转化成编程相关的源代码,邮件内容或其它的文本输出。

考虑下面文档中的例子:




7839
KING

用下面的XSLT样式表来转化King.xml,将 节点转化成







& Family



 

输出结果:



KING & Family

 

记住,XSLT样式表是格式良好的文档,所以有些特殊字符需要转义,像“&”转义成“&”和“<”转成“<”

还要注意的就是像“&”这样的数值型实体字符,它是数字“38”的Unicode编码形式。如果你习惯这种十六进制的表示,像换行,你可以用 来代替。

指定output为html, 下面的样式表会将转化成简单的,包含图片的HTML页面。








& Family






 

用这个样式表来转化King.xml,输出的结果:



KING & Family




如果指定output为text将转化成文本,那么输出的结果不包含任何标签:




Hello

& Family,
Your id is


结果:

Hello King & Family,
Your id is 7839

注意, 我们转化的样式表中用来处理文本内容,一般来说, 空白字符在样式表中是被忽略的, 所以可以实现标签的缩进,以达到更好的可读性。 但是,文本内容中的空格需要被保留,可以帮助我们实现这个目的,这样,空白符就会出现在输出的文本中。除了空白字符外,还有换行符和TAB(制表符),利用元素,这些符号都会被逐字保留。上面例子中的“ ”代表回车换行符。

下图说明了源文档,源节点树,结果节点树和利用这三部分实现的序列化以及通过指定output,输出不同的格式。

使用XSLT转换XML_第1张图片

除了outpu属性, 还有其它几个属性可以用来控件输出行为。

使用XSLT转换XML_第2张图片

多模板实现灵活性

我们都清楚, 样式表包含一组规则, 当我们用单个模板方式的时候,整个样式表就只有一个规则:“匹配源文件的根节点,执行其中所有的XSLT指令。”这种方式,就像我们在Java编码的时候,将所有的逻都放在一个main()函数里,肯定会有人赞成,有人反对。

public class doit {
public static void main() (String[] args) {
// 所有的代码都放在这里}
}

做为开发人员,刚入门的时候会觉得将所有实现都放在单个方法里比较容易理解,但他们很快就会发现,当逻辑越来越复杂的时候,在单个方法可能有很多可以共用的部分,如果能将它们单独做为一个方法, 可以更好的提供代码的可重用性,多模板也是基本这个考虑。如果采用多模板,我们也可以利用人家已经实现的规则,就好比站在巨人的肩膀上,可以让你节约时间,而且你也可以新建一个自己规则,替换老的。

我们可以发现,XSLT编程与JAVA编程在很多方面的类似性。在JAVA里,每个方法是包含形为的整体而且可以被重写。如果你实现一个类,并将所有的代码逻辑都放在main()函数里,那么要是有人准备扩展你的代码,那就只能重写main()方法,尽管有时候,他们只是需要一个很小的改动。那最有效的方式,就是将一个方法中的逻辑拆分成几个子方法,而且这些子方法应用易于重用,易于重写。

在XSLT中, 模板是形为和重写的基本单元。 就像上面提到的JAVA一样,如果你将样式表中的逻辑分成多个可重用的模板, 那么其他人就有可能继承你的模板,然后调用你写好的模板或者重写模板以实现他们自己的行为逻辑。

根据每个转化任务来拆分模板是最有效果的。你的模板越容易被调用,越容易被人重写,就说明你的拆分越合理。

应用多模板

下面的例子中, 我们会将上面提到的单个模板进行细化,分成多个模板,细化后的每个模板都对应源文档中的一个元素,负责对应元素的转化工作。每个模板都用指令告诉XSLT处理器,如果当前元素还有子元素,需要递归遍历,直到所有的节点都处理完成。




















 

我们可以举其中的某个模板为例:



它的含义是: 对于源文档中的元素, XSLT处理器会应用这个模板来进行转化,该模板会在结果树中构建一个

元素,因为模板包含指令,处理器会继续查找元素的所有子节点,然后用相应节点的模板来转化,直到所有的节点都转化完成,最终,各个节点的转化片段组成大的结果树,做为
元素的子元素。

通常情况,处理器会从源文档的根节点开始,搜索匹配的模板,在我们的样式表中就是match="/"的那个模板,“/”符号就是表示根节点。所有这个根节点就做为当前节点被实例化。根节点模板构造出 和 ,然后调用去处理根节点的所有子节点。 这些子节点包含一个注释节点和一个元素节点,为了构造这些节点的结果树片段,处理器按顺序执行所有的子节点。注释节点会被忽略(这个稍后解释),对于节点,会有相匹配的模板(match等于"ROWSET")来处理。

下面的图说明XSLT处理器的顺序处理的过程。

使用XSLT转换XML_第3张图片

所有的模板都会被实例化,然后输出到结果树中。根节点模板会输出 和元素,"ROWSET"模板输出

,嵌套在元素里面,接下来,执行指令,匹配所有的子节点,节点包含以下四个节点,按顺序排列:

1. 空白节点

2. 节点

3. 空白节点

4. 节点

针对这些节点,处理器会查找匹配的模板,实例化模板,然后通过模板转化节点,输出到结果树中。对于空白节点,默认情况下,系统会直接拷贝空白字符,而match等于"ROW"的模板会构造出两个

元素,然后继续处理其他节点。

转化完成的结果跟单个模板输出的结果是完全一致的,但是,在接下来的几个例子中, 多模板方式会显现出其强大的好处。

理解内置模板

在继续之前, 我们需要解释一下,为什么注释节点会被处理器忽略? 对于”7839“, ”KING“, “7788,  和“SCOTT”这样的空白节点和文本节点处理器是如何进行转化的?

要解答这些问题,不得不提到XSLT中的内置模板,这些内置模板是XSLT处理器的默认组成部分。


     

匹配根节点或任何节点,这个内置模板不输出任何东西,但会告诉处理器去执行源文档当前节点下的所有子节点。

      

匹配文本节点或属性节点,将当前节点的值输出。

匹配指令节点或注释节点,但什么也不做。

为什么需要内置模板,设想一下,如果有人只想匹配源文档中的某个节点,但是默认情况下,XSLT处理器都是从根节点开始匹配,如果没有内置模板,系统会提示模板不存在就会报错,要是每个开发人员都要将这些模板在自己的样式表中都实现,会使样式表看起来不够简洁。

会了更好的理解内置模板,我们会用下面的样式表来转化文档”Emp.xml “,该样式表中不包含任何模板:



得到的结果是:


7839
KING


7788
SCOTT

处理器用内置的模板来匹配源文档中的元素,对于根节点元素,内置模板不会做任何的输出,只会循环遍历它的子元素,当子元素是空白元素的时候或者”7839“, ”KING“, “7788, 和“SCOTT”这样的文本元素,就会用内置的text()来匹配,调用来拷贝当前文本节点的元素值到结果树中。 相应的, 结果树就是源文档中的一堆文本内容,来自任何层次的节点,以文档中显示的顺序输出。尽管这很有意思,但我们不会将这种不包含任何模板的样式表用在实际的项目中。

通配符匹配和空白字符的处理

让我们看下下面列出的几个模板:




    


    

实际上,这两个模板做同样的事情,它们都用来匹配下面的两个不同的子节点, 然后构建一个表格元素

,其中包含相应子节点的转化结果树。但是,我们要是在增加新的节点,叫做,那是不是我们还要建立两个新的,类似的模板呢,XSLT为我们提供了更好的解决方案,通配符。在XPATH表达式中,我们可以用通配符来指定某个结点下面的所有子节点,像这样”ROW/*“。用这种方式,可以不再需要为每个子节点设置一个匹配模板,而只要用一个泛型模板就足够了。




用通用的方式实现的模板,例子如下:

















输出的结果与之前的一致,但是样式表看起来更简洁。

使用XSLT转换XML_第4张图片

等等, 好像跟之前的输出比较起来,还是有不一样的地方。

这段文本并没有像预期的那样缩进,但是在单模板样式表中,输出的结果是排版良好的,所有节点都有缩进。

明白导致这个问题的原因很重要,因为这关系到XSLT如何处理源文档中的空白符,回想一下,Emp.xml文档的缩进是通过空白字符和回车符实现的。如果我们将这些都显现出来的话,应该是下面的样子。

使用XSLT转换XML_第5张图片

当执行匹配元素的模板的时候,XSLT处理器会构建一个

标签,接着循环处理的所有子节点,包含下面几个节点:

1. 文本节点,包含空白字符用来缩进:carriage return, space, space

2. 节点

3. 文本节点,包含空白字符用来缩进:carriage return, space, space

4. 节点

用多模板方式,XSLT处理器顺序执行的所有子节点,查找匹配的模板。当匹配第一个空白节点的时候,因为没有明确的模板,处理器会调用内置模板"text()|@*"来处理这个节点,这个模板会将空白字符直接拷贝到结果树,对于模板来说,空白节点跟文本节点是一样的,同时,回车符也被直接输出到结果树中,就这是导致缩进不一致的问题。

那么单模板方式为什么没有这个问题? 单模板在匹配根节点后, 通过执行指令来选择节点集,这些节点集中不包含空白节点,所以不存在上面提到的困扰。

要解决这个问题, 我们需要告诉XSLT处理器在转化的时候,剔除这些节点,要实现这个功能, 要用到指令,这个指令必须放在样式表的顶部。



与之相反,如果你要保留某个元素的空白节点,需要用,它同样包含有elements属性。默认情况下,XSLT处理器保留所有元素的空白节点。

不同模式下的节点处理

前面例子中,输出的结果只有数据信息,并没有包含表头,

image

为了创建一个通用的方式来生成表头,我们必须遍历所有的元素,然后取出它子节点的名称做为表格的头单元。然而,我们已经有了一个匹配“ROW/*”的模板来处理元素的子节点,现在为了创建表头,也需要处理这些节点,如果不同的模板有相同的MATCH属性,XSLT处理会根据优先规则只采用其中的一个,那么如何来区分这两种不同的应用呢,XSLT为模板提供了另外一个属性MODE:

  • 假设我们指定了MODE属性值为"ColumnHeaders",那么它就与原来没有MODE属性的模板区分开来,它们虽然是处理相同的节点,但逻辑上完全不同。




  • 在用调用模板时,必须指定MODE属性,像








但是这样的写法有问题,生成表头的模板会匹配下面的所有 节点,根据 节点的个数,会生成重复的表头信息,实际上, 我们只要处理一个 节点就够了。

解决这个问题非常简单,只要保证我们的XPATH表达式只选择一个 节点就行,修改

使用XSLT转换XML_第6张图片

重用和定制已有的模板

我们曾经提到过,利用多模板方式,可以重复利用已有的模板规则,甚至可以用新的模板替换老的模板,比如我们要生成一张类似上面的表格,不同的是,对于工资大于2000的行,要对其进行高亮显示,那我们要如何实现呢?

假设上面提到过的一些模板都已经存放在了样式表文件TableBaseWithCSS.xsl中,然后我们重新建了新的样式表EmpOver2000.xsl,这个文件包含新的模板,并且用指定将TableBaseWithCSS.xsl引入到新的样式表中,大家都知道,TableBaseWithCSS.xsl中已经定义了转化表头和行的基本模板。








当用EmpOver2000.xsl这个样式表来转化源文档的时候,XSLT处理器会查找节点下的所有,之前,就只有一个模板匹配这个节点,但在新的样式表中,我们创建了match值为ROW[SAL>2000]"的模板,这意味着对于当前节点集中的节点,如果这个值大于2000,处理器就会发现有两个模板匹配这个节点,我们说过,处理器只会选择一个最合适的模板来进行匹配,在这里ROW[SAL>2000]的范围更具体,所以更适合。

让我们再举几个例子:

  • 格式化奇数行
  • 格式化偶数行
  • 将DEPTNO 等于20的行,输出“Classified”

下面是样式表,包含要用到的所有模板:























Classified

 

position()函数用于取得当前的结点位置,mod 是取余操作符。

经过实验,ROW[DEPTNO=20]模板从来没用被调用过,这就说明,如果模板的优先级相同的话,处理器会永远选择最新的模板,比如当前样式表中的模板优于被引用样式表中的,文件中位置靠后的模板优先前面的。

使用Priorities来解决模板冲突

XSLT处理器在选择合适的模板时,遵守下面的原则:

  • 通配符“*”低于指定某个节点,像ROWSET
  • 某个节点低于其中带有查询条件的节点,像ROWSET[predicate]或者ROWSET/ROW

根据这种具体程序来区别多模板,万一,有多个模板,它们的程度都是一样的,处理器又该如何选择呢?一种方式就是利用priority属性,priority="realnumber"可以是任意的正负值,当处理器无法根据规则选出最恰当的模板时,模板拥有较高priority就会被选中,priority大于0.5会使你自定义的模板比内置的要优先使用。

所以,当我们将priority="2"加到模板ROW[DEPTNO=20]中,比起匹配奇,偶行的模板,这个模板就有更高的优先级,在处理DEPTNO等于20的那行时,模板ROW[DEPTNO=20]会优先被处理器使用。

使用XSLT转换XML_第7张图片

 

定义命名模板

接下来,我们看个格式化数字的例子,下面的样式表有个format-number()函数,它的作用就是将数值转换成指定的格式。













这里,我们还是要引用TableBaseWithCSS.xsl,因为要用到它里面的模板,当前的样式表重写了匹配节点”ROW/SAL“的模板,而且用了另外的方式来处理变化的行,原来的方式是定义两个模板来处理奇行和偶行,现在只需要一个模板就完成这个功能:

这个模板会构造

body { font-family: Verdana; font-size: 8pt }
th { color: black}
.tr0 {background-color: #f9f9f9; color: black}

如果,你需要经常对数值进行格式化,像是对货币格式的转换,最好是再建一个模板,以方便重用。我们可以用name属性替换的match属性。



然后,无论什么时候我们想调用模板,只要执行带name属性的指令。

命名模板从来不会自动被XSLT处理器执行,除非在样式表中明确的调用。当用调用命名模板的时候,命名模板里面的字面元素和XSL指令会被实例化,就像它们就位于调用它的模板的当前位置。

不同的是,不会改变当前正在处理的结点,被调用的模板跟调用它的使用相同的节点,而xsl:apply-templates 会根据select属性的值改变节点位置,理解这一点非常重要。

像其他模板一样,命名模板可以被放在其他的文件里,相当于一个“方法库”文件,从而被其它样式表所引用。

 

常见错误

当你在样式表中混搭使用基于match的模板跟命名模板的时候,会经常不经意出现错误:

  • 你定义个模板匹配”ROWSET/ROW“ 节点,你可能会错误的使用,正确的写法应该是
  • 你想应用某个模板,你这么写,而正解的写法应该是

你可能感兴趣的:(使用XSLT转换XML)