go语言用八百行代码实现一个JSON解析器

目录
  • 前言
  • 实现原理
  • 词法分析
    • 提前检查
  • 生成 JSONObject 树
  • 总结

前言

之前在写 gscript时我就在想有没有利用编译原理实现一个更实际工具?毕竟真写一个语言的难度不低,并且也很难真的应用起来。

一次无意间看到有人提起 JSON 解析器,这类工具充斥着我们的日常开发,运用非常广泛。

以前我也有思考过它是如何实现的,过程中一旦和编译原理扯上关系就不由自主的劝退了;但经过这段时间的实践我发现实现一个 JSON 解析器似乎也不困难,只是运用到了编译原理前端的部分知识就完全足够了。

得益于 JSON 的轻量级,同时语法也很简单,所以核心代码大概只用了 800 行便实现了一个语法完善的 JSON 解析器。

首先还是来看看效果:

import "github.com/crossoverJie/xjson"
func TestJson(t *testing.T) {
    str := `{
   "glossary": {
       "title": "example glossary",
        "age":1,
        "long":99.99,
        "GlossDiv": {
           "title": "S",
            "GlossList": {
               "GlossEntry": {
                   "ID": "SGML",
                    "SortAs": "SGML",
                    "GlossTerm": "Standard Generalized Markup Language",
                    "Acronym": "SGML",
                    "Abbrev": "ISO 8879:1986",
                    "GlossDef": {
                       "para": "A meta-markup language, used to create markup languages such as DocBook.",
                        "GlossSeeAlso": ["GML", "XML", true, null]
                   },
                    "GlossSee": "markup"
               }
           }
       }
   }
}`
    decode, err := xjson.Decode(str)
    assert.Nil(t, err)
    fmt.Println(decode)
    v := decode.(map[string]interface{})
    glossary := v["glossary"].(map[string]interface{})
    assert.Equal(t, glossary["title"], "example glossary")
    assert.Equal(t, glossary["age"], 1)
    assert.Equal(t, glossary["long"], 99.99)
    glossDiv := glossary["GlossDiv"].(map[string]interface{})
    assert.Equal(t, glossDiv["title"], "S")
    glossList := glossDiv["GlossList"].(map[string]interface{})
    glossEntry := glossList["GlossEntry"].(map[string]interface{})
    assert.Equal(t, glossEntry["ID"], "SGML")
    assert.Equal(t, glossEntry["SortAs"], "SGML")
    assert.Equal(t, glossEntry["GlossTerm"], "Standard Generalized Markup Language")
    assert.Equal(t, glossEntry["Acronym"], "SGML")
    assert.Equal(t, glossEntry["Abbrev"], "ISO 8879:1986")
    glossDef := glossEntry["GlossDef"].(map[string]interface{})
    assert.Equal(t, glossDef["para"], "A meta-markup language, used to create markup languages such as DocBook.")
    glossSeeAlso := glossDef["GlossSeeAlso"].(*[]interface{})
    assert.Equal(t, (*glossSeeAlso)[0], "GML")
    assert.Equal(t, (*glossSeeAlso)[1], "XML")
    assert.Equal(t, (*glossSeeAlso)[2], true)
    assert.Equal(t, (*glossSeeAlso)[3], "")
    assert.Equal(t, glossEntry["GlossSee"], "markup")
}

从这个用例中可以看到支持字符串、布尔值、浮点、整形、数组以及各种嵌套关系。

实现原理

这里简要说明一下实现原理,本质上就是两步:

  • 词法解析:根据原始输入的 JSON 字符串解析出 token,也就是类似于 "{" "obj" "age" "1" "[" "]" 这样的标识符,只是要给这类标识符分类。
  • 根据生成的一组 token 集合,以流的方式进行读取,最终可以生成图中的树状结构,也就是一个 JSONObject 。

下面来重点看看这两个步骤具体做了哪些事情。

词法分析

