手写一个JSON反序列化程序

2023-05-16

上一篇文章《JSON是什么》给大家介绍了JSON的标准规范,今天就自己动手写一个JSON的反序列化程序,并命名它为 zjson。

0

开始之前

本篇文章的目的是学习实践,所以我们选择相对简单的Python实现,原因在于JSON的值类型可以很方便的映射到Python的数据类型。下面是二者之间的映射关系:

    ———————————————————————
    JSON             Python
    ———————————————————————
    null             None
    true             True
    false            False
    number           float
    string           str
    array            list
    object           dict

实现整个反序列化的功能大概需要200多行的代码,项目虽小,但是需要考虑的场景还是挺多的,为了提高代码质量,增加了单元测试用例,这样就可以很方便的定位问题。

最终,加上单元测试,整个项目的代码行数在400行以内。

1

入口函数

首先定义反序列化的入口函数:它接受一个字符串,解析并映射成 Python 类型。

def parse(text):
    pass

考虑到我们可能需要从前到后挨个读取 text 里面的字符,特殊情况下甚至需要预读后续的若干位字符,为了方便,定义一个类用来保存这些内部状态。

WHITE_SPACES = {"\n", "\t", "\r", " "}




class TextObj:
    def __init__(self, text):
        self.text = text
        self.index = 0  # 保存读取的位置信息
        self.line = 1 # 记录行数,在遇到错误方便定位


    def read(self):
        # 读取下一个字符
        self.index += 1
        # 判断是否已经读取完毕
        if self.index >= len(self.text):
            return ""
        char = self.text[self.index]
        if char == '\n':
            self.line += 1
        return char


    def skip(self):
        # 对于解析函数来说,调用skip可方便的跳过空白字符
        if self.current in WHITE_SPACES:
            char = self.read()
            while char and char in WHITE_SPACES:
                char = self.read()


    def read_slice(self, step):
        # 方便一次性读取多个字符
        start = self.index
        end = start + step
        self.index = end
        if end > len(self.text):
            raise UnexpectedCharacterError(self)
        return self.slice(start, end)


    def slice(self, start, end):
        # 跟read_slice区别在于,slice不消耗下标
        return self.text[start:end]


    @property
    def current(self):
        # 使用描述符来动态获取当前字符
        if self.index >= len(self.text):
            return ""
        return self.text[self.index]


另外,对于不合法的 text 输入,程序应该能够抛出异常,所以我们定义了 UnexpectedCharacterError :

class UnexpectedCharacterError(ValueError):
    def __init__(self, text_obj):
        super().__init__("unexpected character at index %s(line %s): %s" % (text_obj.index, text_obj.line, text_obj.text))


有了 TextObj 和 UnexpectedCharacterError,假设我们还有一个函数 parse_value 可以正确解析 JSON 值类型,到这里基本上就可以给出完整的 parse 函数了:

def parse(text):
    text_obj = TextObj(text)
    text_obj.skip()  # 跳过开始的空白符
    result = parse_value(text_obj)
    text_obj.skip()  # 跳过结束的空白符
    if text_obj.current != "":
        raise UnexpectedCharacterError(text_obj)
    return result

剩下的编码工作就是完成 parse_value 函数了,但是在定义 parse_value 之前,我们先来编写单元测试用例。

2

单元测试

测试模块使用 Python 内置的 unittest,测试用例需要覆盖各种 JSON 值类型的解析,另外还需要覆盖异常情况。

