go语言学习之json处理工具gjson

孤单的生命旅程,一个人无奈的行走,不在回头奢望,迷失在黑暗的角落,悟情,沉思,反醒,研讨,一切都是错,只因太痴,太傻,苦苦的爱着,伤了自己,痛了自己,然而,最终何所求!

Posted by yishuifengxiao on 2023-02-21

一 快速安装

GJSON路径是一种文本字符串语法,它描述了从JSON有效载荷中快速检索值的搜索模式。

GJSON路径易于表示为一系列由字符分隔的组件。

除了字符之外,还有一些字符具有特殊含义: ,.|#@\*!?

当json的key里包含特殊字符且需要根据key进行提取时,需要进行转义操作

安装命令如下

1
go get -u github.com/tidwall/gjson

github的地址为 https://github.com/tidwall/gjson

语法介绍可见 https://github.com/tidwall/gjson/blob/master/SYNTAX.md

该语法可以在 GJSON Playground https://gjson.dev/ 上在线运行查看结果

使用的json示例如下:

1
2
3
4
5
6
7
8
9
10
11
{
"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"]}
]
}

二 基本使用

2.1 快速启动

1
2
3
4
5
6
7
8
9
10
package main

import "github.com/tidwall/gjson"

const json = `{"name":{"first":"Janet","last":"Prichard"},"age":47}`

func main() {
value := gjson.Get(json, "name.last")
println(value.String())
}

输出结果为

1
Prichard

在许多情况下,您只需要按对象名称或数组索引检索值。

对于实例json,以下语法对应的结果如下:

1
2
3
4
5
6
7
8
9
10
11
name.last              "Anderson"
name.first "Tom"
age 37
children ["Sara","Alex","Jack"]
children.0 "Sara"
children.1 "Alex"
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"]}]
friends.1 {"first": "Roger", "last": "Craig", "age": 68}
friends.1.first "Roger"
friends.#.first ["Dale","Roger","Jane"]
friends.*.first 无结果

2.2 结果类型

GJSON支持json类型stringnumberboolnull。数组和对象作为其原始json类型返回。

Result类型包含以下内容之一:

1
2
3
4
bool, for JSON booleans
float64, for JSON numbers
string, for JSON string literals
nil, for JSON null

要直接访问值:

1
2
3
4
5
6
result.Type           // can be String, Number, True, False, Null, or JSON
result.Str // holds the string
result.Num // holds the float64 number
result.Raw // holds the raw json
result.Index // index of raw value in original json, zero means index unknown
result.Indexes // indexes of all the elements that match on a path containing the '#' query character.

有多种方便的函数可以处理结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
result.Exists() bool
result.Value() interface{}
result.Int() int64
result.Uint() uint64
result.Float() float64
result.String() string
result.Bool() bool
result.Time() time.Time
result.Array() []gjson.Result
result.Map() map[string]gjson.Result
result.Get(path string) Result
result.ForEach(iterator func(key, value Result) bool)
result.Less(token Result, caseSensitive bool) bool

result.Value()函数返回的类型是interface{},它是以下Go类型之一:

1
2
3
4
5
6
boolean >> bool
number >> float64
string >> string
null >> nil
array >> []interface{}
object >> map[string]interface{}

result.Array()函数返回一个值数组。如果结果表示一个不存在的值,那么将返回一个空数组。如果结果不是JSON数组,则返回值将是包含一个结果的数组。

2.3 64位整数

result.Int()result.Uint()调用能够读取所有64位,允许使用大型JSON整数。

1
2
result.Int() int64    // -9223372036854775808 to 9223372036854775807
result.Uint() uint64 // 0 to 18446744073709551615

2.4 Simple Parse and Get

可以用Parse(json)函数执行简单的解析,result.Get(path)函数搜索结果。

例如,所有这些都将返回相同的结果:

1
2
3
gjson.Parse(json).Get("name").Get("last")
gjson.Get(json, "name").Get("last")
gjson.Get(json, "name.last")

2.5 检查值是否存在

有时你只想知道一个值是否存在。

1
2
3
4
5
6
7
8
9
10
11
value := gjson.Get(json, "name.last")
if !value.Exists() {
println("no last name")
} else {
println(value.String())
}

// Or as one step
if gjson.Get(json, "name.last").Exists() {
println("has a last name")
}