BeginObject  {
String  "name"
SepColon  :
String  "cj"
SepComma  ,
String  "object"
SepColon  :
BeginObject  {
String  "age"
SepColon  :
Number  10
SepComma  ,
String  "sex"
SepColon  :
String  "girl"
EndObject  }
SepComma  ,
String  "list"
SepColon  :
BeginArray  [

其实词法解析就是构建一个有限自动机的过程(DFA),目的是可以生成这样的集合(token),只是我们需要将这些 token进行分类以便后续做语法分析的时候进行处理。

比如 "{" 这样的左花括号就是一个 BeginObject 代表一个对象声明的开始,而 "}" 则是 EndObject 代表一个对象的结束。

其中 "name" 这样的就被认为是 String 字符串,以此类推 "[" 代表 BeginArray

这里我一共定义以下几种 token 类型:

type Token string
const (
    Init        Token = "Init"
    BeginObject       = "BeginObject"
    EndObject         = "EndObject"
    BeginArray        = "BeginArray"
    EndArray          = "EndArray"
    Null              = "Null"
    Null1             = "Null1"
    Null2             = "Null2"
    Null3             = "Null3"
    Number            = "Number"
    Float             = "Float"
    BeginString       = "BeginString"
    EndString         = "EndString"
    String            = "String"
    True              = "True"
    True1             = "True1"
    True2             = "True2"
    True3             = "True3"
    False             = "False"
    False1            = "False1"
    False2            = "False2"
    False3            = "False3"
    False4            = "False4"
    // SepColon :
    SepColon = "SepColon"
    // SepComma ,
    SepComma = "SepComma"
    EndJson  = "EndJson"
)

其中可以看到 true/false/null 会有多个类型,这点先忽略,后续会解释。

以这段 JSON 为例:{"age":1},它的状态扭转如下图:

总的来说就是依次遍历字符串,然后更新一个全局状态,根据该状态的值进行不同的操作。

部分代码如下:

感兴趣的朋友可以跑跑单例 debug 一下就很容易理解:

以这段 JSON 为例:

func TestInitStatus(t *testing.T) {
    str := `{"name":"cj", "age":10}`
    tokenize, err := Tokenize(str)
    assert.Nil(t, err)
    for _, tokenType := range tokenize {
        fmt.Printf("%s  %s\n", tokenType.T, tokenType.Value)
    }
}

最终生成的 token 集合如下:

BeginObject  {
String  "name"
SepColon  :
String  "cj"
SepComma  ,
String  "age"
SepColon  :
Number  10
EndObject  }

提前检查

由于 JSON 的语法简单,一些规则甚至在词法规则中就能校验。

举个例子: JSON 中允许 null 值,当我们字符串中存在 nu nul 这类不匹配 null 的值时,就可以提前抛出异常。

比如当检测到第一个字符串为 n 时,那后续的必须为 u->l->l 不然就抛出异常。

浮点数同理,当一个数值中存在多个 . 点时,依然需要抛出异常。

这也是前文提到 true/false/null 这些类型需要有多个中间状态的原因。

生成 JSONObject 树

在讨论生成 JSONObject 树之前我们先来看这么一个问题,给定一个括号集合,判断是否合法。

  • [<()>] 这样是合法的。
  • [<()>) 而这样是不合法的。

如何实现呢?其实也很简单,只需要利用栈就能完成,如下图所示:

利用栈的特性,依次遍历数据,遇到是左边的符号就入栈,当遇到是右符号时就与栈顶数据匹配,能匹配上就出栈。

当匹配不上时则说明格式错误,数据遍历完毕后如果栈为空时说明数据合法。

其实仔细观察 JSON 的语法也是类似的:

{
    "name": "cj",
    "object": {
        "age": 10,
        "sex": "girl"
    },
    "list": [
        {
            "1": "a"
        },
        {
            "2": "b"
        }
    ]
}

BeginObject:{ 与 EndObject:} 一定是成对出现的,中间如论怎么嵌套也是成对的。 而对于 "age":10 这样的数据,: 冒号后也得有数据进行匹配,不然就是非法格式。

所以基于刚才的括号匹配原理,我们也能用类似的方法来解析 token 集合。

我们也需要创建一个栈,当遇到 BeginObject 时就入栈一个 Map,当遇到一个 String 键时也将该值入栈。

当遇到 value 时,就将出栈一个 key,同时将数据写入当前栈顶的 map 中。

当然在遍历 token 的过程中也需要一个全局状态,所以这里也是一个有限状态机

举个例子:当我们遍历到 Token 类型为 String,值为 "name" 时,预期下一个 token 应当是 :冒号;

所以我们得将当前的 status 记录为 StatusColon,一旦后续解析到 token 为 SepColon 时,就需要判断当前的 status 是否为 StatusColon ,如果不是则说明语法错误,就可以抛出异常。

同时值得注意的是这里的 status 其实是一个集合,因为下一个状态可能是多种情况。

{"e":[1,[2,3],{"d":{"f":"f"}}]} 比如当我们解析到一个 SepColon 冒号时,后续的状态可能是 value 或 BeginObject { 或 BeginArray [

因此这里就得把这三种情况都考虑到,其他的以此类推。

具体解析过程可以参考源码:

https://github.com/crossoverJie/xjson/blob/main/parse.go

虽然是借助一个栈结构就能将 JSON 解析完毕,不知道大家发现一个问题没有: 这样非常容易遗漏规则,比如刚才提到的一个冒号后面就有三种情况,而一个 BeginArray 后甚至有四种情况

StatusArrayValue, StatusBeginArray, StatusBeginObject, StatusEndArray

这样的代码读起来也不是很直观,同时容易遗漏语法,只能出现问题再进行修复。

既然提到了问题那自然也有相应的解决方案,其实就是语法分析中常见的递归下降算法。

我们只需要根据 JSON 的文法定义,递归的写出算法即可,这样代码阅读起来非常清晰,同时也不会遗漏规则。

完整的 JSON 语法查看这里:

https://github.com/antlr/grammars-v4/blob/master/json/JSON.g4

我也预计将下个版本改为递归下降算法来实现。

总结

当目前为止其实只是实现了一个非常基础的 JSON 解析,也没有做性能优化,和官方的 JSON 包对比性能差的不是一星半点。

cpu: Intel(R) Core(TM) i7-9750H CPU @ 2.60GHz
BenchmarkJsonDecode-12            372298             15506 ns/op             512 B/op         12 allocs/op
BenchmarkDecode-12                141482             43516 ns/op           30589 B/op        962 allocs/op
PASS

同时还有一些基础功能没有实现,比如将解析后的 JSONObject 可以反射生成自定义的 Struct,以及我最终想实现的支持 JSON 的四则运算:

xjson.Get("glossary.age+long*(a.b+a.c)")

目前我貌似没有发现有类似的库实现了这个功能,后面真的完成后应该会很有意思,感兴趣的朋友请持续关注。

源码:https://github.com/crossoverJie/xjson

以上就是go语言用八百行代码实现一个JSON解析器的详细内容,更多关于go语言实现JSON解析器的资料请关注我们其它相关文章!

(0)

相关推荐

  • Go语言JSON解析器gjson使用方法详解

    目录 gjson 安装 使用 gjson GJSON 是一个Go包,它提供了一种从json文档中获取值的快速简单的方法.它具有单行检索.点符号路径.迭代和解析 json 行等功能. 还可以查看SJSON以修改 json,以及JJ命令行工具. 本自述文件是如何使用 GJSON 的快速概述,有关更多信息,请查看GJSON 语法. github 的地址在这里. 安装 安装gjson,使用的是go传统的安装方法: go install github.com/tidwall/gjson@latest 在文

  • 仅用500行Python代码实现一个英文解析器的教程

    语法分析器描述了一个句子的语法结构,用来帮助其他的应用进行推理.自然语言引入了很多意外的歧义,以我们对世界的了解可以迅速地发现这些歧义.举一个我很喜欢的例子: 正确的解析是连接"with"和"pizza",而错误的解析将"with"和"eat"联系在了一起: 过去的一些年,自然语言处理(NLP)社区在语法分析方面取得了很大的进展.现在,小小的 Python 实现可能比广泛应用的 Stanford 解析器表现得更出色. 文章剩下

  • C语言百行代码绘制圣诞水晶球

    目录 序 项目代码 总结 序 我爱你,不是因为你是一个怎样的人,而是因为我喜欢与你在一起时的感觉. 嗨!这里是狐狸~~ 今天就是圣诞节了,再过一个星期就是2022年了,最近总是感觉伤感,有些事情就是比想象中来的快一些,希望大家都可以把握2021年最后的时间,不留遗憾吧,后天圣诞节,今天再教大家一个圣诞项目吧,圣诞水晶球,今天这个呢代码不多,但难度会有点,因为这个涉及桌面,就是可以在桌面实现,希望大家可以认真看,认真学吧. 同样,先给大家看效果吧 效果还是很不错的,再加上一个音乐,女朋友看完就马上

  • python百行代码自制电脑端网速悬浮窗的实现

    前言 看到某60的网速悬浮球有点心动,但是又不想装这个流氓软件,就自己用python加PyQt5自制了一个,实测还行,关键不占用电脑一点资源,已将软件打包,可自行下载使用. 预览 观看直播时实时网速. 文件结构 运行管理 开始运行时内存消耗18.3m,cpu,磁盘,网络不占用. 运行一天后内存稳定于6.4m,cpu,磁盘,网络不占用. 整体思路 使用psuti.net_io_counters 监控电脑网卡IO 将流量数据格式化,统计每次数据总和保存在本地<流量使用情况.txt>(这个是个缺陷,

  • C/C++百行代码实现热门游戏消消乐功能的示例代码

    游戏设计 首先我们需要使用第三方框架,这里我使用的是sfml,不会使用sfml在我的上几篇文章当中-扫雷(上)有详细的开发环境搭建介绍 首先准备图片资源 一张背景图片,一张宝石图片 窗口初始化加载图片 Texture t1; t1.loadFromFile("images/bg2.png"); 当鼠标第一次单击时,记录下位置,第二次单击又记录一下位置,如果两个小方块相邻就交换位置,如果不相邻如图c的位置则,不发生变化 判断行或列如果三张一样的图片相邻,清除一下图片,进行刷新 实列 #i

  • python百行代码实现汉服圈图片爬取

    目录 分析网站 子链接获取 获取标题和图片地址 保存图片 主函数 平时旅游的时候,在旅游景区我们经常可以看到穿各种服饰去拍照的游客,也不会刻意多关注.前两天浏览网页无意看到一个网站,看到穿汉服的女孩是真的很好看.无论是工作需要还是创作文案,把这么漂亮的图片来当作素材都是一个很好的idea.有需要,我们就爬它,爬它,爬它! 话不多说,我们下面详细介绍图片爬取. 分析网站 网址如下: https://www.aihanfu.com/zixun/tushang-1/ 这是第一页的网址,根据观察,第二页

  • Python三百行代码实现飞机大战

    目录 一. 动态效果图如下 二. 思路框架 三. Python代码实现 四. 小结 一. 动态效果图如下 先来看下飞机大战游戏最终实现的动态效果图. 二. 思路框架 plane_sprite.py文件内容 1.导入需要使用的模块 import random import pygame 在导入pygame之前,需要先使用命令: pip install pygame 进行包模块的安装 2.设置屏幕大小和刷新帧率等常量 3.创建继承于pygame.sprite.Sprite的基类GameSprite

  • Python+tkinter使用80行代码实现一个计算器实例

    本文主要探索的是使用Python+tkinter编程实现一个简单的计算器代码示例,具体如下. 闲话不说,直奔主题.建议大家跟着敲一遍代码,体会一下代码复用.字符串方法的运用和动态创建组件的妙处,然后在这个框架的基础上进行补充和发挥. 选择任何一款Python开发环境,创建一个程序文件,命名为tkinter_Calculator.pyw,然后编写下面的代码: 1)导入标准库re和tkinter,创建并简单设置应用主程序,在窗口顶部放置一个只读的文本框用来显示信息. 2)编写计算器上各种按钮的通用处

  • 100行代码实现一个vue分页组功能

    今天用vue来实现一个分页组件,总体来说,vue实现比较简单,样式部分模仿了elementUI.所有代码的源码可以再github上下载的到:下载地址 先来看一下实现效果: 点击查看效果 整体思路 我们先看一下使用到的文件的目录: 我们在 pageComponentsTest.vue 页面引入了 pageComponent.vue 分页组件.整体思路是通过 props 来达到组件的灵活通用的效果,整体语法是使用vue的VM语法. pageComponent.vue实现 首先实现一个分页,需要知道数

  • iOS使用核心的50行代码撸一个路由组件

    使用组件化是为了解耦处理,多个模块之间通过协议进行交互.而负责解析协议,找到目的控制器,或者是返回对象给调用者的这个组件就是路由组件.本文讲解如何使用核心的50行代码实现一个路由组件. 组件化和路由 路由的实现 路由注册实现 路由使用实现 客户端的使用 一些小想法 组件化和路由 之前看过挺多的关于路由管理.路由处理的文章,常常会和组件化出现在一起,一开始不知道为何路由和组件化出现在一起,后来公司的项目中使用了路由组件(他本身也是一个组件,确切的说是一个中间人或者中介者),才突然想明白了,原来如此

  • 80行代码写一个Webpack插件并发布到npm

    1. 前言 最近在学习 Webpack 相关的原理,以前只知道 Webpack 的配置方法,但并不知道其内部流程,经过一轮的学习,感觉获益良多,为了巩固学习的内容,我决定尝试自己动手写一个插件. 这个插件实现的功能比较简单: 默认清除 js 代码中的 console.log 的打印输出: 可通过传入配置,实现移除 console 的其它方法,如 console.warn.console.error 等: 2. Webpack 的构建流程以及 plugin 的原理 2.1 Webpack 构建流程

随机推荐