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-templates 和 for-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 > 30"> <xsl:attribute name="class">expensive</xsl:attribute> </xsl:when> <xsl:when test="price > 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 里的比较运算符要用转义:> 写成 >,< 写成 <。初学者经常在这卡住。
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-each 或 apply-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
javaTransformerFactory 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。生产环境建议显式指定:
javaTransformerFactory factory = TransformerFactory.newInstance( "net.sf.saxon.TransformerFactoryImpl", null);
Python(lxml)
pythonfrom 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.0 | 2.0 | 3.0 |
|---|---|---|---|
| 分组 | Muenchian 分组(复杂) | for-each-group | for-each-group |
| 正则 | 不支持 | xsl:analyze-string | xsl:analyze-string |
| 函数定义 | 不支持 | xsl:function | xsl:function |
| 多输出 | 不支持 | xsl:result-document | xsl:result-document |
| 包机制 | 不支持 | 不支持 | xsl:use-package |
| try/catch | 不支持 | 不支持 | xsl:try |
XSLT 1.0 是浏览器唯一支持的版本。服务端处理建议至少用 2.0,分组和函数定义这两个特性就能省掉大量代码。
实战踩坑
字符编码问题:转换输出中文乱码,通常是因为没有指定 xsl:output 的 encoding 属性,或者输出文件的编码和声明不一致。加上 <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 的学习曲线主要卡在思维方式的转换——从命令式的"怎么做"切换到声明式的"要什么"。理解了模板匹配的递归模型,剩下的语法只是工具。