☰
Sliver 项目内置 GJSON 路径语法实战指南:快速检索 JSON 载荷的查询语言
2026/9/25 6:06:03 网站建设 项目流程
  • 网络安全

【免费下载链接】sliver

Adversary Emulation Framework

项目地址:https://gitcode.com/gh_mirrors/sl/sliver
点击查看免费下载

导读

GJSON Path 是一种用于从 JSON 载荷中快速检索值的文本字符串语法,它允许你以简洁的表达式在一条路径内完成对象取值、数组遍历、条件过滤、结果重组与格式转换。本指南以 Sliver 项目 vendor 目录中内置的github.com/tidwall/gjson(版本 v1.18.0,见 go.mod)的官方语法文档为核心,结合其完整实现源码 vendor/github.com/tidwall/gjson/gjson.go 讲解每一种路径元素的写法、含义与执行时机。读完本文后,你将能直接用gjson.Get(json, "friends.#(age>45)#.last")这类单行表达式替代多段解析代码,并在自己的 Go 程序中通过自定义修饰器扩展查询能力。

路径结构:一条表达式描述一种搜索模式

GJSON Path 的定位是"用一条文本字符串描述在 JSON 载荷中的搜索模式",它被设计为一系列由.分隔的组件:

name.last friends.1.first

除了.之外,还有几个具有特殊含义的字符:|、#、@、\、*、!、?。它们分别承担数组、查询、修饰器、转义、通配符、字面量等职责,是掌握整套语法的基础。

本文后续所有示例都基于下面这份示例 JSON:

{ "name": {"first": "Tom", "last": "Anderson"}, "age":37, "children": ["Sara","Alex","Jack"], "fav.movie": "Deer Hunter", "friends": [ {"first": "Dale", "last": "Murphy", "age": 44, "nets": ["ig", "fb", "tw"]}, {"first": "Roger", "last": "Craig", "age": 68, "nets": ["fb", "tw"]}, {"first": "Jane", "last": "Murphy", "age": 47, "nets": ["ig", "tw"]} ] }

Basic:按对象名与数组索引取值

大多数场景下你只需要按对象字段名或数组下标取值,路径与结果的对应关系如下:

name.last "Anderson" name.first "Tom" age 37 children ["Sara","Alex","Jack"] children.0 "Sara" children.1 "Alex" friends.1 {"first": "Roger", "last": "Craig", "age": 68} friends.1.first "Roger"

字段名逐级下钻,数组直接用整数下标(从 0 开始)。这是 GJSON 使用频率最高的基础形态。

Wildcards:用*与?做键名模糊匹配

一个键名中可以包含通配符*和?:*匹配任意零个或更多字符,?恰好匹配任意一个字符。

child*.2 "Jack" c?ildren.0 "Sara"

child*.2借助*同时命中children,再取下标 2 得到"Jack";c?ildren.0用?匹配children中的h字符(c+ 任意 1 字符 +ildren)。通配符让路径对键名变化具有容错性,适合批量、未知结构的数据探查。

Escape character:转义特殊字符

当键名本身包含.、*、?等特殊字符时(例如示例 JSON 中的"fav.movie"),需要用反斜杠\转义:

fav\.movie "Deer Hunter"

需要注意的是,在 Go 源码中硬编码路径时,普通字符串字面量里的\本身也要被转义,因此存在两种写法:

// Go val := gjson.Get(json, "fav\\.movie") // must escape the slash val := gjson.Get(json, `fav\.movie`) // no need to escape the slash

第一种用双引号字符串,需要写成\\才能让运行时真正拿到\.;第二种用反引号原始字符串,\原样保留,更不易出错。Rust 版本(gjson::get)同理:普通字符串需写成"fav\\.movie",raw stringr#"fav\.movie"#则无需转义。

Arrays:用#深入数组

#字符专门用于挖掘 JSON 数组。单独使用#即可获得数组长度;#放在字段名之后时,则表示对数组中每个元素依次求值后续路径,并把结果聚合成一个新数组:

friends.# 3 friends.#.age [44,68,47]

friends.#返回元素个数 3,friends.#.age则对三个朋友对象分别取age,得到[44,68,47]。

