jq 高阶:reduce / 变量 / 自定义函数与引擎降级
当查询从「挑几个字段」升级到「边遍历边聚合、按业务规则改写」时,就需要变量、reduce 与自定义函数。本文承接 jq 进阶, 讲清 as、reduce、def 与错误处理,并说明引擎不可用时如何降级到 JSONPath。
一、示例数据
{
"store": {
"book": [
{ "category": "reference", "title": "Sayings", "author": "Nigel Rees", "price": 8.95 },
{ "category": "fiction", "title": "Sword of Honour", "author": "Evelyn Waugh", "price": 12.99 },
{ "category": "fiction", "title": "Moby Dick", "author": "Herman Melville", "price": 8.99 }
],
"bicycle": { "color": "red", "price": 19.95 }
}
}二、变量绑定:as
表达式 as $名字 | 后续 把当前值绑定到名字,后续管道可以反复引用。绑定后原值仍会传给下一步:
.store.bicycle as $b | {color: $b.color, price: $b.price}→{"color":"red","price":19.95}。.store.book as $books | ($books | map(.price) | add) / ($books | length)→10.31,同一份数据被引用两次。
三、reduce:边遍历边聚合
reduce 序列 as $x (初值; 累加表达式) 逐个消费序列,把结果累积到累加器里。适合内置函数覆盖不到的自定义聚合:
reduce .store.book[] as $b (0; . + $b.price)→30.93,累加所有价格。reduce .store.book[] as $b ({}; .[$b.category] += 1)→{"reference":1, "fiction":2},按分类计数。reduce .store.book[] as $b ([]; . + [$b.title])→ 手动收集标题数组,等价于map(.title)。
简单聚合优先用内置函数:[.store.book[] | .price] | add 与 map(.price) | add 都能求和,比手写 reduce 更短。
四、def:自定义函数
def 名字: 表达式; 在同一表达式内定义可复用的函数,定义后即可像内置函数一样调用,可带参数、可递归:
def total: map(.price) | add; .store.book | total→30.93。def avg: map(.price) | add / length; .store.book | avg→10.31。def cheap($max): map(select(.price < $max)); .store.book | cheap(10)→ 低于 10 的书对象数组(带参数)。
五、错误处理:// 与 try
- 空值兜底
//。.store.car // {color: "unknown"}在左侧为 null / false 时取右侧默认值。 - 捕获错误
try ... catch。try (.store.book | length) catch "取不到"在出错时返回兜底值,避免整条表达式中断。 - 对类型不符用后缀
?。"abc" | .[0]?对非预期类型静默返回空,而不是抛错。
六、引擎降级:WASM 不可用时改用 JSONPath
jq 引擎由官方 jq 1.8 编译为 WebAssembly,首次查询时按需加载(约 0.9MB,仅一次,之后走缓存),因此第一次执行会稍慢,之后即时。 若浏览器不支持 WASM 或资源被拦截,结果区会显示:
「jq 引擎(WebAssembly)加载失败:当前浏览器可能不支持 WASM 或资源加载被拦截,可改用 JSONPath 模式完成查询。」
此时点结果区的 「改用 JSONPath」 按钮即可切到 JSONPath 模式,在不刷新页面的情况下用等价路径表达式继续查询。这是 jq 在线工具内置的兜底路径。
七、实战速查
| 目标 | 表达式 | 结果 |
|---|---|---|
| 变量复用 | .store.bicycle as $b | {color: $b.color, price: $b.price} | {"color":"red","price":19.95} |
| 求平均(变量) | .store.book as $b | ($b | map(.price) | add) / ($b | length) | 10.31 |
| reduce 求和 | reduce .store.book[] as $b (0; . + $b.price) | 30.93 |
| reduce 分类计数 | reduce .store.book[] as $b ({}; .[$b.category] += 1) | {"reference":1, "fiction":2} |
| 自定义无参函数 | def total: map(.price) | add; .store.book | total | 30.93 |
| 自定义带参函数 | def cheap($max): map(select(.price < $max)); .store.book | cheap(10) | 低于 10 元的 book 对象数组 |
八、常见错误
- reduce 三元写错。形式固定为
reduce 序列 as $x (初值; 累加),括号里是分号分隔初值与累加式,用逗号会解析失败。 - 累加器忘了返回。累加式必须是新的累加器值(如
. + $b.price),只写$b.price会每轮覆盖、丢掉历史。 - def 定义漏了结尾分号。自定义函数结束的
;后才是主表达式,漏掉分号会把函数体与主式粘在一起报错。 - 把 jq 当 JSONPath 写。变量绑定是
as $x而非@x;reduce 与 def 都是 jq 独有,JSONPath 不支持。 - 首次查询以为卡死。第一次会下载约 0.9MB 的 WASM 引擎,稍慢属正常;加载失败时用「改用 JSONPath」兜底。
动手跑一遍
把本文表达式粘进 jq 在线工具,对示例数据即时聚合与变形。引擎为官方 jq 1.8(WASM),全部本地执行。
常见问题
什么时候该用 reduce,什么时候用 add?
求和、求极值这类「已知的单一聚合」优先用内置函数(`add`、`min`、`max`、`length`),更短更清晰。reduce 适合内置函数覆盖不到的累加逻辑,比如按分类计数、边遍历边构造新结构:`reduce .store.book[] as $b ({}; .[$b.category] += 1)`。能用 add 就别手写 reduce。
as 变量绑定和管道有什么区别?
管道「把值交给下一步」,值本身不保留名字;`as` 把当前值绑定到一个名字,之后可以在表达式里反复引用它,甚至引回上一个上下文。典型场景是需要「同时用到父值和子值」,例如 `.store.bicycle as $b | {color: $b.color, price: $b.price}`。
def 自定义函数能带参数、能递归吗?
都能。定义形式为 `def 名字: 表达式;` 或带参 `def 名字($x): 表达式;`,同名不同参数个数的定义可共存;函数体内还可调用自身实现递归。定义一次后,同一条表达式里的后续管道都能复用,写复杂 ETL 转换时可显著减少重复。
jq 引擎加载失败怎么办?
本站 jq 引擎是官方 jq 1.8 编译的 WASM,首次使用时按需下载(约 0.9MB,仅一次)。若浏览器不支持 WASM 或资源被拦截,结果区会提示「jq 引擎(WebAssembly)加载失败」,此时点结果区的「改用 JSONPath」即可切到 JSONPath 模式完成等价查询,无需刷新页面。
这些表达式会把我数据发送到服务器吗?
不会。变量绑定、reduce、def 全部在浏览器本地 Web Worker 中由 WASM 引擎执行,站点没有后端,数据不出设备。只有首次加载 WASM 引擎会从 CDN 拉取引擎文件本身。