XSLT 样式表在处理 XML 数据时,经常需要为转换结果中的元素分配标识符,例如为标题生成锚点、为脚注建立链接、给表格行设置唯一键等。XSLT 1.0 提供的 generate-id() 函数就是专门用于这种需求的工具。它不会去读取源文档里已有的 id 属性,而是直接根据节点在源树中的身份返回一个字符串,因此即使输入数据完全没有唯一标识也可以正常使用。

一、generate-id() 函数的基本语法
generate-id() 函数的签名非常简单。它接受一个可选的节点集参数,返回一个字符串。如果不写参数,函数会以当前上下文节点为对象。语法形式如下:
<!-- 对当前节点生成唯一ID -->
<xsl:value-of select="generate-id()"/>
<!-- 对指定节点生成唯一ID -->
<xsl:value-of select="generate-id(//chapter[1])"/>
<!-- 在属性值中动态输出ID -->
<div id="{generate-id()}">...</div>
上面第三行展示了属性值模板的用法。在 XSLT 中,只要属性值包含花括号,处理器就会把花括号内的表达式当作 XPath 求值,并把结果写入属性。因此 id="{generate-id()}" 会在输出元素上生成一个动态且唯一的 id 属性。这也是生成 HTML 锚点最常用的做法。
需要注意的是,generate-id() 的参数必须是一个节点集。如果传入字符串、数字或布尔值,处理器会报错。参数可以来自 select 表达式,也可以来自变量,例如 generate-id($target)。当节点集包含多个节点时,函数只对节点集中的第一个节点生成 ID,因此在实际使用中要确保传入的节点集筛选到了唯一目标。
二、为什么 generate-id() 比 position() 或计数器更可靠
很多 XSLT 初学者会用 position() 函数来生成类似 id 的字符串,例如输出 item-1、item-2 这样的编号。这种方法在简单的 <xsl:for-each> 循环里似乎可行,但一旦引入排序、条件筛选或嵌套循环,position() 的值就会随上下文变化,导致同一节点可能得到不同编号,不同节点也可能出现相同编号。比如在按价格排序后再输出列表,位置序号和原始文档顺序不再对应,引用就会失效。
generate-id() 的稳定性与节点身份绑定,而不是与遍历位置绑定。无论你在循环中如何排序、筛选,只要拿到的是同一个节点,函数返回的字符串就保持一致。这个特性对于构建目录、交叉引用和建立映射关系非常重要。比如你可以在第一次遍历时为每个章节标题生成 ID,并将其写入输出文档,第二次遍历时再次对同一批节点调用函数,得到的字符串仍然相同,从而可以正确生成指向标题的链接。
手动维护计数器变量也是一种备选方案,但它会增加样式表复杂度,并且容易出现作用域和递归重入问题。generate-id() 不需要额外变量,代码更简洁,也更不容易出错。不过要理解它的唯一性范围:规范保证同一个 XML 文档内的不同节点一定获得不同 ID,但并不保证来自不同输入文档的节点之间不重复。如果使用 document() 加载了多个文档,需要自行拼接前缀来避免冲突。
三、生成目录与交叉引用的完整示例
下面通过一个图书目录的例子演示 generate-id() 的实际用法。假设输入 XML 包含多个章节,每个章节有标题和段落。我们要输出一个目录列表,每个目录项链接到正文中对应章节的标题锚点。输入文档大致如下:
<book>
<chapter>
<title>XSLT 基础</title>
<para>...</para>
</chapter>
<chapter>
<title>XPath 表达式</title>
<para>...</para>
</chapter>
</book>
样式表可以先遍历所有章节生成目录链接,同时给正文标题元素输出 id。第二次遍历章节内容时,对标题节点再次调用 generate-id(),保证两次得到的 ID 完全一致。完整 XSLT 代码如下:
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="html" indent="yes"/>
<xsl:template match="/book">
<html>
<body>
<h1>目录</h1>
<ul>
<xsl:for-each select="chapter">
<li>
<a href="#{generate-id(title)}">
<xsl:value-of select="title"/>
</a>
</li>
</xsl:for-each>
</ul>
<xsl:for-each select="chapter">
<h2 id="{generate-id(title)}">
<xsl:value-of select="title"/>
</h2>
<xsl:apply-templates select="para"/>
</xsl:for-each>
</body>
</html>
</xsl:template>
<xsl:template match="para">
<p><xsl:value-of select="."/></p>
</xsl:template>
</xsl:stylesheet>
在这个例子中,目录链接的 href 属性使用了属性值模板,值为 #{generate-id(title)},表示先对当前章节的 title 子节点生成唯一 ID,再在前面加上井号形成片段标识符。正文标题的 id 属性也使用同样的表达式,因此两者能够精确匹配。这个方法不要求标题本身有 id 属性,也不要求标题文本唯一,哪怕是两个同名章节也能得到不同的锚点。
如果需要引用的是当前章节节点而不是标题节点,可以使用 generate-id() 不带参数,或写成 generate-id(.)。两者的效果在大多数情况下相同,但显式写 . 能提高可读性。在 XSLT 2.0 和 3.0 中,该函数仍然可用,行为与 1.0 基本一致,不过更推荐使用 xsl:key 配合 generate-id() 来实现高效分组和去重。
四、常见错误与调试技巧
第一个常见错误是在 <xsl:value-of> 中误把 generate-id 当作元素名使用,写成 <xsl:value-of select="generate-id"/> 并不会调用函数,而是把 generate-id 当作一个节点名称来匹配。必须加上括号,写成 <xsl:value-of select="generate-id()"/> 才是函数调用。这个问题在 XPath 中非常普遍,所有函数都必须带括号。
第二个常见错误是试图用生成的 ID 做业务主键存储到数据库。虽然同一文档内稳定唯一,但不同次转换或不同文档之间并不保证一致。XSLT 处理器可能使用内部节点序号、路径哈希或其他算法来生成 ID,格式和值会随实现改变。因此,如果需要持久化唯一标识,应该从源文档中提取或生成规则明确的业务 ID,而不是依赖 generate-id()。
调试时如果发现生成的 ID 不符合预期,可以通过单独的 <xsl:value-of select="generate-id(.)"/> 输出到临时节点,观察是否重复或变化。还要注意不要在属性值模板外嵌套双引号导致 XSLT 属性解析错误。在 <xsl:if> 或 <xsl:when> 中比较 ID 时,建议先把 ID 保存到变量,例如 <xsl:variable name="currentId" select="generate-id()"/>,然后统一引用变量,这样能减少重复计算并让逻辑更清晰。
XSLTgenerate-id唯一ID修改时间:2026-08-24 20:06:24