Queries:用#(...)过滤数组元素

基本比较与通配匹配

可以对数组做条件查询:#(...)返回第一个匹配元素,#(...)#返回全部匹配元素。支持的比较运算符包括==、!=、<、<=、>、>=,以及模式匹配运算符%(like)与!%(not like):

friends.#(last=="Murphy").first "Dale" friends.#(last=="Murphy")#.first ["Dale","Jane"] friends.#(age>45)#.last ["Craig","Murphy"] friends.#(first%"D*").last "Murphy" friends.#(first!%"D*").last "Craig"

要点:#(...)只取首个匹配,#(...)#收集所有匹配;%后面的D*是通配模式,"D*"匹配以D开头的名字,!%则取反。

对非对象元素的查询

当数组元素本身不是对象(而是字符串、数字等标量)时,可以省略运算符右侧的字符串,直接对元素本身做比较或模式匹配:

children.#(!%"*a*") "Alex" children.#(%"*a*")# ["Sara","Jack"]

children.#(!%"*a*")返回第一个不含a的孩子名"Alex";children.#(%"*a*")#返回所有含a的名字["Sara","Jack"]。

嵌套查询

查询内部允许嵌套查询,例如筛选"社交网络列表中包含fb的朋友":

friends.#(nets.#(=="fb"))#.first >> ["Dale","Roger"]

nets.#(=="fb")是数组nets内部的元素查询(==左侧省略,直接比较元素本身),外层再对命中结果取.first。

历史兼容性说明:在 v1.3.0 之前,查询使用的是#[...]方括号语法;v1.3.0 为避免与后文 Multipaths 语法冲突而改为#(...)。出于向后兼容,#[...]在下一个大版本发布之前仍会继续工作。

Tilde 布尔转换:~运算符

~(波浪号)运算符会在比较前把值转换为布尔语义,可用于表达"真值/假值/是否存在"等判定。支持的四种类型如下:

~true Converts true-ish values to true ~false Converts false-ish and non-existent values to true ~null Converts null and non-existent values to true ~* Converts any existing value to true

对下面这份 JSON 做查询:

{ "vals": [ { "a": 1, "b": "data" }, { "a": 2, "b": true }, { "a": 3, "b": false }, { "a": 4, "b": "0" }, { "a": 5, "b": 0 }, { "a": 6, "b": "1" }, { "a": 7, "b": 1 }, { "a": 8, "b": "true" }, { "a": 9, "b": false }, { "a": 10, "b": null }, { "a": 11 } ] }

查询所有真值(true-ish)或假值(false-ish)元素:

vals.#(b==~true)#.a >> [2,6,7,8] vals.#(b==~false)#.a >> [3,4,5,9,10,11]

可以看到true、"1"、1、"true"均被当作真值;false、"0"、0、null以及完全缺失的字段都被当作假值——{"a":11}中不存在b字段,也被~false捕获。

查询 null 与显式存在性:

vals.#(b==~null)#.a >> [10,11] vals.#(b==~*)#.a >> [1,2,3,4,5,6,7,8,9,10] vals.#(b!=~*)#.a >> [11]

~null同时命中b: null与b缺失两种情形;~*只要求字段"存在"(值是什么不重要),因此{a:11}被排除;配合!=反向后,b!=~*恰好选出字段缺失的元素。

Dot vs Pipe:.与|的执行时机差异

.是标准分隔符,但也可以用|代替。绝大多数情况下二者结果相同,真正的差异出现在#(数组/查询)之后:

friends.0.first "Dale" friends|0.first "Dale" friends.0|first "Dale" friends|0|first "Dale" friends|# 3 friends.# 3 friends.#(last="Murphy")# [{"first": "Dale", "last": "Murphy", "age": 44},{"first": "Jane", "last": "Murphy", "age": 47}] friends.#(last="Murphy")#.first ["Dale","Jane"] friends.#(last="Murphy")#|first <non-existent> friends.#(last="Murphy")#.0 [] friends.#(last="Murphy")#|0 {"first": "Dale", "last": "Murphy", "age": 44} friends.#(last="Murphy")#.# [] friends.#(last="Murphy")#|# 2

逐条拆解差异的根源:

  • 路径friends.#(last="Murphy")#本身的结果是数组:

    [{"first": "Dale", "last": "Murphy", "age": 44},{"first": "Jane", "last": "Murphy", "age": 47}]
  • .first后缀会对前一步结果的每个数组元素先执行first再汇总,得到["Dale","Jane"];

  • |first则是对上一步的整体结果(一个数组,而非对象)执行first,数组上没有first字段,因此结果<non-existent>;

  • .0同理,把0当作路径依次作用到每个元素上(元素是对象而非数组,取不到下标),得到空数组[];

  • 而|0直接对数组结果取下标 0,命中第一个元素对象;

  • .#对每个元素求长度失败得[],|#直接求数组长度得2。

一句话总结:.是"映射"语义(对集合中每个元素求值),|是"管道"语义(对整体结果求值)。理解这一点是避免写出意外空结果的关键。

Modifiers:内置修饰器与参数

修饰器(modifier)是一种对 JSON 做自定义处理的路径组件,写法为@名称。例如内置的@reverse可以反转数组:

children.@reverse ["Jack","Alex","Sara"] children.@reverse.0 "Jack"

内置修饰器在 vendor/github.com/tidwall/gjson/gjson.go 中注册,当前共有 13 个:

修饰器作用
@reverse反转数组,或反转对象成员的顺序
@ugly移除 JSON 中所有空白字符
@pretty让 JSON 更易读(美化输出)
@this返回当前元素,可用于取回根元素
@valid校验 JSON 文档是否合法
@flatten展平数组
@join将多个对象合并为单个对象
@keys返回对象的键名数组
@values返回对象的值数组
@tostr把 JSON 转换为字符串(包一层 JSON 字符串)
@fromstr从 JSON 字符串解包(还原内层 JSON)
@group对对象数组做分组
@dig无需给出完整路径即可搜索值

修饰器参数

修饰器可以接受一个可选参数,参数可以是合法 JSON 载荷,也可以是普通字符,语法为@name:参数。以@pretty为例,它接受一个 JSON 对象作为参数:

@pretty:{"sortKeys":true}

该表达式会美化 JSON 并按键名排序所有键,得到:

{ "age":37, "children": ["Sara","Alex","Jack"], "fav.movie": "Deer Hunter", "friends": [ {"age": 44, "first": "Dale", "last": "Murphy"}, {"age": 68, "first": "Roger", "last": "Craig"}, {"age": 47, "first": "Jane", "last": "Murphy"} ], "name": {"first": "Tom", "last": "Anderson"} }

@pretty的完整选项为sortKeys、indent、prefix与width。从源码实现(gjson.go)可见,@pretty底层把参数解析为pretty.DefaultOptions,通过sortKeys、indent等字段控制输出格式——这也解释了为何参数必须以 JSON 对象形式给出。

自定义修饰器:AddModifier

你可以在 Go 中注册自己的修饰器。下面的例子创建了一个把整个 JSON 载荷转为大写或小写的修饰器:

gjson.AddModifier("case", func(json, arg string) string { if arg == "upper" { return strings.ToUpper(json) } if arg == "lower" { return strings.ToLower(json) } return json }) "children.@case:upper" ["SARA","ALEX","JACK"] "children.@case:lower.@reverse" ["jack","alex","sara"]

AddModifier的签名与注册表实现见 gjson.go:第一个参数是修饰器名,第二个参数是func(json, arg string) string回调,接收被处理 JSON 与参数并返回新 JSON。需要留意的是,该操作不是线程安全的,应放在使用其他所有 gjson 函数之前执行(源码注释明确说明这一点,见 gjson.go);ModifierExists可用于检测某修饰器是否已注册。此外,自定义修饰器目前仅在 Go 版本可用,Rust 版本暂不支持。

Multipaths:把多条路径拼接成新文档

自 v1.3.0 起,GJSON 支持将多条路径拼接成新文档:把逗号分隔的路径包在[...]中会生成新数组,包在{...}中会生成新对象。例如:

