3

GO中 gjson 的应用和分享

 3 years ago
source link: https://segmentfault.com/a/1190000040419679
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

[TOC]

GO中gjson的应用和分享

咱们上次分享到使用 GO 爬取静态网页的数据,一起来回顾一下

  • 分享静态网页和动态网页的简要说明
  • GO 爬取静态网页简单数据
  • GO 爬取网页上的图片
  • 并发爬取网页上的资源

要是对 GO 爬取静态数据还有点兴趣的话,欢迎查看文章 分享一波 GO 的爬虫

json 是什么?

JSON(JavaScript Object Notation, JS 对象简谱) 是一种轻量级的数据交换格式

它基于 ECMAScript (欧洲计算机协会制定的 JS 规范)的一个子集,采用完全独立于编程语言的文本格式来存储和表示数据

json 有如下几个优势:

  • 层次结构简洁清晰
  • 易于阅读和编写
  • 易于机器解析和生成
  • 能够提升网络传输效率

简单列一下咱们常用的数据序列化的方式

是可扩展标记语言,是一种简单的数据存储语言

  • protobuf

是一种平台无关、语言无关、可扩展且轻便高效的序列化数据结构的协议,可以用于 网络通信 和 数据存储

gjson 是什么?

是 GO 里面的一个库

它主要是提供了一种非常快速简单的方式从json文档中获取相应值

这个 gjson库,实际上是 get + json的缩写,无独有偶,同样的也有sjson库,小伙伴们就知道他代表的含义了吧,是 set + json的意思

gjson 如何使用?

对于 gjson如何使用,XDM,我这里把这个库的基本使用,涉及到的知识点,以及注意事项,给大家梳理梳理

要是想看看 gjson的源码是如何实现高效快速的操作json的,感兴趣的朋友,可以在先会引用的基础上再去详细查看一下源码

本文的分享,围绕如下 4 个方面来实操和梳理 gjson 的使用:

  • gjson 的简单使用
  • gjsonjson
  • gjson 的 修饰符 和 自定义修饰符
  • gjson 键路径的匹配规则

gjson 的简单使用

咱们简单使用一个gjson ,如下编码涉及如下几个点:

  • 设置具体的json 数据
  • 校验 json 数据 是否合法
  • 一次性获取单个值
  • 一次性获取多个值
package main

import (
   "log"
   "github.com/tidwall/gjson"
)

func main() {

   // 设置参数,打印行数
   log.SetFlags(log.Lshortfile | log.LstdFlags)
   
  // 设置 json 数据
   json := `
         {
            "author": {
               "name": "xiaomotong",
               "age": 18,
               "hobby": "writing"
            },
            "extra": "hello wolrd"
            "picList":[{"name":"xiaozhu1"},{"name":"xiaozhu2"}]
         }
         `
   // 校验 json 字符串是否合法
   // 如果不合法的话, gjson 不会报错 panic,可能会拿到一个奇怪值
   if gjson.Valid(json){
      log.Println("json valid ...")
   }else{
      log.Fatal("json invalid ... ")
   }

   // 获取 author.name 的 值
   aName := gjson.Get(json, "author.name")
   log.Println("aName :", aName.String())

   // 获取 extra 的值
   extra := gjson.Get(json, "extra")
   log.Println("extra:", extra)

   // 获取 一个不存在的 键 对应的 值
   non := gjson.Get(json, "non")
   log.Println("non:", non)


   // 一次性 获取json 的多个键 值
    res := gjson.GetMany(json, "author.age", "author.hobby","picList")
    for i, v := range res{
        if i == 0{
            log.Println(v.Int())
        }else if i == 2{
            for _,vv := range v.Array(){
                log.Println("picList.name :",vv.Get("name"))
            }
        }else{
            log.Println(v)
        }
    }
}

运行上述代码后,可以看到如下效果:

2021/06/xx xx:32:04 main.go:28: json valid ...
2021/06/xx xx:32:04 main.go:35: aName : xiaomotong
2021/06/xx xx:32:04 main.go:39: extra: hello wolrd
2021/06/xx xx:32:04 main.go:43: non:
2021/06/xx xx:32:04 main.go:50: 18
2021/06/xx xx:32:04 main.go:57: writing
2021/06/xx xx:32:04 main.go:53: picList.name : xiaozhu1
2021/06/xx xx:32:04 main.go:53: picList.name : xiaozhu2

我们需要注意,要把我们的数据源弄对,也就是咱们的json数据必须是合法的,否则,使用gjson 库拿到的数据就不会是咱们期望的值

  • 使用 gjson.Get() ,获取单个值
  • 使用 gjson.GetMany() ,获取多个值
  • 使用gjson.Valid(),判断json数据是否有效

