广告位 · 页头(后台-模块管理-新建模块后替换此区域)

XSLT结果文档格式如何控制?

XSLT处理器的工作过程可以分为两个阶段:先根据模板规则把源XML转换为结果树,再将结果树序列化为字节流。很多格式问题都卡在第二阶段。比如结果文档要不要带XML声明、元素之间是否缩进、特殊字符是转义还是原样输出,这些都不是靠修改模板里的元素能解决的,而是要显式配置序列化参数。<xsl:output>和<xsl:result-document>正是完成这项工作的核心元素。理解它们的作用范围和优先级,能避免大量无谓的调试。

XSLT结果文档格式如何控制?

一、用 xsl:output 设定主输出格式

<xsl:output>是XSLT中控制主结果文档序列化行为的第一道开关。它必须作为<xsl:stylesheet>或<xsl:transform>的顶级子元素出现,可以带name属性形成具名输出定义,供后续的<xsl:result-document>通过format属性引用。如果不指定name,则作用于主结果文档。需要明确的是,<xsl:output>只提供序列化参数,不会改变结果树本身的结构,因此即使设置了indent="yes",那些已经作为文本节点存在的空白也不会被删除。

核心属性包括method、encoding、indent、omit-xml-declaration、doctype-system、doctype-public、cdata-section-elements、media-type等。method决定输出类型,常见取值有xml、html、xhtml、text以及自定义QName。encoding指定字节编码;indent控制是否添加缩进空白;omit-xml-declaration控制是否省略XML声明。它们之间存在联动关系,例如当method="html"时,某些XML特有的序列化规则会被放宽,doctype-system也常用于生成HTML5的标准文档类型声明。

下面是一个面向HTML输出的典型配置,它让处理器输出带缩进、使用UTF-8编码的HTML文档,并自动加上HTML5的DOCTYPE声明。

<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet version="3.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
  <xsl:output method="html" encoding="utf-8" indent="yes" doctype-system="about:legacy-compat"/>
  <xsl:template match="/">
    <html>
      <head>
        <title>示例页面</title>
      </head>
      <body>
        <h1>你好,XSLT</h1>
        <p>这是一个结果文档格式控制示例。</p>
      </body>
    </html>
  </xsl:template>
</xsl:stylesheet>

需要注意的是,indent="yes"只会在元素节点之间添加缩进空白,不会改动文本节点内部的空格或换行。如果源文档里已经包含了用于排版的空白文本节点,缩进操作可能会叠加产生多余的空白。因此在使用indent之前,最好先确认结果树中是否存在混合内容。对于纯数据型XML转XML的场景,开启缩进通常很安全;但对于散文、诗歌等依赖空白语义的内容,应当关闭缩进并手工控制空白。

二、用 xsl:result-document 拆分并分别控制格式

XSLT 2.0引入<xsl:result-document>后,一次转换可以生成多个结果文档,而不再局限于单一主输出。它的href属性指定输出位置,format属性指向某个具名<xsl:output>定义。这为多页静态站点生成、按章节拆分文档、批量生成配置文件等场景提供了原生支持。与主输出不同,<xsl:result-document>必须放在模板规则内部,因为它的执行依赖于当前节点上下文。

假设源XML包含多个章节,每个章节需要输出为独立的HTML文件,同时主输出保持一个简单的XML索引。可以在样式表中先定义一个具名输出格式chapter-html,然后在匹配章节的模板中使用<xsl:result-document>。示例如下:

<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet version="3.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
  <xsl:output name="chapter-html" method="html" encoding="utf-8" indent="yes" doctype-system="about:legacy-compat"/>
  <xsl:output method="xml" encoding="utf-8" indent="yes"/>
  <xsl:template match="/">
    <index>
      <xsl:for-each select="//chapter">
        <entry file="chapter-{position()}.html" title="{@title}"/>
      </xsl:for-each>
    </index>
  </xsl:template>
  <xsl:template match="chapter">
    <xsl:result-document href="chapter-{position()}.html" format="chapter-html">
      <html>
        <head>
          <title><xsl:value-of select="@title"/></title>
        </head>
        <body>
          <h1><xsl:value-of select="@title"/></h1>
          <xsl:apply-templates/>
        </body>
      </html>
    </xsl:result-document>
  </xsl:template>
</xsl:stylesheet>

