- 网络安全
【免费下载链接】sliver
Adversary Emulation Framework
导读
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
相关推荐
GJSON路径语法完全指南:快速掌握JSON查询的终极技巧
GJSON路径语法完全指南:快速掌握JSON查询的终极技巧 GJSON是一款专为Go语言设计的高性能JSON解析工具,它提供了简单直观的路径语法,让开发者能够快
序列化深入解析GJSON路径语法:高效查询JSON数据的利器
深入解析GJSON路径语法:高效查询JSON数据的利器 GJSON是一个强大的Go语言JSON解析库,其核心特性之一就是提供了一套简洁高效的路径查询语法。本文将
序列化Karmada 项目中的 GJSON 实战指南:Go 语言快速提取与解析 JSON 的路径语法、修饰器与零拷贝技巧
Karmada 项目中的 GJSON 实战指南:Go 语言快速提取与解析 JSON 的路径语法、修饰器与零拷贝技巧 导读 GJSON( github.com/t
云原生多集群集群管理微服务
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考