编写JSON解析器是熟悉解析技术的最简单方法之一。格式非常简单。它是递归定义的,所以与解析Brainfuck相比,你会遇到轻微的挑战 ; 你可能已经使用JSON。除了最后一点之外,解析 Scheme的S表达式可能是更简单的任务。
解析通常分为两个阶段:词法分析和句法分析。词法分析将源输入分解为称为“令牌”的语言中最简单的可分解元素。句法分析(通常称为“解析”)会接收到令牌列表,并尝试查找其中的模式以符合要解析的语言。
解析不能确定输入源的语义可行性。输入源的语义可行性可能包括变量是否在使用之前定义,是否使用正确的参数调用函数,或者是否可以在某个范围内第二次声明变量。
当然,人们选择解析和应用语义规则的方式总是有所不同,但我正在假设一种“传统”方法来解释核心概念。
JSON库的接口
最终,应该有一个from_string方法接受一个JSON编码的字符串并返回等效的Python字典。
例如:
assert_equal(from_string('{"foo": 1}'),
{"foo": 1})
词汇分析
词法分析将输入字符串分解为令牌。注释和空白在词法分析过程中经常被丢弃,因此您只需输入一个简单的输入即可在语法分析过程中搜索语法匹配。
假设一个简单的词法分析器,您可以代替输入字符串中的所有字符,并将它们拆分为非整数,字符串和布尔文字等非递归定义的语言结构。特别是,字符串 必须是词法分析的一部分,因为在不知道它不是字符串的一部分的情况下,不能丢弃空格。
在一个有用的词法分析器中,您可以跟踪您跳过的空格和注释,当前行号和文件,以便您可以在任何阶段通过分析源代码所产生的错误中引用它。V8 Javascript引擎最近能够重现一个函数的确切源代码。这至少需要词法分析器的帮助才能成为可能。
实现一个JSON词法分析器
JSON词法分析器的要点是遍历输入源,并尝试查找字符串,数字,布尔值,空值或JSON语法(如左圆括号和左括号)的模式,最终将每个元素作为列表返回。
以下是词法分析器应该为示例输入返回的内容:
assert_equal(lex('{"foo": [1, 2, {"bar": 2}]}'),
['{', 'foo', ':', '[', 1, ',', 2, '{', 'bar', ':', 2, '}', ']', '}'])
以下是这种逻辑可能开始看起来像:
def lex(string):
tokens = []
while len(string):
json_string, string = lex_string(string)
if json_string is not None:
tokens.append(json_string)
continue
# TODO: lex booleans, nulls, numbers
if string[0] in JSON_WHITESPACE:
string = string[1:]
elif string[0] in JSON_SYNTAX:
tokens.append(string[0])
string = string[1:]
else:
raise Exception('Unexpected character: {}'.format(string[0]))
return tokens
这里的目标是尝试匹配字符串,数字,布尔值和空值并将它们添加到令牌列表中。如果这些都不匹配,检查字符是否为空白,如果是,则将其丢弃。否则,将其作为标记存储,如果它是JSON语法的一部分(如左圆括号)。如果字符/字符串不匹配任何这些模式,则最后抛出异常。
让我们在这里扩展核心逻辑以支持所有类型并添加函数存根。
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)
if json_string is not None:
tokens.append(json_string)
continue
json_number, string = lex_number(string)
if json_number is not None:
tokens.append(json_number)
continue
json_bool, string = lex_bool(string)
if json_bool is not None:
tokens.append(json_bool)
continue
json_null, string = lex_null(string)
if json_null is not None:
tokens.append(json_null)
continue
if string[0] in JSON_WHITESPACE:
string = string[1:]
elif string[0] in JSON_SYNTAX:
tokens.append(string[0])
string = string[1:]
else:
raise Exception('Unexpected character: {}'.format(string[0]))
return tokens
Lexing字符串
对于该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, string[len(json_string)+1:]
else:
json_string += c
raise Exception('Expected end-of-string quote')
对于这个lex_number函数,要点是遍历输入,直到找到一个不能成为数字的字符。(当然,这是一种粗略的简化,但更准确的将作为练习留给读者。)在找到不能作为数字一部分的字符后,如果您使用的字符不是返回浮点数或int累计数大于0.否则返回None和原始字符串输入。
def lex_number(string):
json_number = ''
number_characters = [str(d) for d in range(0, 10)] + ['-', 'e', '.']
for c in string:
if c in number_characters:
json_number += c
else:
break
rest = string[len(json_number):]
if not len(json_number):
return None, string
if '.' in json_number:
return float(json_number), rest
return int(json_number), rest
Lexing布尔和空值
查找布尔值和空值是一个非常简单的字符串匹配。
def lex_bool(string):
string_len = len(string)
if string_len >= TRUE_LEN and \
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
def lex_null(string):
string_len = len(string)
if string_len >= NULL_LEN and \
string[:NULL_LEN] == 'null':
return True, string[NULL_LEN]
return None, string
现在,词法分析器代码已经完成!
句法分析
语法分析器(基本)的工作是遍历一维的令牌列表,并根据语言的定义将令牌组匹配到多个语言片断。如果在句法分析过程中的任何时候,解析器都无法将当前的一组令牌与语言的有效语法进行匹配,那么解析器将会失败,并可能为您提供有用的信息,包括您给出的内容,位置以及期望的内容您。
实现JSON解析器
JSON解析器的要点是对调用后接收到的令牌进行迭代,lex并尝试将令牌与对象,列表或普通值进行匹配。
以下是解析器应该为示例输入返回的内容:
tokens = lex('{"foo": [1, 2, {"bar": 2}]}')
assert_equal(tokens,
['{', 'foo', ':', '[', 1, ',', 2, '{', 'bar', ':', 2, '}', ']', '}'])
assert_equal(parse(tokens),
{'foo': [1, 2, {'bar': 2}]})
以下是这种逻辑可能开始看起来像:
def parse_array(tokens):
return [], tokens
def parse_object(tokens):
return {}, tokens
def parse(tokens):
t = tokens[0]
if t == JSON_LEFTPAREN:
return parse_array(tokens[1:])
elif t == JSON_LEFTBRACKET:
return parse_object(tokens[1:])
else:
return t, tokens[1:]
这个词法分析器和解析器之间的一个关键结构区别是词法分析器返回一个一维的标记数组。解析器通常是递归定义的,并返回一个递归的树状对象。由于JSON是数据序列化格式而不是语言,因此解析器应该使用Python生成对象,而不是可以在其上执行更多分析(或编译器情况下的代码生成)的语法树。
而且,词法分析独立于解析器的好处在于,这两个代码都比较简单,只关注特定的元素。
分析数组
解析数组需要解析数组成员,并期望它们之间有一个逗号标记或指示数组结尾的右圆括号。
def parse_array(tokens):
json_array = []
t = tokens[0]
if t == JSON_RIGHTPAREN:
return json_array, tokens[1:]
while True:
json, tokens = parse(tokens)
json_array.append(json)
t = tokens[0]
if t == JSON_RIGHTPAREN:
return json_array, tokens[1:]
elif t != JSON_COMMA:
raise Exception('Expected comma after object in array')
else:
tokens = tokens[1:]
raise Exception('Expected end-of-array round bracket')
解析对象
解析对象是一个解析由冒号内部分隔的键值对,外部用逗号分隔的对象,直到到达对象的末尾。
def parse_object(tokens):
json_object = {}
t = tokens[0]
if t == JSON_RIGHTBRACKET:
return json_object, tokens[1:]
while True:
json_key = tokens[0]
if type(json_key) is str:
tokens = tokens[1:]
else:
raise Exception('Expected string key, got: {}'.format(json_key))
if tokens[0] != JSON_COLON:
raise Exception('Expected colon after key in object, got: {}'.format(t))
json_value, tokens = parse(tokens[1:])
json_object[json_key] = json_value
t = tokens[0]
if t == JSON_RIGHTBRACKET:
return json_object, tokens[1:]
elif t != JSON_COMMA:
raise Exception('Expected comma after pair in object, got: {}'.format(t))
tokens = tokens[1:]
raise Exception('Expected end-of-object bracket')
现在解析器代码已经完成!查看 代码的 pj / parser.py作为一个整体。为了提供理想的界面,创建from_string包装lex和parse功能的函数。
def from_string(string):
tokens = lex(string)
return parse(tokens)[0]
编写完毕!文章来源:黑客周刊