5月28日 03:47

XSLT 是什么?XML 转换的模板匹配机制详解

XSLT 经常被误解为"XML 的 CSS"——其实它更像一门函数式编程语言。你写一系列模板规则,XSLT 处理器拿着这些规则去匹配 XML 节点,匹配上了就输出对应内容。理解这个模型,比背语法重要得多。

XSLT 处理模型:模板驱动的递归匹配

XSLT 的核心不是"写一个程序去遍历 XML",而是"告诉处理器遇到什么节点就输出什么"。处理器从根节点开始,按模板优先级逐级匹配,遇到 apply-templates 就递归处理子节点。

这个过程有几个关键规则:

  • 匹配优先级:更具体的匹配规则优先。match="bookstore/book"match="*" 优先级高
  • 内置模板:如果你没写匹配某节点的模板,XSLT 有默认行为——继续递归处理子节点,文本节点直接输出内容。这就是为什么你只写了部分模板,其他内容也会"冒出来"
  • 一次匹配:一个节点只会被优先级最高的模板处理一次
xml
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform"> <!-- 匹配根节点,输出 HTML 框架 --> <xsl:template match="/"> <html> <body> <xsl:apply-templates select="bookstore/book"/> </body> </html> </xsl:template> <!-- 匹配每本书,输出一行 --> <xsl:template match="book"> <p><xsl:value-of select="title"/> - <xsl:value-of select="author"/></p> </xsl:template> </xsl:stylesheet>

apply-templatesfor-each 都能遍历节点,但区别很重要:apply-templates 把控制权交给模板匹配机制,天然支持递归和模块化;for-each 是命令式的,所有逻辑都写在一个块里。简单遍历用 for-each 没问题,但一旦逻辑复杂,模板匹配更好维护。

XPath:XSLT 的导航语言

XSLT 离不开 XPath。你在 select 属性里写的表达式就是 XPath,它决定了"从 XML 里取什么数据"。

几个高频用法:

XPath 表达式含义
/bookstore/book从根节点选取所有 book
//book任意层级的 book 节点
book[@category='web']category 属性为 web 的 book
book[position() > 1]第二本书开始(下标从 1 计)
count(//book)book 节点数量

一个容易踩的坑://book 看起来方便,但它在整个文档树中搜索,大数据量下性能很差。能写绝对路径 /bookstore/book 就不要用 //

条件判断和循环

if 和 choose

XSLT 1.0 没有 else,只有 xsl:if。需要多分支判断时用 choose/when/otherwise

xml
<xsl:template match="book"> <div> <xsl:choose> <xsl:when test="price &gt; 30"> <xsl:attribute name="class">expensive</xsl:attribute> </xsl:when> <xsl:when test="price &gt; 20"> <xsl:attribute name="class">moderate</xsl:attribute> </xsl:when> <xsl:otherwise> <xsl:attribute name="class">cheap</xsl:attribute> </xsl:otherwise> </xsl:choose> <xsl:value-of select="title"/> - $<xsl:value-of select="price"/> </div> </xsl:template>

注意 XML 里的比较运算符要用转义:> 写成 &gt;< 写成 &lt;。初学者经常在这卡住。

for-each 和排序

xml
<xsl:for-each select="bookstore/book"> <xsl:sort select="price" order="ascending" data-type="number"/> <p><xsl:value-of select="title"/> - $<xsl:value-of select="price"/></p> </xsl:for-each>

sort 必须紧跟在 for-eachapply-templates 后面,放在其他位置会被忽略——而且不会报错。

变量和参数

变量(不可变)

XSLT 的变量一旦赋值就不能修改,这是函数式编程的特征:

xml
<xsl:variable name="maxPrice" select="100"/> <xsl:variable name="bookCount" select="count(//book)"/>

想实现"累加计数"?不能靠修改变量,得用递归模板或者 sum() 等 XPath 聚合函数。这是从命令式语言转过来的开发者最容易不适应的地方。

参数(模板间传值)

xml
<!-- 定义带参数的命名模板 --> <xsl:template name="formatPrice"> <xsl:param name="price"/> <xsl:param name="currency" select="'$'"/> <xsl:value-of select="concat($currency, format-number($price, '#,##0.00'))"/> </xsl:template> <!-- 调用 --> <xsl:call-template name="formatPrice"> <xsl:with-param name="price" select="price"/> <xsl:with-param name="currency" select="''"/> </xsl:call-template>

模板模式:同一节点不同输出

同一个 XML 节点,你可能在不同位置需要不同的输出形式。mode 属性解决这个需求:

xml
<!-- 简略模式:只显示标题 --> <xsl:template match="book" mode="summary"> <li><xsl:value-of select="title"/></li> </xsl:template> <!-- 详细模式:显示全部信息 --> <xsl:template match="book" mode="detail"> <div class="book-detail"> <h3><xsl:value-of select="title"/></h3> <p>Author: <xsl:value-of select="author"/></p> <p>Price: $<xsl:value-of select="price"/></p> </div> </xsl:template> <!-- 按需调用 --> <ul><xsl:apply-templates select="book" mode="summary"/></ul> <div><xsl:apply-templates select="book" mode="detail"/></div>

key:XSLT 的"索引"

xsl:key 可以实现类似数据库索引的效果,最常用于分组(XSLT 1.0 没有 group-by,得用 Muenchian 分组法):

xml
<!-- 定义按 author 分组的 key --> <xsl:key name="books-by-author" match="book" use="author"/> <!-- 取出每个 author 的第一本书(去重) --> <xsl:for-each select="bookstore/book[count(. | key('books-by-author', author)[1]) = 1]"> <h2><xsl:value-of select="author"/></h2> <ul> <xsl:for-each select="key('books-by-author', author)"> <li><xsl:value-of select="title"/></li> </xsl:for-each> </ul> </xsl:for-each>

Muenchian 分组的写法确实反直觉。如果你可以用 XSLT 2.0+,直接用 xsl:for-each-group 就行,省掉这些弯弯绕绕。

在不同语言中执行 XSLT 转换

Java

java
TransformerFactory factory = TransformerFactory.newInstance(); Transformer transformer = factory.newTransformer( new StreamSource(new File("transform.xsl"))); transformer.transform( new StreamSource(new File("data.xml")), new StreamResult(new File("output.html")));

注意 TransformerFactory.newInstance() 会按特定顺序查找实现,如果 classpath 里有 Saxon 等第三方实现,可能拿到的不是 JDK 内置的 Xalan。生产环境建议显式指定:

java
TransformerFactory factory = TransformerFactory.newInstance( "net.sf.saxon.TransformerFactoryImpl", null);

Python(lxml)

python
from lxml import etree xml_doc = etree.parse("data.xml") xslt_doc = etree.parse("transform.xsl") transform = etree.XSLT(xslt_doc) result = transform(xml_doc)

lxml 的 XSLT 只支持 1.0。需要 2.0/3.0 特性的话,得用 saxonc 库调用 Saxon-HE。

浏览器端

浏览器曾经原生支持 XSLT(XSLTProcessor),但现在已经不推荐在前端做转换了——性能差、调试难、XSLT 1.0 功能有限。现代做法是在构建阶段或服务端完成转换。

XSLT 1.0 vs 2.0 vs 3.0

特性1.02.03.0
分组Muenchian 分组(复杂)for-each-groupfor-each-group
正则不支持xsl:analyze-stringxsl:analyze-string
函数定义不支持xsl:functionxsl:function
多输出不支持xsl:result-documentxsl:result-document
包机制不支持不支持xsl:use-package
try/catch不支持不支持xsl:try

XSLT 1.0 是浏览器唯一支持的版本。服务端处理建议至少用 2.0,分组和函数定义这两个特性就能省掉大量代码。

实战踩坑

字符编码问题:转换输出中文乱码,通常是因为没有指定 xsl:outputencoding 属性,或者输出文件的编码和声明不一致。加上 <xsl:output method="html" encoding="UTF-8"/> 基本能解决。

命名空间冲突:源 XML 带了默认命名空间(如 xmlns="http://example.com"),你写的模板死活匹配不上。XSLT 里命名空间必须显式匹配,不能用空命名空间去匹配有命名空间的节点。解决方法是给命名空间加前缀:xpath-default-amespace="http://example.com"(2.0+),或者在 1.0 里手动声明前缀并使用。

大文件内存溢出:XSLT 处理器默认把整个 XML 加载到内存。几十 MB 的 XML 文件可能直接 OOM。Saxon-EE 的流式处理(streaming)可以解决这个问题,但社区版(HE)不支持。

XSLT 的学习曲线主要卡在思维方式的转换——从命令式的"怎么做"切换到声明式的"要什么"。理解了模板匹配的递归模型,剩下的语法只是工具。

标签:XML