凌晨两点。一个 webhook handler 正在悄悄丢事件,告警把你叫醒。你 curl 上游接口想看最近一小时的载荷,一坨六兆的 JSON 直接糊到终端上。心里想要的过滤器大概是这样:“所有 status == "retry"attempt > 3 的事件,按 tenant 分组,每组计数” — 但精确的 jq 写法离能跑还差一个括号、一个管道,而你完全没耐心反复改 filter、重新 pipe、滚屏、再试一遍。你需要的其实是一块草稿板:JSON 粘一次,过滤器迭代十次,输出实时刷新。

打开 jq Playground →

工具就这么点东西。官方 jq 1.7 编译成 WebAssembly,左边 JSON 面板,右边过滤器面板,十个预设示例随手抄,结果区带语法高亮、每敲一个键就重渲染。所有数据不离开页面。

为什么偏偏是 jq

jq 是一门小型函数式语言,专门用来切片、转换、聚合 JSON。它当了十多年 shell 里处理 JSON 的事实标准,因为它一次性解决了四个问题:流式求值能连读多份拼接的 JSON 文档;管道写起来像 Unix 管道;路径表达式读起来就跟数据本身一样;一小撮高阶组合子(mapselectreduceforeachgroup_by)能从一行式扩展到两百行程序。

Playground 跑的是 jq-web,由上游 jq 1.7 源码经 Emscripten 编译而来。gzip 后约 1 MB,首次访问拉一次,浏览器缓存住,整个会话期间常驻内存。之后每一次过滤器求值都是对 WASM 模块的同步调用:无网络、无服务器、无上传。

补一句关键的:行为和 CLI 完全一致。reduceforeachdeftry/catch.. 递归下降、@base64dsplitspathswalk — 你笔记本上 jq 能跑通的,这里都能跑通。

jq vs JSONPath vs JMESPath

“查询 JSON 文档”最常见的三门语言就是这三个。简单字段访问上有重叠,一旦涉及变换就立刻分道扬镳。

能力jqJSONPathJMESPath
字段与索引访问.user.name.items[0]$.user.name$.items[0]user.nameitems[0]
按谓词过滤`.items[]select(.qty > 3)`$.items[?(@.qty > 3)]
重塑为新对象map({id, total: .qty * .price})不支持items[].{id: id}(投影,不支持算术)
聚合 / reducereduce .[] as $x (0; . + $x.qty)不支持不支持(仅 sumlengthmax_by 等 builtin)
定义函数def total: reduce .[] as $x (0; . + $x);不支持不支持
标准实现jqlang.org/jq(C 写的,官方)多个互不兼容的方言jmespath.org(多语言 SDK)
输出基数值流匹配结果数组单个值

一句话:JSONPath 和 JMESPath 是查询语言 — 把值取出来。jq 是变换语言 — 把值取出来、重塑、聚合,再吐一份新的 JSON 文档。如果你的任务只到”给我匹配的节点”为止,三个都能用。如果你要”给我一份 tenant 到错误数的 CSV”,只有 jq 一遍过。

只查不改的场景,ZeroTool 还有 JSONPath Tester — 有时候你只需要这个。

五个真正派得上用场的过滤器

1. 把分页 API 响应展平成可写 CSV 的流

.results[] | [.id, .tenant, .status, (.created_at | fromdateiso8601)] | @csv

每个输入行变成一行 CSV。在终端里把结果 pipe 到 > events.csv;在 Playground 里直接从结果面板复制。

2. 按 tenant 分组日志并统计错误数

[.events[] | select(.level == "error")]
  | group_by(.tenant)
  | map({tenant: .[0].tenant, errors: length})
  | sort_by(-.errors)

group_by 返回一个二维数组 — 每个内层数组共享一个 key。map 在这些组上做投影,把每组压成一个小的汇总对象。sort_by(-.errors) 降序排。这是 jq 里”按字段做直方图”的标准写法。

3. 遍历嵌套树,到处改写一个字段

walk(if type == "object" and has("password") then .password = "***" else . end)

walk 是后序遍历:先访问子节点再访问父节点,所以改写叶子节点不用担心遍历顺序。适合把载荷脱敏后分享出去,或者在结构任意深的数据里统一单位。

4. 用 foreach 计算运行余额

[foreach .transactions[] as $t (0; . + $t.amount; {date: $t.date, balance: .})]