class ZjsonTest(unittest.TestCase):


    def test_parse_null(self):
        self.assertIsNone(zjson.parse("null"))


        with self.assertRaises(zjson.UnexpectedCharacterError):
            zjson.parse("null0")


    def test_parse_false(self):
        self.assertFalse(zjson.parse("false"))


        with self.assertRaises(zjson.UnexpectedCharacterError):
            zjson.parse("flase")


    def test_parse_true(self):
        self.assertTrue(zjson.parse("true"))


        with self.assertRaises(zjson.UnexpectedCharacterError):
            zjson.parse("ture")


    def test_parse_number(self):
        self.assertEqual(zjson.parse("-1"), -1.0)
        self.assertEqual(zjson.parse("1"), 1.0)
        self.assertEqual(zjson.parse("0"), 0.0)
        self.assertEqual(zjson.parse("-0"), 0.0)
        self.assertEqual(zjson.parse("1.1"), 1.1)
        self.assertEqual(zjson.parse("1.10"), 1.1)
        self.assertEqual(zjson.parse("1E1"), 10.0)
        self.assertEqual(zjson.parse("1E-1"), 0.1)
        self.assertEqual(zjson.parse("1E0"), 1.0)
        self.assertEqual(zjson.parse("1E-0"), 1.0)
        self.assertEqual(zjson.parse("1.1E0"), 1.1)
        self.assertEqual(zjson.parse("-1.1E1"), -11.0)


        with self.assertRaises(zjson.UnexpectedCharacterError):
            zjson.parse("00")
        with self.assertRaises(zjson.UnexpectedCharacterError):
            zjson.parse("0..0")
        with self.assertRaises(zjson.UnexpectedCharacterError):
            zjson.parse("0.E0")
        with self.assertRaises(zjson.UnexpectedCharacterError):
            zjson.parse("0.")
        with self.assertRaises(zjson.UnexpectedCharacterError):
            zjson.parse("-")
        with self.assertRaises(zjson.UnexpectedCharacterError):
            zjson.parse("+0")
        with self.assertRaises(zjson.UnexpectedCharacterError):
            zjson.parse("+1")
        with self.assertRaises(zjson.UnexpectedCharacterError):
            zjson.parse(".23")


    def test_parse_string(self):
        self.assertEqual(zjson.parse('"hello"'), "hello")
        self.assertEqual(zjson.parse('"1111"'), "1111")
        self.assertEqual(zjson.parse('"1111\\""'), "1111\"")
        self.assertEqual(zjson.parse('"1111\\n"'), "1111\n")
        self.assertEqual(zjson.parse('"1111\\r"'), "1111\r")
        self.assertEqual(zjson.parse('"1111   "'), "1111   ")
        self.assertEqual(zjson.parse('"  1111   "'), "  1111   ")
        self.assertEqual(zjson.parse('"\\\\"'), "\\")
        self.assertEqual(zjson.parse('"\\/"'), "/")
        self.assertEqual(zjson.parse('""'), "")
        self.assertEqual(zjson.parse('"\\u6c49"'), "汉")
        self.assertEqual(zjson.parse('"\\uD834\\uDD1E"'), "????")


        with self.assertRaises(zjson.UnexpectedCharacterError):
            zjson.parse("\"")
        with self.assertRaises(zjson.UnexpectedCharacterError):
            zjson.parse("\"111")
        with self.assertRaises(zjson.UnexpectedCharacterError):
            zjson.parse("111\"")
        with self.assertRaises(zjson.UnexpectedCharacterError):
            zjson.parse('"\\uxxxx"')
        with self.assertRaises(zjson.UnexpectedCharacterError):
            zjson.parse('"\\uD801\\ux"')


    def test_parse_array(self):
        self.assertEqual(zjson.parse('["hello"]'), ["hello"])
        self.assertEqual(zjson.parse('[1]'), [1])
        self.assertEqual(zjson.parse('[null]'), [None])
        self.assertEqual(zjson.parse('[1,2,3]'), [1,2,3])
        self.assertEqual(zjson.parse('[1,2,"hello"]'), [1,2,"hello"])
        self.assertEqual(zjson.parse('[true, false]'), [True, False])
        self.assertEqual(zjson.parse('[[1,2], [3,4]]'), [[1,2], [3,4]])
        self.assertEqual(zjson.parse('[{"hello": "world"}]'), [{"hello": "world"}])


        with self.assertRaises(zjson.UnexpectedCharacterError):
            zjson.parse('[1')
            zjson.parse('[1, 2')
            zjson.parse('[1, 2}')


    def test_parse_object(self):
        self.assertEqual(zjson.parse(' {"hello": 1} '), {"hello": 1})
        self.assertEqual(zjson.parse('\t{"hello": "world"}\n'), {"hello": "world"})
        self.assertEqual(zjson.parse('{"hello": "world", "k2": "v2"}'), {"hello": "world", "k2": "v2"})
        self.assertEqual(zjson.parse('{"hello": [1,2]}'), {"hello": [1,2]})
        self.assertEqual(zjson.parse('{"hello": {"k1":"v1", "k2": "v2"}}'), {"hello": {"k1":"v1", "k2": "v2"}})


        with self.assertRaises(zjson.UnexpectedCharacterError):
            zjson.parse("{1:2}")
            zjson.parse('{"1":2')
            zjson.parse('{"1":2')

