本节摘要:XPath 1.0 没有用户变量。2.0 引入
$name,可由宿主注入,或用let $x := ... return ...在表达式里绑定。自定义函数主要在 XSLT 的xsl:function和 XQuery 的declare function里声明,再在路径中调用。lxml 1.0 路径里既没有 let,也不能 def。本节把原文 3.1 与 3.2 合并,动手时先看引擎,再决定把复用放在 XPath 里还是宿主里。
阅读完本节,你应当能够:
book[number(price)>$min]同一条贵书路径,35 写死会散落各处。2.0 或支持外部变量的 API:
/bookstore/book[number(price)>$min]/title
宿主在求值前绑定 $min = 35。测试时换 40,不必改表达式。这是变量最值得用的场景:阈值、当前日期、当前用户语言。
1.0 的 lxml 也能通过扩展函数或在 Python 里拼字符串把 35 嵌进去。拼字符串要小心引号,数字还好,字符串阈值要转义。我更倾向「数字在宿主比完再选」,或升级引擎。
原文集强调:1.0 重复使用某个值时显得吃力,2.0 变量让路径接近一门小语言。别把接近小语言理解成该把业务逻辑全塞进 XPath。变量适合「同一个节点集要引用两次」和「外部参数」。
2.0:
let $web := /bookstore/book[@category='web'] return count($web)
$web 只在这条 let-return 里可见。嵌套 let 内层可以挡外层同名,但可读性会烂。命名用业务词 $webBooks,不要 $a $b。
| 手段 | 可用环境 | 适合 |
|---|---|---|
| 拼进字符串 | 1.0 | 小阈值,注意注入 |
| 宿主绑定变量 | 部分 1.0 API、2.0 | 参数化查询 |
| let-return | 2.0+ | 重复子路径 |
| xsl:variable | XSLT | 模板级复用 |
| declare variable | XQuery | 查询模块 |
| 自定义函数 | XSLT 2 / XQuery | 多处相同计算 |
自定义函数原文集放在 3.2:签名带类型、函数体是 XPath 表达式。例:把「贵书」收成 local:expensive($n)。声明不在纯路径文件里,而在样式表或 XQuery 模块的序言。XPath 3.0 有内联函数项,那是第 5 章的事。
⚠️ 常见坑:XSLT 里
$变量和当前节点.混用。xsl:for-each改变上下文,外面绑的节点集还在变量里,里面.已经是每一项。该用$web时写了.,结果缩成当前项。
💡 关键直觉:1.0 的复用放在 Python/Java 函数里;2.0 才值得在表达式里 let。不要为了「纯 XPath」把引擎升级成本忽略掉。

