C#实现自己的Json解析器(LALR(1)+miniDFA)

news/2025/3/19 12:56:26/文章来源:https://www.cnblogs.com/bitzhuwei/p/18779851

C#实现自己的Json解析器(LALR(1)+miniDFA)

Json是一个用处广泛、文法简单的数据格式。本文介绍如何用bitParser(拥有自己的解析器(C#实现LALR(1)语法解析器和miniDFA词法分析器的生成器))迅速实现一个简单高效的Json解析器。

读者可在(JsonFormat)查看、下载完整代码。

Json格式的文法

我们可以在(https://ecma-international.org/wp-content/uploads/ECMA-404_2nd_edition_december_2017.pdf )找到Json格式的详细说明。据此,可得如下文法:

// Json grammar according to ECMA-404 2nd Edition / December 2017
Json = Object | Array ;
Object = '{' '}' | '{' Members '}' ;
Array = '[' ']' | '[' Elements ']' ;
Members = Members ',' Member | Member ;
Elements = Elements ',' Element | Element ;
Member = 'string' ':' Value ;
Element = Value ;
Value = 'null' | 'true' | 'false' | 'number' | 'string'| Object | Array ;%%"([^"\\\u0000-\u001F]|\\["\\/bfnrt]|\\u[0-9A-Fa-f]{4})*"%% 'string'
%%[-]?(0|[1-9][0-9]*)([.][0-9]+)?([eE][+-]?[0-9]+)?%% 'number'

实际上这个文法是我用AI写出来后再整理成的。

此文法说明:

  1. 一个Json要么是一个Object,要么是一个Array

  2. 一个Object包含0-多个键值对("key" : value),用{ }括起来。

  3. 一个Array包含0-多个value,用[ ]括起来。

  4. 一个value有如下几种类型:nulltruefalsenumberstringObjectArray

其中:

nulltruefalse就是字面意思,因而可以省略不写。如果要在文法中显式地书写,就是这样:

%%null%% 'null'
%%true%% 'true'
%%false%% 'false'

{}[],:也都是字面意思,因而可以省略不写。如果要在文法中显式地书写,就是这样:

%%null%% 'null'
%%null%% 'null'
%%null%% 'null'
%%null%% 'null'
%%null%% 'null'
%%null%% 'null'

number可由下图描述:

image

图上直观地说明了number这个token的正则表达式由4个依次排列的部分组成:

[-]?  (0|[1-9][0-9]*)  ([.][0-9]+)?  ([eE][+-]?[0-9]+)?

string可由下图描述:

image

图上直观地说明了string这个token的正则表达式是用"包裹起来的某些字符或转义字符:

" (  [^"\\\u0000-\u001F]  |  \\["\\/bfnrt]  |  \\u[0-9A-Fa-f]{4}  )*  "
/*
实际含义为:
非"、非\、非控制字符(\u0000-\u001F)
\"、\\、\/、\b、\f、\n、\r、\t
\uNNNN
*/

Value = Object | Array;说明Json中的数据是可以嵌套的。

将此文法作为输入,提供给bitParser,就可以一键生成下述章节介绍的Json解析器代码和文档了。

生成的词法分析器代码

image

DFA

image

DFA文件夹下是依据确定的有限自动机原理生成的词法分析器的全部词法状态。

初始状态lexicalState0
。。。

DFA文件夹下的实现是最初的也是最直观的实现。它已经被更高效的实现方式取代了。现在此文件夹仅供学习参考用。因此我将C#文件的扩展名cs改为cs_,以免其被编译。

miniDFA

image

miniDFA文件夹下是依据Hopcroft算法得到的最小化的有限自动机的全部词法状态。它与DFA的区别仅在于词法状态数量可能减少了。

它是第二个实现,它也已经被更高效的实现方式取代了。现在此文件夹仅供学习参考用。因此我将C#文件的扩展名cs改为cs_,以免其被编译。

tableDFA

image

tableDFA文件夹下是二维数组形式(ElseIf[][])的miniDFA。它与miniDFA表示的内容相同,区别在于:它用一个数组(ElseIf[])表示一个词法状态,而miniDFA用一个函数(Action<LexicalContext, char, CurrentStateWrap>)表示一个词法状态。这样可以减少内存占用。

二维数组形式的miniDFA
。。。

它是第三个实现,它也已经被更高效的实现方式取代了。现在此文件夹仅供学习参考用。因此我将C#文件的扩展名cs改为cs_,以免其被编译。

Json.LexiTable.gen.bin

image

这是将二维数组形式(ElseIf[][])的miniDFA写入了一个二进制文件。加载JsonParser时,读取此文件即可得到二维数组形式(ElseIf[][])的miniDFA。这就不需要将整个ElseIf[][]硬编码到源代码中了,从而进一步减少了内存占用。

为了方便调试、参考,我为其准备了对应的文本格式:

Json.LexiTable.gen.txt

它是第四个实现,这是目前使用的实现方式。为了加载路径上的方便,我将其从Json.gen\LexicalAnalyzer文件夹挪到了Json.gen文件夹下。

Json.LexicalScripts.gen.cs

这是各个词法分析状态都可能用到的函数,包括3类:BeginExtendAccept。其作用是:记录一个token的起始位置(Begin)和结束位置(Extend),设置其类型、行数、列数等信息,将其加入List<Token> tokens数组(Accept)。

Json.LexicalScripts.gen.cs

Json.LexicalReservedWords.gen.cs

这里记录了Json文法的全部保留字(任何编程语言中的keyword),也就是{}[],:nulltruefalse这些。显然这是辅助的东西,不必在意。

Json.LexicalReservedWords.gen.cs

README.gen.md

这是词法分析器的说明文档,用mermaid画出了各个token的状态机和整个文法的总状态机,如下图所示。

image

我知道你们看不清。我也看不清。找个大屏幕直接看README.gen.md文件吧。

生成的语法分析器代码

image

Dicitonary<int, LRParseAction>

Json.Dict.LALR(1).gen.cs_是LALR(1)的语法分析状态机,每个语法状态都是一个Dicitonary<int, LRParseAction>对象。

Json.Dict.LALR(1).gen.cs_

另外3个Json.Dict.*.gen.cs_分别是LR(0)、SLR(1)、LR(1)的语法分析状态机,不再赘述。

这是最初的也是最直观的实现,它已经被更高效的实现方式取代了。现在此文件夹仅供学习参考用。因此我将C#文件的扩展名cs改为cs_,以免其被编译。

int[]+LRParseAction[]

Json.Table.LALR(1).gen.cs_是LALR(1)的语法分析状态机,每个语法状态都是一个包含int[]LRParseAction[]的对象。这里的每个int[t]LRParseAction[t]合起来就代替了Dictionary<int, LRParseAction>对象的一个键值对(key/value),从而减少了内存占用,也稍微提升了运行效率。

Json.Table.LALR(1).gen.cs_

另外4个Json.Dict.*.gen.cs_分别是LL(1)、LR(0)、SLR(1)、LR(1)的语法分析状态机,不再赘述。

它是第二个实现,它已经被更高效的实现方式取代了。现在此文件夹仅供学习参考用。因此我将C#文件的扩展名cs改为cs_,以免其被编译。

Json.Table.*.gen.bin

与词法分析器类似,这是将数组形式(int[]+LRParseAction[])的语法分析表写入了一个二进制文件。加载JsonParser时,读取此文件即可得到数组形式(int[]+LRParseAction[])的语法分析表。这就不需要将整个语法分析表硬编码到源代码中了,从而进一步减少了内存占用。

为了方便调试、参考,我为其准备了对应的文本格式,例如LALR(1)的语法分析表:

Json.Table.LALR(1).gen.txt

它是第三个实现,这是目前使用的实现方式。为了加载路径上的方便,我将其从Json.gen\SyntaxParser文件夹挪到了Json.gen文件夹下。

image

生成的提取器代码

所谓提取,就是按后序优先遍历的顺序访问语法树的各个结点,在访问时提取出语义信息。

例如,{ "a": 0.3, "b": true, "a": "again" }的语法树是这样的:

R[0] Json = Object ;⛪T[0->12]└─R[3] Object = '{' Members '}' ;⛪T[0->12]├─T[0]='{' {├─R[6] Members = Members ',' Member ;⛪T[1->11]│  ├─R[6] Members = Members ',' Member ;⛪T[1->7]│  │  ├─R[7] Members = Member ;⛪T[1->3]│  │  │  └─R[10] Member = 'string' ':' Value ;⛪T[1->3]│  │  │     ├─T[1]='string' "a"│  │  │     ├─T[2]=':' :│  │  │     └─R[15] Value = 'number' ;⛪T[3]│  │  │        └─T[3]='number' 0.3│  │  ├─T[4]=',' ,│  │  └─R[10] Member = 'string' ':' Value ;⛪T[5->7]│  │     ├─T[5]='string' "b"│  │     ├─T[6]=':' :│  │     └─R[13] Value = 'true' ;⛪T[7]│  │        └─T[7]='true' true│  ├─T[8]=',' ,│  └─R[10] Member = 'string' ':' Value ;⛪T[9->11]│     ├─T[9]='string' "a"│     ├─T[10]=':' :│     └─R[16] Value = 'string' ;⛪T[11]│        └─T[11]='string' "again"└─T[12]='}' }

按后序优先遍历的顺序,提取器会依次访问T[0]T[1]T[2]T[3]并将其入栈,然后访问R[15] Value = 'number' ;⛪T[3],此时应当:

// [15] Value = 'number' ;
var r0 = (Token)context.rightStack.Pop();// T[3]出栈
var left = new JsonValue(JsonValue.Kind.Number, r0.value);
context.rightStack.Push(left);// Value入栈

之后会访问R[10] Member = 'string' ':' Value ;⛪T[1->3],此时应当:

// [10] Member = 'string' ':' Value ;
var r0 = (JsonValue)context.rightStack.Pop();// Value出栈
var r1 = (Token)context.rightStack.Pop();// :出栈
var r2 = (Token)context.rightStack.Pop();// string出栈
var left = new JsonMember(key: r2.value, value: r0);
context.rightStack.Push(left);// Member入栈

这样逐步地访问到根节点R[0] Json = Object ;⛪T[0->12],此时应当:

var r0 = (List<JsonMember>)context.rightStack.Pop();// Member列表出栈
var left = new Json(r0);
context.rightStack.Push(left);// Json入栈

这样,语法树访问完毕了,栈context.rightStack中有且只有1个对象,即最终的Json。此时应当:

// [-1] Json' = Json ;
context.result = (Json)context.rightStack.Pop();
提取器的完整代码InitializeExtractorItems

不同的应用场景会要求不同的语义信息,因而一键生成的提取器代码不是这样的,而是仅仅将语法树压平了,并且保留了尽可能多的源代码信息,如下所示:

一键生成的提取器代码

这是步子最小的保守式代码,程序员可以在此基础上继续开发,也可以自行编写访问各类型结点的提取动作。本应用场景的目的是尽可能高效地解析Json文本文件,因而完全自行编写了访问各类型结点的提取动作。

测试

测试用例0

测试用例1

测试用例2

测试用例3

测试用例4

测试用例5

上述测试用例都能够被JsonParser正确解析,也可以在(https://jsonlint.com/)验证。

End

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/901474.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

R语言中绘制火山图

001、# 生成随机数据 set.seed(123) # 设置种子以便结果可重复 genes <- paste("Gene", 1:1000) # 基因名称 logFC <- rnorm(1000, mean = 0, sd = 2) # log2 fold change,均值为0,标准差为2的正态分布 pvalue <- runif(1000, min = 0, max = 1) # p值…

Windows 系统安装 Python3.7 、3.8、3.9、3.10、 3.11 最新版,附带相应程序。

在 Windows 系统上下载和安装 Python 的教程: 下是 Python 3.7 到 3.11 每个版本在 Windows 64 位系统下最后更新版本的直接下载地址。 其他版本访问Python 官方网站。 Python 3.7.9此版本为 Python 3.7 系列最后更新版本,下载地址:python-3.7.9-amd64.exePython 3.8.10它是…

库存持有成本的底层运算逻辑是什么?

你可能会觉得,库存持有成本这个概念听起来有点抽象: 库存不就是放在仓库里待着吗,怎么还会“花钱”? 其实,库存持有成本就是你把货物放在仓库里,背后所产生的一系列费用。 这些费用包括仓库租金、保险费用、商品的折旧损耗,还有库存过期的风险等等。 今天,我们就来拆解…

Windows部署deepseek R1训练数据后通过AnythingLLM当服务器创建问答页面

如果要了解Windows部署Ollama 、deepseek R1请看我上一篇内容。 这是接上一篇的。 AnythingLLM是一个开源的全栈AI客户端,支持本地部署和API集成。它可以将任何文档或内容转化为上下文,供各种语言模型(LLM)在对话中使用。以下是关于Windows环境下使用AnythingLLM API的一些…

变量数据类型流程控制

常量变量 常量 1.概述:在代码的运行过程中,值不会发生改变的数据 2.分类:整数常量:所有整数小数常量:所有带小数点的2.5 1.5 2.0字符常量:带单引号的 单引号中必须有且只能有一个内容1(算) 11(不算) (不算) a1(不算) (算) (两个空格不算)写一个tab键(算) 字符…

Vue2-自定义创建项目、ESLint、Vuex及综合案例购物车

Vue2自定义创建项目 基于VueCli自定义创建项目架子 步骤:安装VueCLI脚手架npm i @vue/cli -g 可以通过vue --version 判断是否下载VueCLI在某个文件夹中创建vue项目vue create 项目名称 (项目名中不能包含大写字母)选择Manually select features选择Babel(语法降级)、Ro…

yum install -y devtoolset-8-gcc*

如果执行结果为上面这个结果的话,需要执行以下操作 yum install centos-release-scl*修改CentOS-SCLo-scl.repo文件 baseurl=https://mirrors.aliyun.com/centos/7/sclo/x86_64/rh/ 和 gpgcheck=0修改CentOS-SCLo-scl-rh.repo文件和上面一样查看 [root@iZbp153shsqfoddljmkit4…

几个技巧,教你去除文章的 AI 味!

给大家分享一些快速去除文章 AI 味的小技巧,有些是网上被分享过的,也有些是我个人的经验。学会之后,无论是写工作文案、毕业设计、自媒体文章,还是平时生活中写写好评,都是非常轻松的。最近有不少朋友在利用 AI 写毕业设计论文,几秒钟一篇文章就刷出来的,爽的飞起。 结果…

Sci Chart中的XyDataSeries与UniformXyDataSeries

在 SciChart 中,XyDataSeries 和 UniformXyDataSeries 是两种用于处理数据序列的核心类,主要差异体现在数据存储方式、性能优化及适用场景上。 以下是具体对比: 1. 数据存储与结构差异 **XyDataSeries<TX, TY>** 需要同时存储 X 和 Y 值的完整坐标对。例如,对于每个数…

强化学习基础_基于价值的强化学习

Action-Value Functions 动作价值函数 折扣回报(Discounted Return) 折扣回报 Ut 是从时间步 t 开始的累积奖励,公式为: Rt 是在时间步 t 获得的奖励。γ 是折扣因子(0<γ<1),用于减少未来奖励的权重。这是因为未来的奖励通常不如当前奖励重要,例如在金融领域,未…

USB杂谈

一、USB控制器 OHCI 1.0、1.1控制器 UHCI:1.0、1.1控制器 EHCI 2.0控制器 XHCI 3.0控制器 EHCI 2.0控制器 HID:人机交互接口,鼠标、手柄 、键盘、扫描枪USB协议中对集线器的层数是有限制的,USB1.1规定最多为5层,USB2.0规定最多为7层。 理论上,一个USB主控制器最多可接127个…

2025年3月中国数据库排行榜:PolarDB夺魁傲群雄,GoldenDB晋位入三强

2025年3月排行榜解读出炉,榜单前四现波动,PolarDB时隔半年重返榜首、GoldenDB进入前三,此外更有一些新星产品表现亮眼!欢迎阅读、一起盘点~阳春三月,万物复苏。2025年3月中国数据库流行度排行榜的发布,不仅展现了中国数据库企业在技术创新、生态建设和应用深化方面的显著…