整个测试用例大概100行左右,基本上能够覆盖各种值类型的解析场景。有了测试用例,接下来就可以放心大胆的编写功能代码了。

3

值类型解析

现在到了正式定义 parse_value 的时候了,考虑到 JSON 的几种值类型的开始标志全都不一样,只需要读取 text_obj 的当前字符就可以通过分支调用不用值类型的解析函数,所以 parse_value 相对来说也算简单。

def parse_value(text_obj):
    char = text_obj.current
    if char == "n":
        return parse_null(text_obj)
    elif char == "f":
        return parse_false(text_obj)
    elif char == "t":
        return parse_true(text_obj)
    elif char == '"':
        return parse_string(text_obj)
    elif char == "{":
        return parse_object(text_obj)
    elif char == "[":
        return parse_array(text_obj)
    else:
        return parse_number(text_obj)


剩下的全部工作就是定义不同类型的解析函数了。

I. parse_null

最简单的还是 parse_null,只需要再预读剩下的三个字符,判断是不是跟 “null” 相等即可,如果不相等意味着原始 text 不合法,需要抛出异常。

def parse_null(text_obj):
    if text_obj.read_slice(4) != "null":
        raise UnexpectedCharacterError(text_obj)
    return None

II. parse_true  parse_false

同 parse_null 类似,下面是 parse_true 和 parse_false 的函数定义:

def parse_false(text_obj):
    if text_obj.read_slice(5) != "false":
        raise UnexpectedCharacterError(text_obj)
    return False




def parse_true(text_obj):
    if text_obj.read_slice(4) != "true":
        raise UnexpectedCharacterError(text_obj)
    return True

III. parse_number

虽然我们可以使用 float 直接转换 text 中的 number,但是由于 number 的格式与 Python 存在一些差别,还是需要 parse_number 判断 number 是否合法。

DIGITS = set("0123456789")




def parse_number(text_obj):
    head = text_obj.index  # 记录开始时的下标


    char = text_obj.current
    # 处理负号
    if char == "-":
        char = text_obj.read()
    # 整数部分有两种形式:0或者1-9后面跟若干数字
    if char == "0":
        char = text_obj.read()
    elif char in DIGITS:
        while char in DIGITS:
            char = text_obj.read()
    else:
        # 如果整数部分不合法,则整个number不合法
        raise UnexpectedCharacterError(text_obj)
    # 小数部分
    if char == ".":
        char = text_obj.read()
        if char not in DIGITS:
            raise UnexpectedCharacterError(text_obj)
        while char in DIGITS:
            char = text_obj.read()
    # 指数部分
    if char == "E" or char == "e":
        char = text_obj.read()
        if char == "+" or char == "-":
            char = text_obj.read()
        if char not in DIGITS:
            raise UnexpectedCharacterError(text_obj)
        while char in DIGITS:
            char = text_obj.read()
    tail = text_obj.index  # 记录结束时的下标
    # 使用内置的float将字符串转化为浮点数
    return float(text_obj.slice(head, tail))


对于 number 的解析结果使用 float 映射主要还是为了方便,如果想区分浮点数和整数,也可以加入额外的判断,根据 number 的实际值决定使用哪个类型映射:整数使用 int,浮点数使用 float。

IV. parse_string

parse_string 也是比较复杂的解析函数了,对于转义字符特别是 Unicode 需要十分小心。

ESCAPES = {
    '"': '"',
    "\\": "\\",
    "/": "/",
    "b": "\b",
    "f": "\f",
    "n": "\n",
    "r": "\r",
    "t": "\t",
}




