在 XSLT 样式表里,复制 XML 节点是构建转换输出最基础的操作之一。无论是做身份转换、抽取字段重组文档,还是清洗数据,都离不开两个关键元素:<xsl:copy> 和 <xsl:copy-of>。它们名字相近,但复制范围完全不同。简单来说,前者只复制当前节点本身,不包含属性和子节点;后者则将选中节点连同整棵子树完整复制出来。真正动手写样式表时,如果混淆两者,很容易出现属性丢失、子元素消失或命名空间断裂等问题。理解这两个元素的复制边界,是写好 XML 到 XML 转换的第一步。

xsl:copy 的复制边界
<xsl:copy> 的行为非常克制:对元素节点而言,它只输出一个与当前节点同名的元素,不会自动把属性节点和子元素一起带过来。对文本节点、注释节点、处理指令节点,它会直接复制节点值;对属性节点,它会复制属性本身。这意味着如果只用 <xsl:copy> 而不做后续处理,输入一个带有属性和子元素的节点,输出结果会变成一个空元素。
最典型的使用方式就是身份转换模板。下面这段样式表可以为每个节点生成一个同名副本,同时通过 <xsl:apply-templates> 递归处理属性节点和子节点,从而完整保留整棵文档树:
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="xml" indent="yes"/>
<xsl:template match="node()|@*">
<xsl:copy>
<xsl:apply-templates select="node()|@*"/>
</xsl:copy>
</xsl:template>
</xsl:stylesheet>
这个模板中 match="node()|@*" 会匹配所有元素、文本、注释、处理指令以及属性节点。对每个匹配到的节点先执行 <xsl:copy> 创建副本,然后在副本内部用 <xsl:apply-templates> 继续处理它的属性和子节点。属性节点通过 select="@*" 被选中后,会原样复制到当前输出元素上,子节点同样通过 select="node()" 被递归处理。缺少这一步,<xsl:copy> 只会制造空壳。
还要注意命名空间节点。在 XSLT 1.0 中,<xsl:copy> 会复制当前节点的命名空间节点,所以元素名和命名空间声明通常能够保留,但前缀与默认命名空间的具体表现可能因处理器有所不同。到了 XSLT 2.0 和 3.0,命名空间复制行为更加稳定,但核心规则仍然不变:属性与子节点必须显式处理。
xsl:copy-of 的深复制能力
与 <xsl:copy> 不同,<xsl:copy-of> 执行的是深度复制。它通过 select 属性选中一组节点,然后把这组节点以及它们的所有后代节点、属性节点、命名空间节点完整地复制到结果树中,中间不会触发任何模板匹配。因此它特别适合整块搬移,不需要关心节点内部结构是否复杂。
例如从一本书的 XML 中提取某个指定编号的 book 节点,并原样复制到新文档的 result 元素中:
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="xml" indent="yes"/>
<xsl:template match="/">
<result>
<xsl:copy-of select="/books/book[@id='2']"/>
</result>
</xsl:template>
</xsl:stylesheet>
这里的 select="/books/book[@id='2']" 会找到 id 属性等于 2 的 book 元素。复制结果不仅包含这个元素本身,还包括它的 id 属性、内部所有子元素、文本内容,甚至注释和处理指令。整个节点分支被原封不动地移到输出树中。这就是 <xsl:copy-of> 和 <xsl:copy> 最直观的区别:前者复制整棵树,后者只复制节点外壳。
如果你在处理某个元素时,只想把它的所有属性原样保留,可以直接写 <xsl:copy-of select="@*"/>。它会一次性把所有属性节点复制到当前输出元素上,不必像 <xsl:apply-templates select="@*"/> 那样再走一轮模板匹配。对于不想修改的属性集合来说,这种写法更直接、更可靠。
两者对比与模板设计建议
两个元素在复制粒度上的区别可以从下面的表格看得更清楚:
| 对比项 | xsl:copy | xsl:copy-of |
|---|---|---|
| 复制范围 | 当前节点本身 | 选中节点及整棵子树 |
| 子节点 | 不复制,需要显式处理 | 自动全部复制 |
| 属性节点 | 不复制,需要显式处理 | 自动全部复制 |
| 命名空间 | 复制当前节点命名空间 | 复制所有相关命名空间节点 |
| 是否触发模板 | 不调用匹配模板 | 不调用任何模板 |
| 典型场景 | 身份转换、局部修改 | 整块搬移、提取分支 |
选择哪一个主要看你要不要修改节点内容。如果目标是保持文档结构不变,只对少数元素或属性做调整,那么以 <xsl:copy> 为核心的身份模板是最合适的。你可以在此基础上添加覆盖规则,匹配到特定节点时输出新的结构,其余节点仍然走默认复制逻辑。比如想修改某个元素的部分属性,同时保留其他属性和所有子节点,可以这样写:
<xsl:template match="book">
<xsl:copy>
<xsl:copy-of select="@*[not(name()='status')]"/>
<xsl:attribute name="status">new</xsl:attribute>
<xsl:apply-templates select="node()"/>
</xsl:copy>
</xsl:template>
这段模板先复制 book 元素本身,再用 <xsl:copy-of> 把除 status 以外的所有属性复制过来,接着生成一个新的 status 属性,最后用 <xsl:apply-templates> 递归处理子节点。整个过程中既有浅复制,也有深复制,非常灵活。
反过来,如果需求是抽取某个完整分支、复制大量节点且不做任何修改,那 <xsl:copy-of> 是更高效的选择。它不需要为每个节点生成模板匹配,代码更简洁,执行时也减少了递归模板调度的开销。常见误区是:以为 <xsl:copy> 会复制子节点,结果输出里子元素全部消失;或者想基于现有节点做局部定制,却用了 <xsl:copy-of>,导致后续模板完全无法介入。只要记住浅复制与深复制的边界,就能避免这些结构性错误。
总体来说,<xsl:copy> 提供的是复制节点外壳的能力,真正的完整复制需要配合 <xsl:apply-templates> 完成;而 <xsl:copy-of> 则是一步到位的深度复制工具。根据转换目的是保持结构、局部调整,还是直接搬移、整体抽取,选择合适的复制方式,XSLT 样式表会清晰很多,后期维护也会更加容易。
XSLTxsl:copyxsl:copy-of修改时间:2026-09-27 21:16:39