go语言用八百行代码实现一个JSON解析器
来源:脚本之家
时间:2022-12-31 07:40:47 254浏览 收藏
本篇文章主要是结合我之前面试的各种经历和实战开发中遇到的问题解决经验整理的,希望这篇《go语言用八百行代码实现一个JSON解析器》对你有很大帮助!欢迎收藏,分享给更多的需要的朋友学习~
一次无意间看到有人提起 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解析器》的内容了。个人认为,基础知识的学习和巩固,是为了更好的将其运用到项目中,欢迎关注golang学习网公众号,带你了解更多关于golang的知识点!
-
109 收藏
-
235 收藏
-
191 收藏
-
149 收藏
-
179 收藏
-
438 收藏
-
280 收藏
-
181 收藏
-
371 收藏
-
236 收藏
-
416 收藏
-
- 前端进阶之JavaScript设计模式
- 设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
- 立即学习 542次学习
-
- GO语言核心编程课程
- 本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
- 立即学习 507次学习
-
- 简单聊聊mysql8与网络通信
- 如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
- 立即学习 497次学习
-
- JavaScript正则表达式基础与实战
- 在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
- 立即学习 487次学习
-
- 从零制作响应式网站—Grid布局
- 本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
- 立即学习 484次学习
-
- 失眠的板凳
- 赞 👍👍,一直没懂这个问题,但其实工作中常常有遇到...不过今天到这,看完之后很有帮助,总算是懂了,感谢博主分享文章!
- 2023-01-15 18:27:15
-
- 壮观的大门
- 太全面了,mark,感谢大佬的这篇技术文章,我会继续支持!
- 2023-01-11 16:47:36
-
- 开放的铃铛
- 这篇文章内容出现的刚刚好,细节满满,真优秀,收藏了,关注师傅了!希望师傅能多写Golang相关的文章。
- 2023-01-10 19:28:50
-
- 体贴的香菇
- 感谢大佬分享,一直没懂这个问题,但其实工作中常常有遇到...不过今天到这,帮助很大,总算是懂了,感谢老哥分享文章内容!
- 2023-01-10 12:56:05
-
- 冷傲的中心
- 这篇博文太及时了,太详细了,很棒,收藏了,关注up主了!希望up主能多写Golang相关的文章。
- 2023-01-10 02:31:43
-
- 糊涂的老鼠
- 太全面了,mark,感谢大佬的这篇博文,我会继续支持!
- 2023-01-03 01:30:55
-
- 能干的红牛
- 这篇博文太及时了,作者大大加油!
- 2023-01-02 18:29:42