0x00 序言
蒟蒻作者对Parser,Tokenizer这些东西并不很了解,或者说了解程度仅足以支撑我写一个JSON的解析器.....还望各路大佬多多指点,批评。另外这是我第一次在zhihu上写文章...并不太会用这个编辑器(感觉不太好用...?)所以排版有可能略难堪请各位多多包容(笑
虽然Python已经有自带的json模块,但手动实现一个还是能在一定程度上提升对JsonParser的了解....吧。
本文是对整个Parser编写JSON解析器的简要概述。
由于该项目用于练手+休闲,所以代码并不赏心悦目。
0x01 解析器架构
效仿大佬们的方法,我们先看一下这个解析器是怎样工作的:
如图,首先我们要有原始数据。JSON的原始数据就是一个字符串;然后将字符串传到Reader中,Reader的功能有:1. 返回当前游标位置的字符;2. 向后移动游标;3. 向前移动游标。这样做是为了能够逐个字符读入原始数据并且通过上下文判断关键词作用。Tokenizer是将关键字身份和关键字所对应值存入到一个对象中提供给Parser进行进一步解析。Parser可以通过Tokenizer提供的Tokens判断语法正确性同时根据tokens的含义将数据存入Python的数据对象中。说起来比较简单,写起来难度不大但有一些复杂。这里直接用Python实现一波了...
0x02 定义JSONArray和JSONObject
这应该是写这个Parser最容易的一步了。JSONArray是个list,JSONObject是个dict。
对于JSONArray, 我们需要以下这些功能:
1. append()
2. size()
3. __getitem__() #重载list的函数
4. __str__() # 格式化输出
对于JSONObject:
1. __getitem__(key)
2. __setitem__(key, value)
3. __str__() # 格式化输出
由于比较Simple,这里就不粘贴实现了。
0x03 Reader
Reader用于接收一个字符串然后使用一个cursor逐个字符进行读取。通过nextPos()获取当前位置字符并将游标向后移动一位:
class Reader(object) :
def __init__(self, data) :
self.data = data
self.cursor = 0
def nextPos(self) :
self.cursor += 1
return self.data[self.cursor-1]
同时我们需要检查是否有下一位:
def hasNext(self) :
return self.cursor < len(self.data)
对于移动到上一位,我们需要作越界判断:
def prevPos(self) :
self.cursor = max(self.cursor-1, 0)
至此我们Reader的基础功能就算完成了。
0x04 Token & Tokenizer
Token用于标注一个词素的意义以及对应值。例如:'['是JSONArray开始(BEGIN_ARRAY)的信号词,对应值为'['。由于JSON中的Token比较少,所以为了方便编写Parser,我们每一个Token所对应的意义用
来表示。
Tokenizer是将原始数据转化为一个Token列表让,Parser能看懂的数据。
对数据进行Tokenize时,我们需要将字符逐个取出传入Tokenizer并判断当前字符是否属于关键字(Signal),如果是关键字,我们对这个字符(或连带之后的值)一并进行处理。例如,如果当前处理的字符为'{',我们就返回一个Token(BEGIN_OBJECT, '{')。遇到双引号时,后面所对应数据为字符串;遇到数字或减号时,后面对应的时数字etc. 这时学习OI时接触的读入优化就派上用场啦!事实上,在这个Project中,Tokenizer就是实现各种数据的读入优化然后将他们的意义和值存入一个列表中....由于代码较长,具体实现可以看这里:Tokenizer。当然这里的Tokenizer并不完美...还需优化(也许有更加优美的实现方法呀)。放一个readInt()占位好了。
0x05 Parser
整个项目的核心,也是比较难调试的地方...作者的电脑没有PyCharm只好中间输出调试+瞪眼法调试了2333。当Parser拿到了她的好朋友Tokenizer辛勤劳动的成果——Tokenlist时,首先她会看一下,我是要解析一个JSONArray呢?还是要解析一个JSONObject呢?于是她就去看了第一个Token,如果这个Token的意义三BEGIN_ARRAY,那么她就先执行parseArray(),否则去执行parseObject()。在进行解析时,我们首先要规定我们下一个期望出现的Token时什么。例如,如果当前读到了BEGIN_ARRAY,那么下一个Token我们期待是BEGIN_STRING, BEGIN_BOOL, BEGIN_NUM, BEGIN_NULL和END_ARRAY。但如果当前是BEGIN_OBJECT,那么下一个期待的Token只能是BEGIN_STRING或者END_OBJECT了(JSONObject只能以str作为key)。这时我们规定Token含义为
的好处就体现出来了。事实上,这样规定就相当于状态压缩:将一个Token的含义定义为在一个整数的二进制下第
位为1。这样,如果我们期望下一次是BEGIN_STRING或者END_OBJECT时,我们期望得到下一个Token的值(
)和实际接收到的Token的值(
)进行按位与运算,如果得到的数字不是0,那么二进制下的
和
有至少一位都是1,这就说明当前Token是我们期望得到的:
# 设定期望Token
expected = END_OBJECT | BEGIN_STRING
# 判定actual是否属于END_OBJECT或者BEGIN_STRING
if expected & actual == 0 :
return False
return True
由于STRING在JSONObject中既可以作KEY又可以作VALUE,因此我们需要对其前一个Token进行判断,如果前一个Token是COLON(冒号),那么当前就是VALUE,否则是一个KEY。
另外,中间可能存在递归处理,注意一些特殊数据可能会爆栈(滑稽
当然可以用Python方便的tuple来搞啦~反正都可以实现(滑稽
由于Parser实现代码较长,大家可以去Parser查看代码。
0x06 后记
写这个Parser+调试大概花了我一下午吧...第一次写,不是太清楚。这篇文章也许并不如正在阅读的您期待的那样好,只是大概地讲述了Json解析的步骤和大致的原理...不过还有代码可以看嘛233333.......