gjsonjson

再来看看 json 行

gjson提供如下语法,来解析json 行 数据:

  • ..#

输出 json 行数组的长度

  • ..#.author

输出 json 每一行 里面的 author 对应的值,组成一个数组

  • ..#(author="xiaomotong").hobby

    输出输出 json 行 中,author = xiaomotong 所在行 对应的 hobby 值

  • ..1

输出 json 行 数组的第 2 行 , 若是 ..2则输出第 3

  • 遍历 json 行

使用 gjson.ForEachLine 遍历json 行的每一行数据,每一行数据里面的细节也能遍历出来

咱们写一个DEMO 来覆盖一下上面需要用到的语法:

package main

import (
   "github.com/tidwall/gjson"
   "log"
)

const json = `
   {"author": "xiaomotong", "age": 18, "hobby":"play"}
   {"author": "xiaozhu", "age": 19 , "hobby":"eat"}
   {"author": "zhangsan", "age": 20, "hobby":"drink"}
   {"author": "lisi", "age": 21, "hobby":"sleep"}`

func main() {

   // 设置参数,打印行数
   log.SetFlags(log.Lshortfile | log.LstdFlags)

   // 输出 json 行数组的长度
   log.Println(gjson.Get(json, "..#"))
    
   // 输出 json 行 数组的第 3 行
   log.Println(gjson.Get(json, "..2"))
    
   // 输出 json 每一行 里面的 author 对应的值,组成一个数组
   log.Println(gjson.Get(json, "..#.author"))
    
   // 输出输出 json 行 中,author = xiaomotong 所在行 对应的 hobby 值
   log.Println(gjson.Get(json, `..#(author="xiaomotong").hobby`))

   // 遍历 json 行
   gjson.ForEachLine(json, func(jLine gjson.Result) bool {
      log.Println("author:", gjson.Get(jLine.String(), "hobby"))
      return true
   })
}

上述代码运行之后结果如下:

2021/06/xx xx:17:52 main2.go:20: 4
2021/06/xx xx:17:52 main2.go:22: {"author": "zhangsan", "age": 20, "hobby":"drink"}
2021/06/xx xx:17:52 main2.go:24: ["xiaomotong","xiaozhu","zhangsan","lisi"]
2021/06/xx xx:17:52 main2.go:26: play
2021/06/xx xx:17:52 main2.go:30: author: play
2021/06/xx xx:17:52 main2.go:30: author: eat
2021/06/xx xx:17:52 main2.go:30: author: drink
2021/06/xx xx:17:52 main2.go:30: author: sleep

咱们来看看函数 gjson.ForEachLine 的实现方式:

// ForEachLine iterates through lines of JSON as specified by the JSON Lines
// format (http://jsonlines.org/).
// Each line is returned as a GJSON Result.
func ForEachLine(json string, iterator func(line Result) bool) {
   var res Result
   var i int
   for {
      i, res, _ = parseAny(json, i, true)
      if !res.Exists() {
         break
      }
      if !iterator(res) {
         return
      }
   }
}

每一行都会返回一个 JSON 的结果, Result

parseAny是解析每一行的具体json 数据 , parseAny函数里面就会很详细的涉及到 如何判断处理每一个字符

// parseAny parses the next value from a json string.
// A Result is returned when the hit param is set.
// The return values are (i int, res Result, ok bool)
func parseAny(json string, i int, hit bool) (int, Result, bool) {
   var res Result
   var val string
   // 一个字符一个字符的做处理
   // 不同的字符 有对应的逻辑,感兴趣的XDM 可以细品
   for ; i < len(json); i++ {
      if json[i] == '{' || json[i] == '[' {
         i, val = parseSquash(json, i)
         if hit {
             // 对应字符赋值
            res.Raw = val
            res.Type = JSON
         }
         return i, res, true
      }
      if json[i] <= ' ' {
         continue
      }
      // 排除上述特殊几种情况后,继续按照下述情况进行处理字符
      switch json[i] {
      case '"':
         i++
         var vesc bool
         var ok bool
         i, val, vesc, ok = parseString(json, i)
         if !ok {
            return i, res, false
         }
         if hit {
            res.Type = String
            res.Raw = val
            if vesc {
               res.Str = unescape(val[1 : len(val)-1])
            } else {
               res.Str = val[1 : len(val)-1]
            }
         }
         return i, res, true
      case '-', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9':
         i, val = parseNumber(json, i)
         if hit {
            res.Raw = val
            res.Type = Number
            res.Num, _ = strconv.ParseFloat(val, 64)
         }
         return i, res, true
      case 't', 'f', 'n':
         vc := json[i]
         i, val = parseLiteral(json, i)
         if hit {
            res.Raw = val
            switch vc {
            case 't':
               res.Type = True
            case 'f':
               res.Type = False
            }
            return i, res, true
         }
      }
   }
   return i, res, false
}