def parse_string(text_obj):
    if text_obj.current != '"':
        # 必须以双引号开始
        raise UnexpectedCharacterError(text_obj)


    # 考虑到转义字符的存在,这里需要使用list来保存解析到的单个字符,最后使用join函数返回字符串
    # 避免使用字符串的”+“操作,可以提高效率
    cs = []
    text_obj.read()
    while True:
        char = text_obj.current
        if char == "":
            # 判断text是否已经小号完毕
            raise UnexpectedCharacterError(text_obj)
        if char == "\\":
            # 处理转义字符
            char = text_obj.read()
            if char == "u":
                # 处理 Unicode
                text_obj.read()
                code_point = get_code_point(text_obj)
                if 0xD800 <= code_point <= 0xDBFF and text_obj.slice(text_obj.index, text_obj.index+2) == "\\u":
                    # 处理超过0xFFFF的码点
                    text_obj.read_slice(2)
                    low = get_code_point(text_obj)
                    code_point = 0x10000 + (code_point - 0xD800) * 0x400 + (low - 0xDC00)
                cs.append(chr(code_point))
                continue
            if char not in ESCAPES:
                raise UnexpectedCharacterError(text_obj)
            cs.append(ESCAPES[char])
            text_obj.read()
            continue
        elif char == '"':
            # 结束标志
            text_obj.read()
            return "".join(cs)
        else:
            # 普通字符,直接添加到list
            cs.append(char)
            text_obj.read()
            
            
def get_code_point(text_obj):
    # 解析unicode时使用
    h4 = text_obj.read_slice(4)
    try:
        return int(h4, 16)
    except Exception as e:
        raise UnexpectedCharacterError(text_obj)

完成了基本类型的解析,剩下的就是集合类型了。由于集合类型具有闭包性质,所以集合类型的解析又调用了 parse_value 函数。

V. parse_array

Python 中的 list 类型跟 array 非常相似,映射起来也比较方便,parse_array 的主要的工作还是验证 array 格式是否合法。

def parse_array(text_obj):
    assert text_obj.current == "["
    result = []  # 使用list来保存array解析结果
    text_obj.read()
    text_obj.skip()
    char = text_obj.current
    while True:
        if char == "]":
            # 结束标志。主要是为了判断是否为空array
            text_obj.read()
            break
        value = parse_value(text_obj)
        result.append(value)


        text_obj.skip()


        char = text_obj.current
        if char == ",":
            # 判断是否还有值
            text_obj.read()
            text_obj.skip()
            continue
        elif char == "]":
            # 结束标志
            text_obj.read()
            break
        else:
            raise UnexpectedCharacterError(text_obj)
    return result

VI. parse_object

最后完成的是 parse_object 的函数定义。

def parse_object(text_obj):
    assert text_obj.current == "{"
    result = {}  # 使用dict保存object解析结果
    text_obj.read()
    text_obj.skip()
    char = text_obj.current
    while True:
        if char == "}":
            # 结束标志。主要是为了判断是否为空object
            text_obj.read()
            break
        # 解析 key
        key = parse_string(text_obj)
        text_obj.skip()
        # 解析冒号
        if text_obj.current != ":":
            raise UnexpectedCharacterError(text_obj)
        text_obj.read()
        text_obj.skip()
        # 解析value
        value = parse_value(text_obj)
        text_obj.skip()
        # 添加到dict
        result[key] = value


        char = text_obj.current
        if char == ",":
            # 判断是否还有键值对
            text_obj.read()
            text_obj.skip()
            continue
        elif char == "}":
            # 结束标志
            text_obj.read()
            break
        else:
            raise UnexpectedCharacterError(text_obj)
    return result

   

4

RUN!ZJSON! RUN!

至此,整个反序列化功能就全部完成了,整个程序没有引入第三方依赖,完全由 Python 的内置功能完成。

最后,运行一下单元测试:

Ran 7 tests in 0.010s


OK

完美!

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

手写一个JSON反序列化程序 的相关文章