2.6 json验证

Get*Parse*函数的使用前提是一个正常的json,异常的json数据不会引起panic,但是会返回一个意想不到的结果。

如果您使用的是来自不可预测的源的JSON,那么您可能需要在使用GJSON之前进行验证。

1
2
3
4
if !gjson.Valid(json) {
return errors.New("invalid json")
}
value := gjson.Get(json, "name.last")

2.7 转换为map

将json转换为map[string]interface{}的结构

1
2
3
4
m, ok := gjson.Parse(json).Value().(map[string]interface{})
if !ok {
// not a map
}

2.8 使用字节

如果JSON包含在[]byte切片中,则有GetBytes函数。这优于Get(string(data), path)

1
2
var json []byte = ...
result := gjson.GetBytes(json, path)

如果您正在使用gjson.GetBytes(json,path)函数,并且希望避免将result.Raw转换为[]byte,则可以使用以下模式:

1
2
3
4
5
6
7
8
var json []byte = ...
result := gjson.GetBytes(json, path)
var raw []byte
if result.Index > 0 {
raw = json[result.Index:result.Index+len(result.Raw)]
} else {
raw = []byte(result.Raw)
}

这是原始json的无分配子片。此方法使用result.Index字段,该字段是原始json中原始数据的位置。result.Index的值可能等于零,在这种情况下result.Raw被转换为[]byte

2.9 同时获取多个值

GetMany函数可用于同时获取多个值。

1
results := gjson.GetMany(json, "name.first", "name.last", "age")

返回值是一个[]Result,它将始终包含与输入路径完全相同数量的项。

三通配符

key可以包含特殊通配符和.*?

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

三 转义

特殊字符需要转义才能使用

1
fav\.movie             "Deer Hunter"

在源代码中硬编码路径时,还需要确保字符正确转义