我们来看看 Result的具体数据结构

// Result represents a json value that is returned from Get().
type Result struct {
   // Type is the json type
   Type Type
   // Raw is the raw json
   Raw string
   // Str is the json string
   Str string
   // Num is the json number
   Num float64
   // Index of raw value in original json, zero means index unknown
   Index int
}

根据 Type Type的不同,对应到 Str stringNum float64Index int 里面数据的不同

此处的Type 咱们来看看都有哪些情况

const (
   // Null is a null json value
   Null Type = iota
   // False is a json false boolean
   False
   // Number is json number
   Number
   // String is a json string
   String
   // True is a json true boolean
   True
   // JSON is a raw block of JSON
   JSON
)

这样看起来就一目了然了吧,还是对于gjson库解析对应字符想再深入研究的话,可以下载 gjson ,看看gjson.go源码文件里面的具体实现

gjson 键路径的匹配规则

键路径是什么?

就是以 .分隔的键 , 咱们列个表格看看 gjson都支持哪些匹配规则

tag说明?匹配单个字符,例如hell?
就能够匹配 hello 键,匹配不了 helloo*匹配任意多个字符,例如hell*
可以匹配 hello , helloooo , 都可以xx.xx用于匹配数组,例如 hello 是一个数组,
那么 hello.0 就是匹配数组第 1 个元素
hello.1 就是匹配的 2 个元素xx.#获取数组的长度,例如 hello.#若键名里面出现了 . ,那么需要用\进行转义这个也好理解, 例如 键名字就叫 hello.world
此时需要使用这个键的时候,就需要这样来转义 hello\.world==、!=、<、<=、>、>=例如 hello 是一个组数,数组里面有元素字段是 nameage
咱们匹配的时候,可以加 #来灵活匹配我们想要的数据
例如: hello.#(name="xiaozhu").age%模式匹配 , 例如
hello.#(name%"n*").age!%模式匹配 , 例如
hello.#(name!%"n*").age

咱们一起来使用一下上述的匹配规则:

package main

import (
   "github.com/tidwall/gjson"
   "log"
)
// json 源 数据
const json = `
{
  "author":{"name":"xiaomotong", "nick": "xiaozhu"},
  "age": 18,
  "hobby": ["play", "eat", "drink"],
  "love.music": "one day",
  "location": [
    {"province": "gd", "city":"gz", "area": "huangpu"},
    {"province": "gd", "city":"sz", "area": "nanshan"},
  ]
}
`

func main() {

   // 设置参数,打印行数
   log.SetFlags(log.Lshortfile | log.LstdFlags)

   // 获取名字
   log.Println("author:", gjson.Get(json, "author.name"))
   // 获取年龄
   log.Println("age:", gjson.Get(json, "age"))
    
   // 使用 ? #  *操作 hobby
   log.Println("hobby:", gjson.Get(json, "hobb?"))
   log.Println("hobby count:", gjson.Get(json, "hobby.#"))
   log.Println("second hobby:", gjson.Get(json, "ho?by.1"))
   log.Println("third hobby:", gjson.Get(json, "ho*.2"))
    
   // 键中 带有 .   我们用 \. 来转义
   log.Println("love.music", gjson.Get(json, `love\.music`))
    
   // 获取数组里面的元素 ,若我们需要获取location数组第一个元素里面的 city ,我们可以这样 gjson.Get(json, "location.0.city")
   log.Println("location first city :", gjson.Get(json, "location.0"))
   log.Println("location second city :", gjson.Get(json, "location.1"))
}

上述代码运行结果如下:

2021/06/xx xx:03:26 main3.go:27: author: xiaomotong
2021/06/xx xx:03:26 main3.go:29: age: 18
2021/06/xx xx:03:26 main3.go:31: hobby: ["play", "eat", "drink"]
2021/06/xx xx:03:26 main3.go:32: hobby count: 3
2021/06/xx xx:03:26 main3.go:34: second hobby: eat
2021/06/xx xx:03:26 main3.go:35: third hobby: drink
2021/06/xx xx:03:26 main3.go:37: love.music one day
2021/06/xx xx:03:26 main3.go:39: location first city : {"province": "gd", "city":"gz", "area": "huangpu"}
2021/06/xx xx:03:26 main3.go:40: location second city : {"province": "gd", "city":"sz", "area": "nanshan"}