随机推荐

  • 数据结构与算法(Java版) | 队列的应用场景和介绍

    队列的一个应用场景 给大家介绍完稀疏数组这种数据结构之后 xff0c 接下来我再来给大家介绍另外一种数据结构 xff0c 即队列 队列 xff0c 听其名而知其义 xff0c 相信大家应该都在现实生活中见过 xff0c 比如在火车站排队买票
  • 数据结构与算法(Java版) | 数组模拟队列的思路分析与代码实现

    思路分析 上一讲我们讲过 xff0c 队列既可以用数组来实现 xff0c 也可以用链表来实现 xff0c 但由于我们比较熟悉数组这种结构 xff0c 所以这里我会先给大家讲一下数组这种实现方式 xff0c 至于链表这种实现方式 xff0c
  • android 8.1 MTK 方案修改记录

    Music播放音乐时锁屏不需要显示专辑封面 span class token operator 43 43 span span class token operator 43 span b span class token operator
  • centos 5 yum 不能用出现all mirror URLs are not use ftp http or file

    新装了一个centos 5 11 xff0c yum一直没法用 xff0c 每次使用都是出现下图的提示 xff1a 在网上找各种解决方案 xff0c 都是网络没连上 xff0c DNS有问题之类的 直到找到一篇帖子提到centos 5所有资
  • 远程连接——SSH

    简介 SSH xff08 Secure Shell xff09 是一种安全通道协议 xff0c 主要用来实现字符界面的远程登录 远程 复制等功能 SSH 协议对通信双方的数据传输进行了加密处理 xff0c 其中包括用户登录时输入的用户口令
  • Ubuntu 安装LLVM

    在部署galois时 xff0c 需要安装libllvm gt 61 7 0 with RTTI support xff0c 但是如果使用直接编译好的 xff0c 则并不会对RTTI提供支持 因此选择下载源代码自己编译安装 xff0c 并设
  • 关于ubuntu分区挂载

    转载自https blog csdn net u010409517 article details 88081911 一 硬盘分区 1 查看硬盘及所属分区情况 sudo fdisk lu 如图显示 xff0c 我们对200G硬盘进行分区 x
  • Ubuntu开启SSH远程登录

    本文介绍如何在Ubuntu下开启ssh服务并能通过Xshell进行远程登录的方法 测试使用的是在虚拟机上装的Ubuntu和window10 首先更新自己Ubuntu的源 xff0c 具体自行解决 更改IP地址 可以在设置里面设置 xff0c
  • SSH配置免密登录方法

    转载自https blog csdn net jeikerxiao article details 84105529 1 客户端生成公私钥 本地客户端生成公私钥 xff1a xff08 一路回车默认即可 xff09 ssh span cla
  • 阿里云上为服务器申请外网网卡并绑定公网ip

    在阿里云上创建ECS实例的时候 xff0c 云会自动配置公网ip和内网ip 但实质在该ECS实例上只有内网ip xff0c 可以通过ifconfig进行查看 xff0c 如下图 xff1a eth0是一个内网网卡 xff0c 上面绑定的是内
  • C++中常用函数 (持续更新ing...)

    access 函数std memsetfseek函数ftell std condition variable notify one notify allfread
  • mpi关于send/receive的顺序问题

    引用 MPI buffered send receive order I m using MPI with fortran but the question is more specific to the MPI standard than
  • std::sort导致内存崩贵

    转载自 xff1a 一次stl sort调用导致的进程崩溃 include span class token operator lt span algorithm span class token operator gt span span
  • 极光推送集成问题记录

    安装 npm install jpush react native save 也需要安装jcore react native npm install jcore react native save 核心步骤参考 xff1a https gi
  • 解决Navicat连接远程服务器出现lost connection to mysql server at ‘reading initial communication packet‘

    本文章是基于 这类问题在网上已经有很多类似的解答了 xff0c 诸如各种的修改my cnf xff0c 或者修改MySQL设置 Navicat设置 但是在我自己遇到这些问题的时候我尝试过搜索很多答案 xff0c 但是都没有得到解决 我的My
  • 快速排序quicksort算法Java的实现

    span class token keyword public span span class token keyword class span span class token class name QuickSort span span
  • 一行代码解决macOS下Android Studio提示的“licences have not been accepted”

    打开终端 直接输入 Library Android sdk tools bin sdkmanager licenses 接着一路 y 即可解决问题
  • 最简单的纯css固定table的表头

    页面table通常都需要做表头固定 xff0c 参考了n种实现方式 xff0c 最终还是选择了用纯css来做表头固定 xff0c 只是因为方便 xff0c 易于调试 table tbody display block height 450p
  • kali linux修改更新源及更新

    1 修改sources list源文件 xff1a leafpad etc apt sources list aliyun 阿里云 deb http mirrors aliyun com kali kali rolling main non
  • 手写一个JSON反序列化程序

    上一篇文章 JSON是什么 给大家介绍了JSON的标准规范 xff0c 今天就自己动手写一个JSON的反序列化程序 xff0c 并命名它为 zjson 0 开始之前 本篇文章的目的是学习实践 xff0c 所以我们选择相对简单的Python实