Go 每日一库之 gjson
简介
之前我们介绍过gojsonq
,可以方便地从一个 JSON 串中读取值。同时它也支持各种查询、汇总统计等功能。今天我们再介绍一个类似的库gjson
。在上一篇文章Go 每日一库之 buntdb中我们介绍过 JSON 索引,内部实现其实就是使用gjson
这个库。gjson
实际上是get + json
的缩写,用于读取 JSON 串,同样的还有一个sjson
(set + json
)库用来设置 JSON 串。
快速使用
先安装:
$ go get github.com/tidwall/gjson
后使用:
package main
import (
"fmt"
"github.com/tidwall/gjson"
)
func main() {
json := `{"name":{"first":"li","last":"dj"},"age":18}`
lastName := gjson.Get(json, "name.last")
fmt.Println("last name:", lastName.String())
age := gjson.Get(json, "age")
fmt.Println("age:", age.Int())
}
使用很简单,只需要传入 JSON 串和要读取的键路径即可。注意一点细节,因为gjson.Get()
函数实际上返回的是gjson.Result
类型,我们要调用其相应的方法进行转换对应的类型。如上面的String()
和Int()
方法。
如果是直接打印输出,其实可以省略String()
,fmt
包的大部分函数都可以对实现fmt.Stringer
接口的类型调用String()
方法。
键路径
键路径实际上是以.
分隔的一系列键。gjson
支持在键中包含通配符*
和?
,*
匹配任意多个字符,?
匹配单个字符,例如ca*
可以匹配cat/cate/cake
等以ca
开头的键,ca?
只能匹配cat/cap
等以ca
开头且后面只有一个字符的键。
数组使用键名 + .
+ 索引(索引从 0 开始)的方式读取元素,如果键pets
对应的值是一个数组,那么pets.0
读取数组的第一个元素,pets.1
读取第二个元素。
数组长度使用**键名 + .
+ #
**获取,例如pets.#
返回数组pets
的长度。
如果键名中出现.
,那么需要使用进行转义。
package main
const json = `
{
"name":{"first":"Tom", "last": "Anderson"},
"age": 37,
"children": ["Sara", "Alex", "Jack"],
"fav.movie": "Dear 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"]}
]
}
`
func main() {
fmt.Println("last name:", gjson.Get(json, "name.last"))
fmt.Println("age:", gjson.Get(json, "age"))
fmt.Println("children:", gjson.Get(json, "children"))
fmt.Println("children count:", gjson.Get(json, "children.#"))
fmt.Println("second child:", gjson.Get(json, "children.1"))
fmt.Println("third child*:", gjson.Get(json, "child*.2"))
fmt.Println("first c?ild:", gjson.Get(json, "c?ildren.0"))
fmt.Println("fav.moive", gjson.Get(json, `fav.moive`))
fmt.Println("first name of friends:", gjson.Get(json, "friends.#.first"))
fmt.Println("last name of second friend:", gjson.Get(json, "friends.1.last"))
}
前 3 个比较简单,就不赘述了。看后面几个:
-
children.#
:返回数组children
的长度; -
children.1
:读取数组children
的第 2 个元素(注意索引从 0 开始); -
child*.2
:首先child*
匹配children
,.2
读取第 3 个元素; -
c?ildren.0
:c?ildren
匹配到children
,.0
读取第一个元素; -
fav.moive
:因为键名中含有.
,故需要转义;
-
friends.#.first
:如果数组后#
后还有内容,则以后面的路径读取数组中的每个元素,返回一个新的数组。所以该查询返回的数组所有friends
的first
字段组成; -
friends.1.last
:读取friends
第 2 个元素的last
字段。
运行结果:
last name: Anderson
age: 37
children: ["Sara", "Alex", "Jack"]
children count: 3
second child: Alex
third child*: Jack
first c?ild: Sara
fave.moive
first name of friends: ["Dale","Roger","Jane"]
last name of second friend: Craig
对于数组,gjson
还支持按条件查询元素,#(条件)
返回第一个满足条件的元素,#(条件)#
返回所有满足条件的元素。括号内的条件可以有==
、!=
、<
、<=
、>
、>=
,还有简单的模式匹配%
(符合某个模式),!%
(不符合某个模式):
fmt.Println(gjson.Get(json, `friends.#(last="Murphy").first`))
fmt.Println(gjson.Get(json, `friends.#(last="Murphy")#.first`))
fmt.Println(gjson.Get(json, "friends.#(age>45)#.last"))
fmt.Println(gjson.Get(json, `friends.#(first%"D*").last`))
fmt.Println(gjson.Get(json, `friends.#(first!%"D*").last`))
fmt.Println(gjson.Get(json, `friends.#(nets.#(=="fb"))#.first`))
还是使用上面的 JSON 串。
-
friends.#(last="Murphy").first
:friends.#(last="Murphy")
返回数组friends
中第一个last
为Murphy
的元素,.first
表示取出该元素的first
字段返回; -
friends.#(last="Murphy")#.first
:friends.#(last="Murphy")#
返回数组friends
中所有的last
为Murphy
的元素,然后读取它们的first
字段放在一个数组中返回。注意与上面一个的区别; -
friends.#(age>45)#.last
:friends.#(age>45)#
返回数组friends
中所有年龄大于 45 的元素,然后读取它们的last
字段返回; -
friends.#(first%"D*").last
:friends.#(first%"D*")
返回数组friends
中第一个first
字段满足模式D*
的元素,取出其last
字段返回; -
friends.#(first!%"D*").last
:``friends.#(first!%"D*")返回数组
friends中第一个
first字段**不**满足模式
D*的元素,读取其
last`字段返回; -
friends.#(nets.#(=="fb"))#.first
:这是个嵌套条件,friends.#(nets.#(=="fb"))#
返回数组friends
的元素的nets
字段中有fb
的所有元素,然后取出first
字段返回。
运行结果:
Dale
["Dale","Jane"]
["Craig","Murphy"]
Murphy
Craig
["Dale","Roger"]
修饰符
修饰符是gjson
提供的非常强大的功能,和键路径搭配使用。gjson
提供了一些内置的修饰符:
-
@reverse
:翻转一个数组; -
@ugly
:移除 JSON 中的所有空白符; -
@pretty
:使 JSON 更易用阅读; -
@this
:返回当前的元素,可以用来返回根元素; -
@valid
:校验 JSON 的合法性; -
@flatten
:数组平坦化,即将["a", ["b", "c"]]
转为["a","b","c"]
; -
@join
:将多个对象合并到一个对象中。
修饰符的语法和管道类似,以|
分隔键路径和分隔符。
const json = `{
"name":{"first":"Tom", "last": "Anderson"},
"age": 37,
"children": ["Sara", "Alex", "Jack"],
"fav.movie": "Dear 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"]}
]
}`
func main() {
fmt.Println(gjson.Get(json, "children|@reverse"))
fmt.Println(gjson.Get(json, "children|@reverse|0"))
fmt.Println(gjson.Get(json, "friends|@ugly"))
fmt.Println(gjson.Get(json, "friends|@pretty"))
fmt.Println(gjson.Get(json, "@this"))
nestedJSON := `{"nested": ["one", "two", ["three", "four"]]}`
fmt.Println(gjson.Get(nestedJSON, "nested|@flatten"))
userJSON := `{"info":[{"name":"dj", "age":18},{"phone":"123456789","email":"dj@example.com"}]}`
fmt.Println(gjson.Get(userJSON, "info|@join"))
}
children|@reverse
先读取数组children
,然后使用修饰符@reverse
翻转之后返回,输出:
["Jack","Alex","Sara"]
children|@reverse|0
在上面翻转的基础上读取第一个元素,即原数组的最后一个元素,输出:
Jack
friends|@ugly
移除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|@pretty
格式化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"]
}
]
@this
返回原始的 JSON 串。
@flatten
将数组nested
的内层数组平坦到外层后返回,即将所有内层数组的元素依次添加到外层数组后面并移除内层数组,输出:
["one","two","three", "four"]
@join
将一个数组中的各个对象合并到一个中,例子中将数组中存放的部分个人信息合并成一个对象返回:
{"name":"dj","age":18,"phone":"123456789","email":"dj@example.com"}
修饰符参数
修饰符还可以有参数,通过在修饰符后加:
后跟参数。如果我们在格式化 JSON 串时,想要对键进行排序,那么可以使用@pretty
修饰符的sortKeys
参数。我们还是拿上面的 JSON 数据举例:
fmt.Println(gjson.Get(json, `friends|@pretty:{"sortKeys":true}`))
最终按键名顺序输出 JSON 串:
[
{
"age": 44,
"first": "Dale",
"last": "Murphy",
"nets": ["ig", "fb", "tw"]
},
{
"age": 68,
"first": "Roger",
"last": "Craig",
"nets": ["fb", "tw"]
},
{
"age": 47,
"first": "Jane",
"last": "Murphy",
"nets": ["ig", "tw"]
}
]
当然还可以指定每行缩进indent
(默认两个空格),每行开头字符串prefix
(默认为空串)和一行最多显示字符数width
(默认 80 字符)。下面在每行前增加两个空格:
fmt.Println(gjson.Get(json, `friends|@pretty:{"sortKeys":true,"prefix":" "}`))
自定义修饰符
如此强大的功当然要支持自定义!gjson
使用AddModifier()
添加一个修饰符,传入一个名字和类型为func(json arg string) string
的处理函数。处理函数接受待处理的 JSON 值和修饰符参数,返回处理后的结果。下面编写一个转换大小写的修饰符:
func main() {
gjson.AddModifier("case", func(json, arg string) string {
if arg == "upper" {
return strings.ToUpper(json)
}
if arg == "lower" {
return strings.ToLower(json)
}
return json
})
const json = `{"children": ["Sara", "Alex", "Jack"]}`
fmt.Println(gjson.Get(json, "children|@case:upper"))
fmt.Println(gjson.Get(json, "children|@case:lower"))
}
输出:
["SARA", "ALEX", "JACK"]
["sara", "alex", "jack"]
JSON 行
gjson
提供..
语法可以将多行数据看成一个数组,每行数据是一个元素:
const json = `
{"name": "Gilbert", "age": 61}
{"name": "Alexa", "age": 34}
{"name": "May", "age": 57}
{"name": "Deloise", "age": 44}`
func main() {
fmt.Println(gjson.Get(json, "..#"))
fmt.Println(gjson.Get(json, "..1"))
fmt.Println(gjson.Get(json, "..#.name"))
fmt.Println(gjson.Get(json, `..#(name="May").age`))
}
-
..#
:返回有多少行 JSON 数据; -
..1
:返回第一行,即{"name": "Gilbert", "age": 61}
; -
..#.name
:#
后再接路径,表示对数组中每个元素读取后面的路径,将读取到的值组成一个新数组返回;..#.name
表示读取每一行中的name
字段,最终返回["Gilbert","Alexa","May","Deloise"]
; -
..#(name="May").age
:括号中的内容(name="May")
表示条件,所以该条含义为取name
为"May"
的行中的age
字段。
gjson
还提供了遍历 JSON 行的方法:gjson.ForEachLine()
,参数为 JSON 串和类型为func(line gjson.Result) bool
的回调函数。回调返回false
时遍历停止。下面代码读取输出每一行的name
字段:
gjson.ForEachLine(json, func(line gjson.Result) bool {
fmt.Println("name:", gjson.Get(line.String(), "name"))
return true
})
遍历
上面我们介绍了遍历 JSON 行的方式,实际上gjson
还提供了通用的遍历数组和对象的方式。gjson.Get()
方法返回一个gjson.Result
类型的对象,json.Result
提供了ForEach()
方法用于遍历。该方法接受一个类型为func (key, value gjson.Result) bool
的回调函数。遍历对象时key
和value
分别为对象的键和值;遍历数组时,value
为数组元素,key
为空(不是索引)。回调返回false
时,遍历停止。
const json = `
{
"name":"dj",
"age":18,
"pets": ["cat", "dog"],
"contact": {
"phone": "123456789",
"email": "dj@example.com"
}
}`
func main() {
pets := gjson.Get(json, "pets")
pets.ForEach(func(_, pet gjson.Result) bool {
fmt.Println(pet)
return true
})
contact := gjson.Get(json, "contact")
contact.ForEach(func(key, value gjson.Result) bool {
fmt.Println(key, value)
return true
})
}
校验 JSON
调用gjson.Get()
时,gjson
假设我们传入的 JSON 串是合法的。如果 JSON 非法也不会panic
,这时会返回不确定的结果:
func main() {
const json = `{"name":dj,age:18}`
fmt.Println(gjson.Get(json, "name"))
}
上面 JSON 串是非法的,dj
和age
都没有加上双引号(实际上习惯了 Go 语言map
的写法,很容易把 JSON 写成这样?)。上面代码输出18,显然是错误的。我们可以使用gjson.Valid()
检测 JSON 串是否合法:
if !gjson.Valid(json) {
fmt.Println("error")
} else {
fmt.Println("ok")
}
一次获取多个值
调用gjson.Get()
一次只能读取一个值,多次调用又比较麻烦,gjson
提供了GetMany()
可以一次读取多个值,返回一个数组[]gjson.Result
。
const json = `
{
"name":"dj",
"age":18,
"pets": ["cat", "dog"],
"contact": {
"phone": "123456789",
"email": "dj@example.com"
}
}`
func main() {
results := gjson.GetMany(json, "name", "age", "pets.#", "contact.phone")
for _, result := range results {
fmt.Println(result)
}
}
上面代码返回字段name
、age
、数组pets
的长度和contact.phone
字段。
总结
gjson
使用比较方便,功能强大,性能可观,值得一学。
大家如果发现好玩、好用的 Go 语言库,欢迎到 Go 每日一库 GitHub 上提交 issue?
参考
- gjson GitHub:https://github.com/tidwall/gjson
- Go 每日一库 GitHub:https://github.com/darjun/go-daily-lib
- R语言与点估计学习笔记(EM算法与Bootstrap法)
- 开发 | 为个人深度学习机器选择合适的配置
- 阿里音乐流行趋势预测竞赛数据清洗整合——纯python
- 生产环境sql语句调优实战第二篇(r2第38天)
- 生产环境sql语句调优实战第三篇(r2笔记38天)
- 简单易学的机器学习算法——K-Means算法
- 通过shell脚本定位性能sql和生成报告(r2笔记37天)
- VXFS启用异步IO导致的严重问题(r2笔记56天)
- 通过sql语句分析足彩(r2笔记55天)
- 关于验证表中有无数据的方法比较(r2笔记54天)
- 海量数据迁移之分区并行抽取(r2笔记53天)
- 海量数据迁移之外部表切分(r2笔记52天)
- 怎样突破表名30个字符的限制(r2笔记51天)
- C/C++——排序
- JavaScript 教程
- JavaScript 编辑工具
- JavaScript 与HTML
- JavaScript 与Java
- JavaScript 数据结构
- JavaScript 基本数据类型
- JavaScript 特殊数据类型
- JavaScript 运算符
- JavaScript typeof 运算符
- JavaScript 表达式
- JavaScript 类型转换
- JavaScript 基本语法
- JavaScript 注释
- Javascript 基本处理流程
- Javascript 选择结构
- Javascript if 语句
- Javascript if 语句的嵌套
- Javascript switch 语句
- Javascript 循环结构
- Javascript 循环结构实例
- Javascript 跳转语句
- Javascript 控制语句总结
- Javascript 函数介绍
- Javascript 函数的定义
- Javascript 函数调用
- Javascript 几种特殊的函数
- JavaScript 内置函数简介
- Javascript eval() 函数
- Javascript isFinite() 函数
- Javascript isNaN() 函数
- parseInt() 与 parseFloat()
- escape() 与 unescape()
- Javascript 字符串介绍
- Javascript length属性
- javascript 字符串函数
- Javascript 日期对象简介
- Javascript 日期对象用途
- Date 对象属性和方法
- Javascript 数组是什么
- Javascript 创建数组
- Javascript 数组赋值与取值
- Javascript 数组属性和方法
- matplotlib基础绘图命令之hist
- Python|一文详解数据预处理
- 安排上了!PC人脸识别登录,出乎意料的简单
- valgrind使用介绍
- 实用!一键生成数据库文档,堪称数据库界的Swagger
- 算法集锦(2)|scikit-learn| 如何利用文本挖掘推荐Ted演讲
- 算法集锦(3)|采用医疗数据预测糖尿病的算法
- 谁说Cat不能做链路跟踪的,给我站出来
- Libra:一种Python工具,可以用几行代码自动实现机器学习过程
- 国内首个“新基建”安全大赛启动了!
- Kubernetes 中 Informer 的使用
- 嵌入式开发中常见3个的C语言技巧
- 恕我直言,我也是才知道ElasticSearch条件更新是这么玩的
- 有了MinIO,你还会用FastDFS么?
- STP 实验