gjson库里面的各种规则,不难,我们可以看看其中的用法,自己实操找一下,记忆会更加深刻一些,到时候真正用到了,再来查一遍,就不生疏了

gjson 的 修饰符 和 自定义修饰符

最后咱们再来说说 gjson库里面的修饰符 , 修饰符的功能也是很强大的,一般是和键地址一起玩

咱们先整理一下内置的修饰符都有哪些:

tag说明@reverse翻转一个数组@ugly移除JSON 中的所有空白符@valid校验 JSON 的合法性@pretty使 JSON 更易用阅读@flatten数组平坦化,即将["小猪1", ["小猪2", "小猪3"]]转为["小猪1","小猪2","小猪3"]@this返回当前的元素,可以用来返回根元素@join将多个对象合并到一个对象中

继续上DEMO

package main

import (
    "github.com/tidwall/gjson"
    "log"
)

const json = `
{
  "author":{"name":"xiaomotong", "nick": "xiaozhu"},
  "age": 18,
  "hobby": ["play", "eat", "drink"],
  "love.music": "one day",
  "location": [
    {"province": "gd", "city":"gz", "area": "huangpu"},
    {"province": "gd", "city":"sz", "area": "nanshan"},
  ]
}
`
func main() {

    // 设置参数,打印行数
    log.SetFlags(log.Lshortfile | log.LstdFlags)

    // 翻转 hobby 数组
    log.Println("翻转 hobby 数组 hobby reverse:", gjson.Get(json, "hobby|@reverse"))
    
    // 移除空白符
    log.Println("移除空白符 location.0:", gjson.Get(json, "location.0|@ugly"))

    // 使json 更加容易阅读 pretty
    log.Println("使json 更加容易阅读 pretty location : ", gjson.Get(json, "location.1|@pretty"))
    
    // 输出整个json
    log.Println(" 输出整个json this : ", gjson.Get(json, "@this"))

    test := `["小猪1", ["小猪2", "小猪3"]]`
    // 扁平化
    log.Println("扁平化 this : ", gjson.Get(test, "@flatten"))
}

运行上述代码,我们可以看到如下效果:

2021/06/xx xx:30:24 main4.go:27: 翻转 hobby 数组 hobby reverse: ["drink","eat","play"]
2021/06/xx xx:30:24 main4.go:29: 移除空白符 location.0: {"province":"gd","city":"gz","area":"huangpu"}
2021/06/xx xx:30:24 main4.go:32: 使json 更加容易阅读 pretty location :  {
  "province": "gd",
  "city": "sz",
  "area": "nanshan"
}

2021/06/xx xx:30:24 main4.go:34:  输出整个json this :  {
  "author":{"name":"xiaomotong", "nick": "xiaozhu"},
  "age": 18,
  "hobby": ["play", "eat", "drink"],
  "love.music": "one day",
  "location": [
    {"province": "gd", "city":"gz", "area": "huangpu"},
    {"province": "gd", "city":"sz", "area": "nanshan"},
  ]
}

2021/06/xx xx:30:24 main4.go:39: 扁平化 this :  ["小猪1","小猪2", "小猪3"]

哈哈,有没有觉得很简单嘞,

咱们还可以自定义修饰符哦,一起来瞅瞅

使用函数 gjson.AddModifier ,添加我们自定义修饰符的实现方式,具体可以看看下面这个

func main() {
   gjson.AddModifier("myTo", func(json, arg string) string {
       // 将字符串修改为大写
      if arg == "title" {
         return strings.ToTitle(json)
      }

      return json
   })

   const json = `{"children": ["hello", "world", "xiaomotong"]}`
   fmt.Println(gjson.Get(json, "children|@myTo:title"))
}

结果如下:

["HELLO", "WORLD", "XIAOMOTONG"]

AddModifier将自定义修饰命令绑定到GJSON语法,这个操作不是线程安全的,应该在使用所有其他gjson函数之前执行。

// AddModifier binds a custom modifier command to the GJSON syntax.
// This operation is not thread safe and should be executed prior to
// using all other gjson function.
func AddModifier(name string, fn func(json, arg string) string) {
    modifiers[name] = fn
}
  • 分享了 jsongjson分别代表什么
  • gjson 的简单使用
  • gjson 校验,获取值
  • gjsonjson 行
  • gjson的键路径匹配规则
  • gjson的修饰符和自定义修饰符

欢迎点赞,关注,收藏

朋友们,你的支持和鼓励,是我坚持分享,提高质量的动力

好了,本次就到这里,下一次 GO 权限管理之 Casbin

技术是开放的,我们的心态,更应是开放的。拥抱变化,向阳而生,努力向前行。

我是小魔童哪吒,欢迎点赞关注收藏,下次见~


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK