几百行代码实现一个 JSON 解析器
前言
之前在写 gscript时我就在想有没有利用编译原理实现一个更实际工具?毕竟真写一个语言的难度不低,并且也很难真的应用起来。
一次无意间看到有人提起 JSON
解析器,这类工具充斥着我们的日常开发,运用非常广泛。
以前我也有思考过它是如何实现的,过程中一旦和编译原理扯上关系就不由自主的劝退了;但经过这段时间的实践我发现实现一个 JSON
解析器似乎也不困难,只是运用到了编译原理前端的部分知识就完全足够了。
得益于 JSON
的轻量级,同时语法也很简单,所以核心代码大概只用了 800 行便实现了一个语法完善的 JSON
解析器。
首先还是来看看效果:
import "github.com/crossoverJie/gjson"
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 := gjson.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 一下就很容易理解:
https://github.com/crossoverJie/gjson/blob/main/token_test.go
以这段 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/gjson/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
的四则运算:
gjson.Get("glossary.age+long*(a.b+a.c)")
目前我貌似没有发现有类似的库实现了这个功能,后面真的完成后应该会很有意思,感兴趣的朋友请持续关注。
源码:
https://github.com/crossoverJie/gjson
几百行代码实现一个 JSON 解析器的更多相关文章
- 一起写一个JSON解析器
[本篇博文会介绍JSON解析的原理与实现,并一步一步写出来一个简单但实用的JSON解析器,项目地址:SimpleJSON.希望通过这篇博文,能让我们以后与JSON打交道时更加得心应手.由于个人水平有限 ...
- 如何编写一个JSON解析器
编写一个JSON解析器实际上就是一个函数,它的输入是一个表示JSON的字符串,输出是结构化的对应到语言本身的数据结构. 和XML相比,JSON本身结构非常简单,并且仅有几种数据类型,以Java为例,对 ...
- 一个JSON解析器
来源 <JavaScript语言精粹(修订版)> 代码 <!DOCTYPE html> <html> <head> <meta charset=& ...
- 自己动手实现一个简单的JSON解析器
1. 背景 JSON(JavaScript Object Notation) 是一种轻量级的数据交换格式.相对于另一种数据交换格式 XML,JSON 有着诸多优点.比如易读性更好,占用空间更少等.在 ...
- 用c#自己实现一个简单的JSON解析器
一.JSON格式介绍 JSON(JavaScript Object Notation) 是一种轻量级的数据交换格式.相对于另一种数据交换格式 XML,JSON 有着很多优点.例如易读性更好,占用空间更 ...
- 手写Json解析器学习心得
一. 介绍 一周前,老同学阿立给我转了一篇知乎回答,答主说检验一门语言是否掌握的标准是实现一个Json解析器,网易游戏过去的Python入门培训作业之一就是五天时间实现一个Json解析器. 知乎回答- ...
- 面试题|手写JSON解析器
这周的 Cassidoo 的每周简讯有这么一个面试题:: 写一个函数,这个函数接收一个正确的 JSON 字符串并将其转化为一个对象(或字典,映射等,这取决于你选择的语言).示例输入: fakePars ...
- java 写一个JSON解析的工具类
上面是一个标准的json的响应内容截图,第一个红圈”per_page”是一个json对象,我们可以根据”per_page”来找到对应值是3,而第二个红圈“data”是一个JSON数组,而不是对象,不能 ...
- 一个简单的json解析器
实现一个简单地json解析器. 两部分组成,词法分析.语法分析 词法分析 package com.mahuan.json; import java.util.LinkedList; import ja ...
随机推荐
- 1.5 万字 + 40 张图解 HTTP 常见面试题
作者:小林coding 图解计算机基础网站:https://xiaolincoding.com 大家好,我是小林,我最开始写的第一篇图解文章就是这篇: 那时候我也就不到 100 读者,如今这篇阅读都快 ...
- MySQL学习day3随笔
索引在数据量不大的时候体现不出来,数据很多的时候区别明显 1 select * from app_user where `name`='用户9999';-- 0.053 sec 2 select * ...
- 图片杂乱无章、分享麻烦?HMS Core图片分类服务教你快速筛选、分类、整合相册
如今手机摄影越来越方便,随手一拍就能记录美好生活.但照片越多,整理越麻烦,有的时候我们想对照片进行二次加工.分享,需要不停翻找相册.HMS Core机器学习服务(ML Kit)提供了图片分类服务,方便 ...
- Mysql、Oracle锁表处理
MySql解锁方式 1 # 1. 查看当前数据库锁表的情况 2 SELECT * FROM information_schema.INNODB_TRX; 3 # 2. 杀掉查询结果中锁表的trx_my ...
- switch 和 if else if else 有什么区别
1. 一般情况下,它们两个语句可以相互替换 2. switch..case语句通常处理case为比较确定值的情况,而if...else...语句更加灵活,常用于范围判断(大于.等于某个范围) 3. ...
- Git&Gitee
目录 Git Git介绍 安装 命令学习 Git的作用 git与svn比较 Git,GitHub,GitLab,Gitee Git工作流程 Git常用命令 在仓库目录终端下 - 设置全局用户 在仓库目 ...
- Abp集成HangFire
简要说明 后台作业在系统开发的过程当中,是比较常用的功能.因为总是有一些长耗时的任务,而这些任务我们不是立即响应的,例如 Excel 文档导入.批量发送短信通知等. ABP vNext 提供了后台作业 ...
- 小程序扫码、上传图片、css时间轴
de <!-- 导航 --> <view class="navSec flexBox"> <text class="navItem {{ s ...
- Sentinel Dashboard 规则 持久化到Nacos
本篇文章基于sentinel1.8.4版本进行改造的.本篇主要记录改造步骤 1.下载源码 https://github.com/alibaba/Sentinel 2.打开下载的sentinel,到se ...
- docker 1.1 介绍和安装
1.docker是什么? Docker 是一个开源的应用容器引擎,让开发者可以打包他们的应用以及依赖包到一个可移植的镜像中,然后发布到任何流行的 Linux或Windows操作系统的机器上,也可以实现 ...