手写一个JSON反序列化程序

9fb9dc338ccff7413be2d41bfcfc0a8e.png

上一篇文章《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

完美!

db4370a4097ec8e86b18249a2fd1c2b4.png

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 3
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值