foreachreduce 的显式状态版本。初始状态 0,更新表达式 . + $t.amount,抽取表达式 {date: $t.date, balance: .}。它对每个输入吐一个输出,所以拿到的是余额时间序列,而非单一总和。

5. 按 key diff 两个数组

def by_id: map({key: .id, value: .}) | from_entries;
(.a | by_id) as $a | (.b | by_id) as $b
| ($a | keys) + ($b | keys) | unique
| map({id: ., a: $a[.], b: $b[.]})
| map(select(.a != .b))

def 定义一个辅助函数,建两份按 key 的查找表,合并 key 集合,对两侧做投影,只保留有差异的行。这种程序在 shell 里写得很痛苦 — 一段多行 jq filter 就内联搞定。

容易踩的坑

jq 吐的是流,不是数组

.items[] 产出 N 个独立的值,每个元素一个。.items 产出一个数组。终端里打印长得像,语义却完全不同:

.items[]          # stream: {...}\n{...}\n{...}
[.items[]]        # array:  [{...}, {...}, {...}]
.items            # array:  same as above, no iteration

如果你的 filter 后面要接 maplength,基本都要用数组。如果你要 pipe 给 xargs 或别的 shell 命令,那就用流。Playground 的结果面板两种形态都能直接看到,差异一眼分明。

-r(raw 输出)在这里没用

CLI 老手习惯反手就是一个 jq -r,用来把字符串结果外面的引号剥掉。Playground 按结构渲染结果:字符串值直接显示成字符串,而非 "value"\n。这里没有 -r flag,因为没有 shell 需要转义。如果你要原始字节(比如把 token 粘到别的输入框),从输出区复制即可,外层引号不会被加上。

null 是一个真值,“键不存在”也是

.user.middle_name 不管 key 缺失还是值本身就是 null,都返回 null。要区分两者,用 has("middle_name").user | keys。备选运算符 // 只在左侧为 nullfalse 时替换默认值:

.user.nickname // .user.name // "anonymous"

这才是”取第一个非 null 字段”的正确写法。if .user.nickname then .user.nickname else ... 在空字符串和 false 上会误判。

大输入:到某个量级请切回 CLI

现代笔记本上,约 50 MB 以内的输入 Playground 处理起来很流畅。再往上,WASM 模块的堆压力会变明显,这时候真 CLI 的流式 flag 更合适:

jq --stream -c 'select(.[0][0] == "events") | .[1]' huge.json

--stream 输出 [path, value] 对序列,不需要把整份文档加载到内存。写起来不太顺手,但能扛多 GB 文件。Playground 不是这个量级该用的工具;本地装 jq,切过去。

数字是 IEEE 754 双精度

jq 跟 JavaScript 一样,所有数字都是 64 位浮点。超过 2^53 的整数 ID 会悄悄丢精度。如果你的 JSON 包含大 ID(Twitter 雪花 ID、某些数据库的行 ID),在线上保持字符串形式 — 一旦被解析成数字,jq 和 Playground 都没法把原值还给你。

它和 CLI 的关系

Playground 是写 filter 的草稿板。CLI 才是把 filter 跑在生产环境的工具。

正确流程:在 Playground 里把 filter 原型调到输出形状满意,再用单引号包起来粘进 shell — jq '<filter>' input.json — 跑真数据。两边的 filter 写法完全兼容,没有任何语法差异。

只有 CLI 合理的场景:

  • 文件比几十兆更大(用 --stream)。
  • 任何在 CI 流水线或 shell 脚本里跑的东西。
  • --slurpfile--argfile 合并两份文件的多输入 filter。
  • 长时间运行的数据管道,需要 jq 的 -c 紧凑输出 pipe 给下游进程。

除此之外 — debug webhook 载荷、起草告警查询、探索一个陌生的 API 响应、教队友 group_by 是什么 — Playground 比切窗口去终端更快。

相关 ZeroTool 工具

  • JSONPath Tester — 只提取不变换的场景。
  • JSON Formatter — 写 filter 之前先把文档美化或最小化。
  • JSONL Converter — 处理换行分隔的 JSON,jq 自身也能通过 --slurp 或迭代流来读。

延伸阅读

  • jq Language Manual — 每个运算符和 builtin 的权威参考。
  • jq on GitHub — Playground 跑的那个引擎的源码、release 和 issue tracker。
  • jq-web — 让浏览器内引擎成为可能的 Emscripten 构建。
  • JSON RFC 8259 — 底层数据模型,包括上文提到的 IEEE-754 注意事项。