1
2
3
// Go
val := gjson.Get(json, "fav\\.movie") // must escape the slash
val := gjson.Get(json, `fav\.movie`) // no need to escape the slash
1
2
3
// Rust
let val = gjson::get(json, "fav\\.movie") // must escape the slash
let val = gjson::get(json, r#"fav\.movie"#) // no need to escape the slash

四 数组

4.1 基本使用

#字符允许深入JSON数组,也可以使用#获取数组的长度

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

对于以下json

1
2
3
4
5
6
7
8
9
10
11
12
13
14
{
"programmers": [
{
"firstName": "Janet",
"lastName": "McLaughlin",
}, {
"firstName": "Elliotte",
"lastName": "Hunter",
}, {
"firstName": "Jason",
"lastName": "Harold",
}
]
}

可以使用programmers.#.lastName 查询

1
2
3
4
result := gjson.Get(json, "programmers.#.lastName")
for _, name := range result.Array() {
println(name.String())
}

还可以查询数组中的对象:

1
2
name := gjson.Get(json, `programmers.#(lastName="Hunter").firstName`)
println(name.String()) // prints "Elliotte"

4.2 遍历对象或数组

ForEach函数允许快速遍历对象或数组。键和值被传递给对象的迭代器函数。仅为数组传递值。从迭代器返回false将停止迭代。

1
2
3
4
5
result := gjson.Get(json, "programmers")
result.ForEach(func(key, value gjson.Result) bool {
println(value.String())
return true // keep iterating
})

五 查询

5.1 基本查询

可以使用 #(...)查询数组的第一个符合条件的结果或者使用#(...)#查询全部符合条件的结果。查询语法支持 ==, !=, <, <=, >, >=比较运算符,简单的模式匹配支持% (like)和!% (not like)

1
2
3
4
5
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"

要查询数组中的非对象值,可以放弃运算符右侧的字符串。

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

5.2 嵌套查询

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

请注意,在v1.3.0之前,查询使用#[…]括号。为了避免与新的多路径语法混淆,v1.3.0中对此进行了更改。为了向后兼容,#[…]将继续工作直到下一个主要版本。

~(波浪号)运算符将在比较之前将值转换为布尔值。

l例如对下面的json

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
{
"vals": [
{ "a": 1, "b": true },
{ "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)值:

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

最后一个不存在的值被视为false

5.3 构造新json

使用选择器语法将多个路径连接到新的JSON文档中。

1
2
{name.first,age,"murphys":friends.#(last="Murphy")#.first}
[name.first,age,children.0]

六 点 和 管道符

.是标准分隔符,但也可以使用|,在大多数情况下,它们都会返回相同的结果.

主要注意的是,在使用#查询数组时,|.的用法是不同的

1
2
3
4
5
6
7
8
9
10
11
12
13
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")#获取到的结果为

1
[{"first": "Dale", "last": "Murphy", "age": 44},{"first": "Jane", "last": "Murphy", "age": 47}]

.first后缀会在每个数组元素返回之前处理它的first路径,所以最终结果为

1
["Dale","Jane"]

|first后缀实际上处理前一个结果之后的第一个路径。由于前面的结果是一个数组,而不是一个对象,因此无法处理,因为first不存在。

然而,|0后缀返回

1
{"first": "Dale", "last": "Murphy", "age": 44}

因为0是上一个结果的第一个索引。

七 修饰符

7.1 内置修饰符

修饰符是对JSON执行自定义处理的路径组件。

例如,在上面的JSON负载上使用内置的@reverse修饰符将反转子数组:

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

内置修饰符的含义如下:

  • @reverse:反转数组或对象的成员。
  • @ugly:从 JSON 中删除所有空格。
  • @pretty:使 JSON 更具人类可读性。
  • @this:返回当前元素。它可用于检索根元素。
  • @valid:确保 JSON 文档有效。
  • @flatten:展平数组。
  • @join:将多个对象合并为一个对象。
  • @keys:返回对象的键数组。
  • @values:返回对象的值数组。
  • @tostr:将 json 转换为字符串。包装 json 字符串。
  • @fromstr:从 json 转换字符串。解包 json 字符串。
  • @group:对对象数组进行分组。请参阅 e4fc67c

7.2 修饰符参数

修饰符可以接受可选参数。参数可以是有效的JSON负载,也可以是字符。

例如,修饰符将json对象作为@pretty的参数

1
@pretty:{"sortKeys":true}

这使得json很漂亮,并对其所有键进行排序。

1
2
3
4
5
6
7
8
9
10
11
{
"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"}
}

使用实例

1
2
3
4
5
children.@reverse                          >> ["Jack","Alex","Sara"]
children.@reverse.0 >> "Jack"
{name.first,"murphys":friends.0}.@pretty >> beautiful JSON
{name.first,"murphys":friends.0}.@ugly >> compact JSON
[@this].#(age>35).name.last >> "Anderson"

7.3 自定义修饰符

可以添加自定义修改器。

例如这里我们创建了一个修饰符,它使整个json文档变为大写或小写。

1
2
3
4
5
6
7
8
9
10
11
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"]

自定义修改器在Rust版本中尚不可用

八 多路径

从v1.3.0开始,GJSON增加了将多个路径连接在一起以形成新文档的功能。在或之间包装逗号分隔的路径将分别产生一个新的数组或对象

例如

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

在这里,我们为姓为“Murphy”的朋友选择了名字、年龄和名字。

您将注意到,可以提供一个可选的键,在本例中为“themurphys”,以强制将键分配给一个值。否则,将使用实际字段的名称,在本例中为“first”。如果无法确定名称,则使用“”。

运行结果如下:

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

九 常量

从v1.12.0开始,GJSON增加了对json文本的支持,这提供了一种构造json静态块的方法。这在使用多路径构建新的json文档时尤其有用。

json文本以“!”开头声明字符。

例如,使用给定的多路径:

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

这里我们选择了名字和年龄。然后添加两个新字段,“company”和“employed”。

运行结果为

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

十 JSON行

支持使用..前缀,将多行文档视为数组。

对于以下文本

1
2
3
4
{"name": "Gilbert", "age": 61}
{"name": "Alexa", "age": 34}
{"name": "May", "age": 57}
{"name": "Deloise", "age": 44}

对应的结果为

1
2
3
4
5
..#                   >> 4
..1 >> {"name": "Alexa", "age": 34}
..3 >> {"name": "Deloise", "age": 44}
..#.name >> ["Gilbert","Alexa","May","Deloise"]
..#(name="May").age >> 57

ForEachLines函数将遍历JSON行。

1
2
3
4
gjson.ForEachLine(json, func(line gjson.Result) bool{
println(line.String())
return true
})