手写一个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反序列化程序 的相关文章

  • 【调参】batch_size的选择

    直接上结论 xff1a 当有足够算力时 xff0c 选取batch size为32或更小一些 算力不够时 xff0c 在效率和泛化性之间做trade off xff0c 尽量选择更小的batch size 前期用小batch引入噪声 xff
  • 【Paper】Learning to Resize Images for Computer Vision Tasks

    From 别魔改网络了 xff0c Google研究员 xff1a 模型精度不高 xff0c 是因为你的Resize方法不够好 xff01 知乎 zhihu com paper 2103 09950v2 pdf arxiv org code
  • 【OpenCV】 外接矩形、最小外接矩形、多边形拟合、外接圆

    任务 xff1a 给定这样一张图片求图片中白色区域的外接矩形 最小外接矩形 拟合多边形以及外接圆 1 外接矩形 x y w h 61 cv2 boundingRect points 输入 xff1a 点集 返回值 xff1a 左上角点坐标以
  • Windows柯尼卡打印机驱动安装

    打印机型号 xff1a 柯尼卡 bizhub C300i xff08 打印机机身可见 xff09 1 下载驱动 在柯尼卡驱动官网查找下载打印机驱动 在型号处直接下拉查找自己的型号 xff0c 例如bizhub C300i xff0c 点击搜
  • PyQt开发入门教程

    来源 xff1a PyQt完整入门教程 lovesoo 博客园 cnblogs com 1 GUI开发框架简介 1 1 通用开发框架 electorn xff1a 基于node js xff0c 跨平台 xff0c 开发成本低 xff0c
  • VOC数据集颜色表colormap与代码

    VOC颜色和分类的对于关系 code如下 xff0c 这里提供两个版本 xff0c 一个是list tuple 版本 xff0c 支持直接在opencv的color参数使用 xff1b 另一个是ndarray版返回 list 版 def v
  • 【译】Python3.8官方Logging文档(完整版)

    注 xff1a 文章很长 xff0c 约一万字左右 xff0c 可以先收藏慢慢看哟 01 基础部分 日志是用来的记录程序运行事件的工具 当程序员可以通过添加日志打印的代码来记录程序运行过程中发生的某些事件时 这些事件包含了诸如变量数据在内的
  • OpenCV Scalar value for argument ‘color‘ is not numeric错误处理

    import cv2 cur color 61 np array 128 0 128 astype np uint8 cv2 polylines cvImage ndata isClosed 61 True color 61 cur col
  • COCO格式数据集可视化为框

    使用pycocotools读取和opencv绘制 xff0c 实现COCO格式数据边框显示的可视化 xff0c 可视化前后的示例为 xff1a 代码 xff1a coding utf 8 import os import sys getop
  • 微波遥感(三、SAR图像特征)

    SAR 是主 动式侧视雷达系统 xff0c 且成像几何属于斜距投影类型 因此 SAR 图像与光学图像在成像机理 几何特征 辐射特征等方面都有较大的区别 在进行 SAR 图像处理和应用前 xff0c 需要了解 SAR 图像的基本特征 本文主要
  • 基于Slicing Aided Hyper Inference (SAHI)做小目标检测

    遥感等领域数据大图像检测时 xff0c 直接对大图检测会严重影响精度 xff0c 而通用工具多不能友好支持大图分块检测 Slicing Aided Hyper Inference SAHI 是一个用于辅助大图切片检测预测的包 目前可以良好的
  • YOLOv5训练参数简介

    YOLOv5参数解析 xff0c 这次主要解析源码中train py文件中包含的参数 1 1 39 weights 39 1 2 39 cfg 39 1 3 39 data 39 1 4 39 hyp 39 1 5 39 epochs 39
  • 亚米级土耳其地震影像数据下载

    下载地址1 xff0c 提供震前震后影像 部分震后影像的百度网盘存档 xff1a https pan baidu com s 1 rLV7cR F3casKRwQH7JTw 提取码 xff1a dou3 灾前 灾后影像 下载地址2 xff1
  • nms_rotated编译出错fatal error: THC/THC.h: No such file or directory

    问题描述 xff1a 使用 python setup py develop or 34 pip install v e 34 编译nms rotated时出错 xff1a fatal error THC THC h No such file
  • 解决 AttributeError: module ‘numpy‘ has no attribute ‘int‘

    原因 xff1a numpy int在NumPy 1 20中已弃用 xff0c 在NumPy 1 24中已删除 解决方式 xff1a 将numpy int更改为numpy int xff0c int 方法 xff1a 点击出现错误代码链接会
  • 机载高分辨率SAR数据(~0.1米)

    美国桑迪亚 xff08 sandia xff09 国家实验室提供一系列机载SAR数据 xff0c 包括MiniSAR FARAR等 数据分辨率4英寸 xff0c 约0 1米 原始数据下载地址 xff0c 数据是复数据 xff0c 以不同格式
  • ubuntu18.04 及以上版本命令模式和GUI切换

    网上大多数说的CTRL 43 ALT 43 F1 6进入命令模式 xff0c CTRL 43 ALT 43 F7进入GUI模式 xff0c 在ubuntu18 04 及以上无效 正确的方式是 xff1a 进入命令模式可以通过CTRL 43
  • Python内置库——http.client源码刨析

    看过了http client的文档 xff0c 趁热打铁 xff0c 今天继续研究一下http client的源码 xff08 一 xff09 你会怎么实现 开始之前先让我们回忆一下一个HTTP调用的完整流程 xff1a 看到这张图 xff
  • ssh连接ubuntu访问拒绝(access denied)

    网上大多针对ssh连接ubuntu访问拒绝的解决办法是安装ssh或防火墙开启端口等等 xff0c 但这些都没问题之后还是访问拒绝 xff0c 则考虑ssh包可能安装的有问题 xff0c 可以尝试重装 流程如下 xff1a 1 在ubuntu
  • 【论文-目标检测】RTMDet: An Empirical Study of Designing Real-Time Object Detectors

    论文 代码 官方原理与实现详解 发展YOLO系列并方便支持实例分割和斜框检测等任务 xff0c 亮点 xff1a 设计兼容性backbone和neck xff0c 采用大核深度可分离卷积 xff1b 动态标签分配中采用软标签计算匹配损失 x

随机推荐