Go 每日一库之 gojsonq
简介
在日常工作中,每一名开发者,不管是前端还是后端,都经常使用 JSON。JSON 是一个很简单的数据交换格式。相比于 XML,它灵活、轻巧、使用方便。JSON 也是RESTful API推荐的格式。有时,我们只想读取 JSON 中的某一些字段。如果自己手动解析、一层一层读取,这就变得异常繁琐了。特别是在嵌套层次很深的情况下。今天我们介绍gojsonq
。它可以帮助我们很方便的操作 JSON。
快速使用
先安装:
$ go get github.com/thedevsaddam/gojsonq
后使用:
package main
import (
"fmt"
"github.com/thedevsaddam/gojsonq"
)
func main() {
content := `{
"user": {
"name": "dj",
"age": 18,
"address": {
"provice": "shanghai",
"district": "xuhui"
},
"hobbies":["chess", "programming", "game"]
}
}`
gq := gojsonq.New().FromString(content)
district := gq.Find("user.address.district")
fmt.Println(district)
gq.Reset()
hobby := gq.Find("user.hobbies.[0]")
fmt.Println(hobby)
}
操作非常简单:
- 首先调用
gojsonq.New()
创建一个JSONQ
的对象; - 然后就可以使用该类型的方法来查询属性了。
上面代码我们直接读取位于最内层的district
值和hobbies
数组的第一个元素!层与层之间用.
隔开,如果是数组,则在属性字段后通过.[index]
读取下标为index
的元素。这种方式可以实现很灵活的读取。
注意到一个细节:在查询之后,我们手动调用了一次Reset()
方法。因为JSONQ
对象在调用Find
方法时,内部会记录当前的节点,下一个查询会从上次查找的节点开始。也就是说如果我们注释掉jq.Reset()
,第二个Find()
方法实际上查找的是user.address.district.user.hobbies.[0]
,自然就返回nil
了。除此之外,gojsonq
也提供了另外一种方式。如果你想要保存当前查询的一些状态信息,可以调用JSONQ
的Copy
方法返回一个初始状态下的对象,它们会共用底层的 JSON 字符串和解析后的对象。上面的gq.Reset()
可以由下面这行代码代替:
gpCopy := gp.Copy()
后面就可以使用gpCopy
查询hobbies
了。
这个算是gojsonq
库的一个特点,但也是初学者带来了很多困扰,需要特别注意。实际上,JSONQ
提供的很多方法会改变当前节点,稍后部分我们会更清楚的看到。
数据源
除了从字符串中加载,jsonq
还允许从文件和io.Reader
中读取内容。分别使用JSONQ
对象的File
和Reader
方法:
func main() {
gq := gojsonq.New().File("./data.json")
fmt.Println(gq.Find("items.[1].price"))
}
和下面程序的效果是一样的:
func main() {
file, err := os.OpenFile("./data.json", os.O_RDONLY, 0666)
if err != nil {
log.Fatal(err)
}
gq := gojsonq.New().Reader(file)
fmt.Println(gq.Find("items.[1].price"))
}
为了后面演示方便,我构造了一个data.json
文件:
{
"name": "shopping cart",
"description": "List of items in your cart",
"prices": ["2400", "2100", "1200", "400.87", "89.90", "150.10"],
"items": [
{
"id": 1,
"name": "Apple",
"count": 2,
"price": 12
},
{
"id": 2,
"name": "Notebook",
"count": 10,
"price": 3
},
{
"id": 3,
"name": "Pencil",
"count": 5,
"price": 1
},
{
"id": 4,
"name": "Camera",
"count": 1,
"price": 1750
},
{
"id": null,
"name": "Invalid Item",
"count": 1,
"price": 12000
}
]
}
高级查询
gojsonq
的独特之处在于,它可以像 SQL 一样进行条件查询,可以选择返回哪些字段,可以做一些聚合统计。
字段映射
有时候,我们只关心对象中的几个字段,这时候就可以使用Select
指定返回哪些字段,其余字段不返回:
func main() {
r := gojsonq.New().File("./data.json").From("items").Select("id", "name").Get()
data, _ := json.MarshalIndent(r, "", " ")
fmt.Println(string(data))
}
只会输出id
和name
字段:
$ go run main.go
[
{
"id": 1,
"name": "Apple"
},
{
"id": 2,
"name": "Notebook"
},
{
"id": 3,
"name": "Pencil"
},
{
"id": 4,
"name": "Camera"
},
{
"id": null,
"name": "Invalid Item"
}
]
为了显示更直观一点,我这里用json.MarshalIndent()
对输出做了一些美化。
是不是和 SQL 有点像Select id,name From items
...
这里介绍一下From
方法,这个方法的作用是将当前节点移动到指定位置。上面也说过当前节点的位置是记下来的。例如,上面的代码中我们先将当前节点移动到items
,后面的查询和聚合操作都是针对这个数组。实际上Find
方法内部就调用了From
:
// src/github.com/thedevsaddam/gojsonq/jsonq.go
func (j *JSONQ) Find(path string) interface{} {
return j.From(path).Get()
}
func (j *JSONQ) From(node string) *JSONQ {
j.node = node
v, err := getNestedValue(j.jsonContent, node, j.option.separator)
if err != nil {
j.addError(err)
}
// ============= 注意这一行,记住当前节点位置
j.jsonContent = v
return j
}
最后必须要调用Get()
,它组合所有条件后执行这个查询,返回结果。
条件查询
有了Select
和From
,怎么能没有Where
呢?gojsonq
提供的Where
方法非常多,我们大概看几个就行了。
首先是,Where(key, op, val)
,这个是通用的Where
条件,表示key
和val
是否满足op
关系。op
内置的就有将近 20 种,还支持自定义。例如=
表示相等,!=
表示不等,startsWith
表示val
是否是key
字段的前缀等等等等;
其他很多条件都是Where
的特例,例如WhereIn(key, val)
就等价于Where(key, "in", val)
,WhereStartsWith(key, val)
就等价于Where(key, "startsWith", val)
。
默认情况下,Where
的条件都是And
连接的,我们可以通过OrWhere
让其以Or
连接:
func main() {
gq := gojsonq.New().File("./data.json")
r := gq.From("items").Select("id", "name").
Where("id", "=", 1).OrWhere("id", "=", 2).Get()
fmt.Println(r)
gq.Reset()
r = gq.From("items").Select("id", "name", "count").
Where("count", ">", 1).Where("price", "<", 100).Get()
fmt.Println(r)
}
上面第一个查询,查找id
为 1 或 2 的记录。第二个查询,查找count
大于 1 且 price
小于 100 的记录。
指定偏移和返回条目数
有时我们想要分页显示,第一次查询时返回前 3 条内容,第二次查询时返回接下来的 3 条记录。我们可以使用JSONQ
对象的Offset
和Limit
方法来指定偏移和返回的条目数:
func main() {
gq := gojsonq.New().File("./data.json")
r1 := gq.From("items").Select("id", "name").Offset(0).Limit(3).Get()
fmt.Println("First Page:", r1)
gq.Reset()
r2 := gq.From("items").Select("id", "name").Offset(3).Limit(3).Get()
fmt.Println("Second Page:", r2)
}
来看看运行结果:
$ go run main.go
First Page: [map[id:1 name:Apple] map[id:2 name:Notebook] map[id:3 name:Pencil]]
Second Page: [map[id:4 name:Camera] map[id:<nil> name:Invalid Item]]
聚合统计
我们还能可以对一些字段做简单的统计,计算和、平均数、最大、最小值等:
func main() {
gq := gojsonq.New().File("./data.json").From("items")
fmt.Println("Total Count:", gq.Sum("count"))
fmt.Println("Min Price:", gq.Min("price"))
fmt.Println("Max Price:", gq.Max("price"))
fmt.Println("Avg Price:", gq.Avg("price"))
}
上面统计商品的总数量、最低价格、最高价格和平均价格。
聚合统计类的方法都不会修改当前节点的指向,所以JSONQ
对象可以重复使用!
还可以对数据进行分组和排序:
func main() {
gq := gojsonq.New().File("./data.json")
fmt.Println(gq.From("items").GroupBy("price").Get())
gq.Reset()
fmt.Println(gq.From("items").SortBy("price", "desc").Get())
}
其他格式
默认情况下,gojsonq
使用 JSON 格式解析数据。我们也可以设置其他格式解析器让gojsonq
可以处理其他格式的数据:
func main() {
jq := gojsonq.New(gojsonq.SetDecoder(&yamlDecoder{})).File("./data.yaml")
jq.From("items").Where("price", "<=", 500)
fmt.Printf("%vn", jq.First())
}
type yamlDecoder struct {
}
func (i *yamlDecoder) Decode(data []byte, v interface{}) error {
bb, err := yaml.YAMLToJSON(data)
if err != nil {
return err
}
return json.Unmarshal(bb, &v)
}
上面代码用到了yaml
库,需要额外安装:
$ go get github.com/ghodss/yaml
解析器只要实现gojsonq.Decoder
接口,都可以作为设置到gojsonq
中,这样就可以实现任何格式的处理:
// src/github.com/thedevsaddam/gojsonq/decoder.go
type Decoder interface {
Decode(data []byte, v interface{}) error
}
总结
gojsonq
还有一些高级特性,例如自定义Where
的操作类型,取第一个、最后一个、第 N 个值等。感兴趣可自行研究~
大家如果发现好玩、好用的 Go 语言库,欢迎到 Go 每日一库 GitHub 上提交 issue?
参考
- gojsonq GitHub:https://github.com/thedevsaddam/gojsonq
- Go 每日一库 GitHub:https://github.com/darjun/go-daily-lib
- 如何将Kerberos环境下CDH集群JAVA升级至JDK8
- 干货|如何做准确率达98%的交通标志识别系统?
- 用57行代码搞定花8000万美元采购车牌识别项目
- Cloudera Manager Server服务在RedHat7状态显示异常分析
- 开源 | 基于Python的人脸识别:识别准确率高达99.38%!
- 转录组数据的基因表达变化情况探索
- 如何配置Kerberos服务的高可用
- 利用深度学习生成梵高风格画像
- 使用Python-Requests实现ODL对OVS的流表下发
- Keras入门必看教程
- bedtools 用法大全(一文就够吧)
- 区块链资产安全攻略
- 如何在Kudu1.5中使用Sentry授权
- 深度学习入门实战
- 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 数组属性和方法