有的引擎允许注册 Python 可调用对象当 XPath 函数,例如 lxml 的扩展。这能补 abs、正则。代价是路径不再可移植:换 Java 求值器就挂。团队内部工具可以,对要交给 XSLT 引擎的表达式不要用。
变量类型在 2.0 可写 as xs:decimal。绑错类型求值失败,比静默 NaN 好。1.0 没有这个保护。
不要用变量缓存「整个文档」幻想提速。引擎未必因你 let 了就少走树。性能仍看路径是否 // 泛滥,见第 4 章。
$min 未绑定会怎样?静态错误或运行时未知变量,视引擎。比拼错字符串难静默。这是变量比拼接安全的原因之一。
XSLT/XQuery 的用户函数可以递归,要有终止条件。深度受引擎栈限制。树遍历递归不如用轴。能用 descendant 就不要递归函数走孩子。
XSLT 里的美元变量和当前节点点号最容易混。for-each 改变上下文,外面绑的节点集还在变量里,里面点号已经是每一项。该写美元 web 时写了点号,结果缩成当前项,count 变成 1。谓词里需要「回到 for-each 的当前项」时才请 current 函数,那是 XSLT 补丁,纯 1.0 路径求值没有。把 XSLT 示例里的 current 抄进 lxml,未知函数。
1.0 扩展函数能把 Python 可调用对象挂进路径,补 abs、正则、业务口径。代价是路径绑死宿主。换 Java 求值器立刻挂。团队内部流水线可以,要交给样式表引擎或外部合作方的表达式不要用。可移植性与方便是一根轴上的两端,本章选可移植当默认,扩展当显式例外并写进注释。
变量类型在 2.0 可写 as 小数。绑错类型求值失败,比静默 NaN 好,这是类型存在的工程价值。1.0 没有这个保护,阈值拼进字符串时数字还好,字符串阈值要处理引号。用户输入绝不能直接拼进路径,那是注入。外部变量绑定比拼接安全,未绑定会明确报未知变量,而不是执行到一半变成另一条路径。
let 不是性能开关。引擎未必因为你绑定了节点集就少走树。// 该收还是要收。用变量把同一长路径写一遍再引用,主要赚可读性和少写错,不保证更快。自定义函数可以递归,但能用 descendant 轴就不要递归走孩子,栈深和可读性都差。函数体里再写双斜杠,等于把性能问题藏进了「封装良好」的外表。
1.0 没有 let 时,复用放 Python 函数:入参文档与阈值,出参节点列表。这不是「不够纯」,这是和引擎能力对齐。等项目真的上了 Saxon,再把该收的 let 收进表达式。不要为了纯度提前换栈。
1.0 把 35 拼进字符串:book[number(price)> 加上数字。数字还好,字符串阈值要引号转义,用户输入禁止这么进路径。部分 API 能绑定 $min,测试换 40 不改表达式。2.0 let 把 web 节点集绑一次再 count,少写两遍路径,不保证更快。XSLT 用 xsl:variable,for-each 里点号和美元名不要混。扩展函数绑 Python 不可移植。未绑定变量会明确报错,比拼错字符串安全。递归函数能用轴就别递归。
练习:用宿主变量或拼接实现贵书阈值 35 和 40 各一次,对照留下的书。再故意把 current() 抄进 lxml,确认未知函数。把「1.0 复用放宿主」写成团队默认,Saxon 项目再启用 let。不要为了路径里看起来更纯而提前换栈。自定义函数声明在样式表或查询模块序言,不在 1.0 路径文件里 def。看见 def 式教程,先看它的引擎假设。
没有绑定语法时,阈值放程序变量或后过滤,用户输入禁止拼进路径。能绑定外部名时,测试换阈值不必改表达式。二点零的就地绑定适合同一节点集引用两次,不保证更快,双斜杠该收还是要收。样式表里的美元名和当前节点点号最容易混,循环改变当前项,外面绑的集合还在变量里。当前项函数是样式表补丁,抄进普通求值会未知。扩展函数绑宿主不可移植。自定义函数声明在样式表或查询模块,不在一点零路径里定义。递归能用后代方向就别递归。团队默认:一点零复用放宿主,真上了高版本引擎再把该收的收进表达式。
用宿主把贵书阈值做成三十五和四十各一次,对照留下的书,体会参数化。再故意把样式表里的当前项函数抄进轻量库,确认未知函数。低版本没有就地绑定,复用放程序函数或后过滤,用户输入禁止拼进路径。能绑定外部名时测试换阈值不必改表达式。就地绑定适合同一集合引用两次,不保证更快,铺开全体该收还是要收。样式表循环改变当前项,外面绑的集合还在美元名里,点号已经是每一项。扩展函数绑宿主不可移植。自定义函数声明在样式表或查询模块,不在低版本路径里定义。递归能用后代方向就别递归。团队默认:低版本复用放宿主,真上了高版本再收进表达式。
原文集强调低版本重复使用某个值时吃力,高版本变量让路径接近一门小语言。不要把接近小语言理解成该把业务逻辑全塞进路径。变量适合同一节点集要引用两次,以及外部参数比如阈值和今天。就地绑定只在这条语句里可见,嵌套同名能挡住外层,可读性会烂,命名用业务词。自定义函数签名带类型,函数体是表达式,声明在样式表或查询模块序言。内联函数项是更高版本的事。扩展函数能补缺函数,代价是不可移植。变量不是性能开关,引擎未必因你绑定了就少走树。未绑定会明确报错,比拼错字符串安全。
阈值从三十五改到四十,留下的书应当变少或不变,取决于夹具里有没有夹在中间的价格。把这个对照写成测试,比在路径里写死数字再到处搜替换安全。低版本做这件事靠宿主变量或后过滤,高版本靠外部绑定或就地绑定。用户输入绝不能拼进路径,轻则多跑一截后代,重则读到不该读的配置节点。样式表里美元名和点号混用,是变量课最常见的现场事故:循环已经换人,还用点号去当外面绑的那一堆书,计数变成一。当前项函数是样式表补丁,抄进轻量库未知。扩展函数能补绝对值或正则,路径就绑死宿主,换另一种语言求值立刻挂。自定义函数声明在序言,不在低版本路径文件里定义。递归能用后代方向就不要递归走孩子。变量不是缓存开关,铺开全体该收还是要收。未绑定比拼错字符串更容易被发现,这是绑定比拼接安全的原因。团队默认写在规范里:低版本复用放宿主,高版本再收进表达式,不要为了看起来纯提前换栈。
把阈值、今天、当前语言当成外部参数,而不是写死在路径里,测试才能换值不改表达式。低版本做不到绑定就在宿主里换,不要假装路径里有定义语句。
就地绑定赚的是少写两遍和少写错,不保证更快。看见有人用变量当缓存幻想,把它拉回收双斜杠这一刀,性能账才会回到表达式形状上。
参数化比看起来纯更重要。阈值换了测试还要改路径,说明参数还埋在字面量里,该提出去。低版本提到宿主,高版本提到绑定名。
函数体里再写铺开全体,等于把慢藏进封装。能用后代方向就不要递归走孩子。
动手验收把贵书阈值从三十五改到四十各跑一次,留下的书应当变少或不变,取决于夹具里有没有夹在中间的价格。低版本把阈值放宿主变量或后过滤,高版本再绑进表达式。用户输入禁止拼进路径。样式表循环里点号已经是当前项,外面绑的那一堆书还在美元名里,混用会让计数变成一。
//下一节看类型:没有 Schema 时继续 number();有 Schema 时价格可以真是小数,比较少一层心智负担。