这篇主要参考@phil_eaton 的 "Writing a simple JSON parser" 写的,用于参考学习
他的这这篇文章链接:Writing a simple JSON parser | notes.eatonphil.com
关键词:json, parsing, python
概要:写一个JSON解析器是一个很棒的办法,可以让你熟悉解析的科技。其实之前我还不知道JSON是什么。接下来我会从以下主题介绍:
What JSON is — 什么是 JSON?
JSON 是 一种纯字符串形式的数据。
全称为 "JavaScript Object Notation" — “JavaScript对象表示法”,是一种轻量级、基于文本的、开放的数据交换格式
数据交换是指:两个设备之间建立连接并相互传递数据的过程
What parsing is and (typically) is not
解析器的工作程序:输入文本(字符串) — 构建数据结构。Parsing is a tool like this.
分为两个部分: Lexical analysis & Syntactic analysis (词法和句法)
The JSON library's interface
assert_equal(from_string('{"foo":1}', {"foo":1})
Lexical analysis
# 应用一个词法分析器
assert_equal(lex('{"foo": [1, 2, {"bar": 2}]}'),
['{', 'foo', ':', '[', 1, ',', 2, ',', '{', 'bar', ':', 2, '}', ']', '}'])
词法分析器是这样工作的:
首先有一个输入的字符串,其次将这个字符串打碎拆分成不同类型的标志词,然后没了。
简单来说:字符串拆成词。很简单吧
JSON格式有以下格式的词:
字符串**string**,
数字**number**,
布尔值**bool**,
空**null**
表达式要符合要求,还要有这些
引号**COLON**,
逗号**COMMA**,
左右大括号**{}**,
左右中括号**[]**
这些会装在JSON_SYNTAX的元组(tuple)里面
JSON_SYNTAX = [',', ':', '{', '}', '[', ']' ]
但是在输入字符串的时候,还会有一些**“空”字符**,这些是什么呢:
空格 ,tab(制表定位键)\t,换行\r,回车\n,退格\b(我也不知道为什么有这个东西)
这些会装在JSON_WHITESPACE的元组(tuple)里面
JSON_WHITESPACE = [' ', '\t', '\r', '\n', '\b' ]
有了这些数据,接下来是函数的主体了。作为一个词法分析器,首先最重要的就是能够识别不同类型的词,函数结构大致长这样:
def lex_string(string):
return None, string
def lex_number(string):
return None, string
def lex_bool(string):
return None, string
def lex_null(string):
return None, string
def lex(string):
tokens = []
while len(string):
json_string, string = lex_string(string) # json_string是解析出的字符串,如果没有则设空;string是余下的输入字符串,
if json_string is not None:
tokens.append(json_string)
continue
json_number, string = lex_number(string) # json_number是解析出的数,如果没有则设空;string是余下的输入字符串,
if json_number is not None:
tokens.append(json_number)
continue
json_bool, string = lex_bool(string) # json_bool是解析出的布尔值,非空则赋值为字符串类型的'ture'或'false',如果没有则设空;string是余下的输入字符串,
if json_bool is not None:
tokens.append(json_bool)
continue
json_null, string = lex_null(string) # json_null是解析出的null,非空则赋值为字符串类型的'null',如果没有则设空;string是余下的输入字符串,
if json_null is not None:
tokens.append(None)
continue
if string[0] in JSON_WHITESPACE: # 如果是“空”(空格等)则吞掉
string = string[1:]
elif string[0] in JSON_SYNTAX: # 如果是JSON的符号(逗,冒,括号)则加入元组
tokens.append(string[0])
string = string[1:]
else:
raise Exception('Unexpected character: {}'.format(string[0])) # 将无法识别类型的字符打印并报错
# 这个顺序就是先解析一个词再解析一个JSON符号
return tokens
Lexing strings
检查顺序:冒号,读取字符串,冒号
在lex_string函数里,首先会检查是否有字符串开始的标志 '"' 冒号,如果没有则会返回空,有则会继续一个一个字母录入。
字符串从冒号开始,到冒号结束,如果读完字符串也没有遇到冒号,那就报错咯
def lex_string(string):
json_string = ''
if string[0] == JSON_QUOTE:
string = string[1:]
else:
return None, string
# 读取字符串
for c in string:
if c == JSON_QUOTE:
return json_string
else:
json_string +=c
# 如果读完字符串也没有遇到第二个冒号(字符串结束标志)
raise Exception('Except end-of-string quote')
TODO:lexing numbers, boolens, nulls
Lexing numbers
检查顺序:数字,直到非数字
一个合法的数字有以下形式,包含的字符有0-9,负号,点,e
1, 2222, 1433223, 2745148060 # 正整数
8.88, 0.01, 120348.21384023 # 正浮点数
-10, -221039210, -0.99 # 负数
8e-3, 4e6, -9e11 # 指数
在 lex_numbers函数里,如果得到数,最后返回的数是整数或者浮点形式
def lex_number(string):
json_number = ''
# 创建一个数字标准判断库,如果string中有数库中的字符,即为所需的number
numbers_characters = [str(d) for d in range(1, 10)] + ['-', 'e', '.']
for c in string:
if c in numbers_characters:
json_number += c
else:
break
# 检查负号的位置是否合法
if '-' in json_number:
if 'e' in json_number:
if json_number[0] is not '-' or json_number[json_number.find('e') + 1] is not '-': # 负号只能在数的开头或者e的后面第一位
raise Exception('发生解析错误')
if json_number[0] is not '-':
raise Exception('发生解析错误')
# 获取的字符number有三个情况:1.整型;2.浮点;3.空
if len(json_number) == 0:
return None, string
elif '.' in json_number:
return float(json_number), string[len(json_number):]
else: # 整数
if 'e' in json_number:
a = int(json_number[:json_number.find('e')])
b = int(json_number[json_number.find('e') + 1:])
answer = a * pow(10, b)
else:
answer = int(json_number)
return answer, string[len(json_number):]
Lexing boolens
检查顺序:字符串长度,字符串内容:true or false
在 lex_bool函数里,最后返回的是布尔值
def lex_bool(string):
# bool: true, false
string_len = len(string)
if string_len >= TRUE_LEN and \ # 符合长度且是true
string[:TRUE_LEN] == 'true':
return True, string[TRUE_LEN:]
elif string_len >= FALSE_LEN and \
string[:FALSE_LEN] == 'false':
return False, string[FALSE_LEN:]
return None, string # 否则返回空
Lexing nulls
检查顺序,字符串长度,字符串内容:null
在lex_null函数里,最后返回的是True(代表得到了null,于是就会向tokens中加入None)
def lex_null(string):
# null: null
string_len = len(string)
if string_len >= NULL_LEN:
if string[:NULL_LEN] == 'null':
return True, string[NULL_LEN:]
return None, string
Syntactic analysis
# 应用一个句法分析器
tokens = lex('{"foo": [1, 2, {"bar": 2}]}')
assert_equal(tokens, ['{', 'foo', ':', '[', 1, ',', 2, '{', 'bar', ':', 2, '}', ']', '}'])
assert_equal(parse(tokens), {'foo': [1, 2, {'bar': 2}]})
句法分析器是这样工作的:
首先有通过lex(string)得到的标志词组,其次判断它们组成的表达式是否符合表达式要求
简单来说:判断表达式合法与否
# 合法的表达式
{} # 空
{"example":1} # key : value
{"e2":[1,2,3,'a']} # key : value(list)
# 不合法的表达式
{ # 缺少右大括号
{"e3":[1,'a',99 } # 缺少右边中括号
{"e4":{"f":0} # 逗号后没有词
从上面的例子来看
有了词法分析器解析出的元组tokens后,接下来是句法分析了。作为一个句法分析器,首先最重要的就是能够判断句子是否合法(元组和字典),函数结构大致长这样:
def parse_array(tokens):
return [], tokens
def parse_object(tokens):
return {}, tokens
def parser(tokens):
t = tokens[0]
if t == JSON_LEFTBRACE:
return parse_object(tokens)
elif t == JSON_LEFTBRACKET:
return parse_array(tokens)
else:
tokens = tokens[1:]
Parsing arrays
一个合格的元组,要有 左括号 [ , 右括号 ],还有里面元素之间的逗号
[1, 2] # 很好
[3, # 很不好
进入parse_array 函数之前,已经判断过左中括号了,于是接下来就检查元素之间的逗号和末尾的右括号就行了,检查的时候一个一个添加进元组json_array之中就好了
要注意的是,元素可以是JSON格式的任何值:字典,元组,字符串,数字,布尔值,null。而用来解析并获取这些值的方法就是parse()了,所以
检查顺序:元素,逗号,元素,右中括号结束
报错:1. 元素后没有逗号 2. 元组末尾没有右中括号
def parse_array(tokens):
json_array = []
if tokens[0] == JSON_RIGHTBRACKET:
return json_array, tokens[1:]
while True: # 顺序是:获得元素对象,添加元素;检查逗号或者右中括号,否则报错(元素后需要逗号)
json, tokens = parser(tokens)
json_array.append(json)
t = tokens[0]
if t == JSON_RIGHTBRACKET:
return json_array, tokens[1:]
elif t == JSON_COMMA:
tokens = tokens[1:]
else:
raise Exception('Excepted comma after object')
raise Exception('Excepted end-of-array bracket')
Parsing objects
一个合格的对象,要有 左括号 {, 右括号 },键和值中间的冒号: ,还有元素之间的逗号
要注意的是,键值只能是字符串类型,而键对应的值val可以是任何类型的
检查顺序:键,冒号,值,逗号,键,冒号,值,右大括号结束
报错:1. 键不是字符串 2. 键后没有冒号 3. 元素后没有逗号 4. 末尾没有右大括号
def parse_object(tokens):
json_object = {}
if tokens[0] == JSON_RIGHTBRACE:
return json_object, tokens[1:]
while True:
json_key, tokens = parser(tokens) # 获取键
key_type = type(json_key)
if key_type != str: # 哦 判断键类型,必须是str
raise Exception('Key type must be str')
t = tokens[0]
if t != JSON_COLON: # 检查键后是否有冒号
return Exception('Expected colon after key in object, got: {}'.format(t))
else:
tokens = tokens[1:]
json_val, tokens = parser(tokens)
json_object[json_key] = json_val # 给键赋值
t = tokens[0]
if t == JSON_RIGHTBRACE: # 右大括号结束
return json_object, tokens[1:]
elif t == JSON_COMMA:
tokens = tokens[1:]
else:
raise Exception('Excepted comma after object ')
raise Exception('Excepted end-of-dict brace')
Unifying the library
为了提供一个理想的接口,用一个from_string 函数把把词法分析器和句法分析器封装起来就行啦
def from_string(string):
tokens = lex(string)
return parse(tokens)[0] # 返回的是句法分析器 解析并合成 的一个**字典类型**的对象
这样子这个JSON文库就完成了
下面是测试代码,拿去测试你的工程吧!!
import unittest
import jsonpackage
class TestStringMethods(unittest.TestCase):
def test_empty_object(self):
self.assertEqual(jsonpackage.from_string('{}'), {})
def test_basic_object(self):
self.assertEqual(jsonpackage.from_string('{"foo":"bar"}'), {"foo": "bar"})
def test_basic_number(self):
self.assertEqual(jsonpackage.from_string('{"foo":11}'), {"foo": 11})
def test_empty_array(self):
self.assertEqual(jsonpackage.from_string('{"foo":[]}'), {"foo": []})
def test_basic_array(self):
self.assertEqual(jsonpackage.from_string('{"foo":[1,2,"three"]}'), {"foo": [1, 2, "three"]})
def test_nested_object(self):
self.assertEqual(jsonpackage.from_string('{"foo":{"bar":2}}'), {"foo": {"bar": 2}})
def test_true(self):
self.assertEqual(jsonpackage.from_string('{"foo":true}'), {"foo": True})
def test_false(self):
self.assertEqual(jsonpackage.from_string('{"foo":false}'), {"foo": False})
def test_null(self):
self.assertEqual(jsonpackage.from_string('{"foo":null}'), {"foo": None})
def test_basic_whitespace(self):
self.assertEqual(jsonpackage.from_string('{ "foo" : [1, 2, "three"] }'), {"foo": [1, 2, "three"]})
if __name__ == '__main__':
unittest.main()