{name.first,age,"the_murphys":friends.#(last="Murphy")#.first}

这里同时选取了名、年龄,以及所有姓氏为 Murphy 的朋友的名。注意其中的可选键语法:用"the_murphys":前缀强制给某个值指定键名;如果不指定键,则使用实际字段名(此处为first);当无法确定字段名时,使用_作为键名。最终结果为:

{"first":"Tom","age":37,"the_murphys":["Dale","Jane"]}

Multipaths 是从一次解析中聚合出结构化结果的利器,适合一条查询直接产出可用于日志、上报或二次处理的 JSON。

Literals:用!声明静态 JSON 字面量

自 v1.12.0 起,GJSON 支持 JSON 字面量,用于在构造文档时加入静态 JSON 块,在 Multipaths 场景下尤其有用。JSON 字面量以声明字符!开头。例如:

{name.first,age,"company":!"Happysoft","employed":!true}

该表达式选取了名与年龄,然后追加两个新字段company与employed,结果为:

{"first":"Tom","age":37,"company":"Happysoft","employed":true}

注意!之后直接跟 JSON 值(字符串需加引号,布尔、数字直接书写),且字面量也可带自定义键名。

在 Go 中使用路径语法:核心 API 速览

路径语法的实际执行入口都在 vendor/github.com/tidwall/gjson/gjson.go 中,常用 API 包括:

函数说明
gjson.Get(json, path)对字符串执行单条路径查询,返回Result(定义)
gjson.GetBytes(json []byte, path)对字节切片执行查询,避免字符串拷贝(定义)
gjson.GetMany(json, path...)一次解析同时执行多条路径(定义)
gjson.Parse(json)/gjson.ParseBytes(json)预解析 JSON,供后续多次查询复用(定义)
gjson.AddModifier(name, fn)注册自定义修饰器(定义)

查询返回的Result提供了丰富的取值方法:String()、Bool()、Int()、Float()、Time()、Array()、Map()、ForEach()、Exists()(见 gjson.go)。其中Exists()的实现是t.Type != Null || len(t.Raw) != 0(gjson.go),这正是上文~*/~null等查询判断"值是否存在"的底层依据。Value()会把结果转成 Go 原生类型(bool、float64、string、nil、map[string]interface{}、[]interface{},见 gjson.go)。

一个把上述内容串起来的 Go 示例:

import "github.com/tidwall/gjson" json := `{"name":{"first":"Tom"},"friends":[{"first":"Dale","last":"Murphy"},{"first":"Jane","last":"Murphy"}]}` firstName := gjson.Get(json, "name.first").String() // "Tom" murphys := gjson.GetMany(json, "friends.#(last==\"Murphy\")#.first", "friends.#.last") // 两个 Result if gjson.Get(json, "friends.#(age>45)").Exists() { // 存在年龄大于 45 的朋友 }

实践建议与局限

  • 优先用GetBytes/Parse提升吞吐:在需要反复查询同一载荷的循环中,先Parse再取Result.Get比反复gjson.Get更高效;处理[]byte时直接用GetBytes可避免不必要的字符串转换。
  • 区分.与|的语义:对查询结果继续取字段用.(映射到每个元素),取整体结果的属性/下标用|,这是最常见的"结果为空"排查点。
  • 自定义修饰器注意并发安全:AddModifier需在程序早期、并发使用之前完成注册。
  • 版本差异:查询语法#(...)、Multipaths 与 JSON 字面量分别自 v1.3.0、v1.3.0、v1.12.0 起可用;旧式#[...]查询方括号写法仍兼容但即将在下一个大版本移除。当前仓库 vendor 中锁定的是 v1.18.0(go.mod),上述全部能力均可用。

延伸阅读

  • 语法文档原始出处:vendor/github.com/tidwall/gjson/SYNTAX.md
  • 完整实现源码:vendor/github.com/tidwall/gjson/gjson.go
  • 内置修饰器注册表:gjson.go#L2915-L2929
  • 依赖声明:go.mod
  • 网络安全

【免费下载链接】sliver

Adversary Emulation Framework

项目地址:https://gitcode.com/gh_mirrors/sl/sliver
点击查看免费下载

相关推荐

上一篇:5大理由让你立即体验OpenFrontIO:免费的在线实时战略游戏终极指南
下一篇:完整指南:在Raspberry Pi上快速部署Windows ARM系统

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询