这里出现了两个<xsl:output>:没有name的那个控制主输出,带name="chapter-html"的控制每个章节文件。这种分工让同一份源数据可以同时产出结构完全不同的结果。实际使用时要注意href属性是相对于主输出基URI解析的,不同处理器对相对路径和写入权限的处理有差异。如果输出目录不存在,部分处理器会报错,而另一些可能会静默失败,因此建议在样式表执行前确认目录结构。

对于仍在使用XSLT 1.0的老处理器(如Xalan旧版本),<xsl:result-document>不可用。此时通常需要借助处理器扩展,比如Xalan的redirect扩展,或者把拆分逻辑放到外部脚本中。如果项目有条件升级到支持XSLT 3.0的Saxon或xsltproc,则可以直接享受多文档输出的便利。

三、字符映射与禁用输出转义

序列化阶段除了决定整体的method和encoding,还可以对具体字符进行改写。XSLT 2.0提供的<xsl:character-map>允许定义一个字符替换表,把结果树中文本节点里的某些字符在输出时替换为其他字符串。典型应用包括把制表符替换为固定数量的空格、把特殊Unicode字符映射为命名实体、或者在纯文本输出中插入换行符。字符映射只影响序列化输出,不会改变结果树中的文本节点内容,因此对后续处理是无感的。

下面定义一个字符映射,把制表符(&#x9;)替换为四个空格,把不间断空格(&#xA0;)替换为&nbsp;实体,并在<xsl:output>中通过use-character-maps属性启用它。

<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet version="3.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
  <xsl:character-map name="text-cleanup">
    <xsl:output-character character="	" string="    "/>
    <xsl:output-character character=" " string="&nbsp;"/>
  </xsl:character-map>
  <xsl:output method="html" encoding="utf-8" use-character-maps="text-cleanup"/>
  <xsl:template match="/">
    <html>
      <body>
        <p>这里有一个制表符:	和一段不间断空格: </p>
      </body>
    </html>
  </xsl:template>
</xsl:stylesheet>

字符映射让特殊字符的处理变得可预测,也避免了在模板中到处调用字符串替换函数。不过要注意,映射发生在序列化阶段,处理器必须支持这一特性。如果使用不支持<xsl:character-map>的旧处理器,样式表会直接报错。

另一个历史更久的控制手段是disable-output-escaping。它可以附加在<xsl:value-of>或<xsl:text>上,让文本节点中的特殊字符(如<和&)原样输出而不进行转义。例如在HTML中嵌入一段脚本时,有时会看到这样的写法:<xsl:value-of select="script-content" disable-output-escaping="yes"/>。虽然它确实能解决问题,但破坏了XSLT的抽象模型,处理器也可能在内部实现时忽略该属性,导致结果不可移植。更稳妥的做法是把脚本内容作为文本节点直接放在元素内部,或者通过字符映射来生成需要的字符。

四、常见格式控制问题与调试建议

编码不一致是初学者最容易踩的坑。源XML可能声明为ISO-8859-1,而<xsl:output encoding="UTF-8">要求输出UTF-8。处理器在解析源文档时会根据声明进行解码,再按输出编码重新编码。如果源文档的实际字节和声明不匹配,乱码就不可避免。因此排查格式问题时,先检查源XML的encoding声明是否与实际文件字节一致,再确定输出编码是否满足目标系统的要求。

缩进与空白问题也经常出现。indent="yes"添加的空白只存在于元素之间,不会修改元素内的文本。如果结果树本身就是混合内容,比如段落里既有文本又有行内标签,自动缩进可能破坏内容的自然排版。这种情况下应当关闭indent,并在模板中显式输出换行和空格。另一个技巧是使用<xsl:strip-space>移除源文档中无意义的空白文本节点,以便让自动缩进的效果更干净。

DOCTYPE声明和CDATA区段是格式控制中的两个实用细节。使用doctype-system="about:legacy-compat"可以为HTML5输出生成标准文档类型声明,避免浏览器进入怪异模式。cdata-section-elements属性可以指定哪些元素的内容用CDATA区段包裹,适合在XML中嵌入代码片段。需要注意的是,如果目标格式是HTML,CDATA区段通常会被忽略,因为HTML解析器不识别CDATA标记。

调试序列化问题时,命令行工具的效率远高于反复修改样式表后刷新页面。Saxon的-o选项可以直接把输出写入文件,xsltproc也能通过重定向查看结果。如果怀疑是处理器版本差异导致格式不同,可以分别用Saxon和xsltproc运行同一份样式表,对比输出差异。很多时候,问题并非出在XSLT代码本身,而是处理器的默认序列化参数不同。

广告位 · 文章底部(后台-模块管理替换此区域)