高手教您编写简单的JSON解析器
编写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]
编写完毕!文章来源:黑客周刊
高手教您编写简单的JSON解析器的更多相关文章
- 自己动手实现一个简单的JSON解析器
1. 背景 JSON(JavaScript Object Notation) 是一种轻量级的数据交换格式.相对于另一种数据交换格式 XML,JSON 有着诸多优点.比如易读性更好,占用空间更少等.在 ...
- 一个简单的json解析器
实现一个简单地json解析器. 两部分组成,词法分析.语法分析 词法分析 package com.mahuan.json; import java.util.LinkedList; import ja ...
- 用c#自己实现一个简单的JSON解析器
一.JSON格式介绍 JSON(JavaScript Object Notation) 是一种轻量级的数据交换格式.相对于另一种数据交换格式 XML,JSON 有着很多优点.例如易读性更好,占用空间更 ...
- 如何编写一个JSON解析器
编写一个JSON解析器实际上就是一个函数,它的输入是一个表示JSON的字符串,输出是结构化的对应到语言本身的数据结构. 和XML相比,JSON本身结构非常简单,并且仅有几种数据类型,以Java为例,对 ...
- 一起写一个JSON解析器
[本篇博文会介绍JSON解析的原理与实现,并一步一步写出来一个简单但实用的JSON解析器,项目地址:SimpleJSON.希望通过这篇博文,能让我们以后与JSON打交道时更加得心应手.由于个人水平有限 ...
- 用ExpressionTree实现JSON解析器
今年的春节与往年不同,对每个人来说都是刻骨铭心的.突入其来的新型冠状病毒使大家过上了“梦想”中的生活:吃了睡,睡了吃,还不用去公司上班,如今这样的生活就在我们面前,可一点都不踏实,只有不停的学习才能让 ...
- 面试题|手写JSON解析器
这周的 Cassidoo 的每周简讯有这么一个面试题:: 写一个函数,这个函数接收一个正确的 JSON 字符串并将其转化为一个对象(或字典,映射等,这取决于你选择的语言).示例输入: fakePars ...
- 手写Json解析器学习心得
一. 介绍 一周前,老同学阿立给我转了一篇知乎回答,答主说检验一门语言是否掌握的标准是实现一个Json解析器,网易游戏过去的Python入门培训作业之一就是五天时间实现一个Json解析器. 知乎回答- ...
- SLAM+语音机器人DIY系列:(二)ROS入门——5.编写简单的消息发布器和订阅器
摘要 ROS机器人操作系统在机器人应用领域很流行,依托代码开源和模块间协作等特性,给机器人开发者带来了很大的方便.我们的机器人“miiboo”中的大部分程序也采用ROS进行开发,所以本文就重点对ROS ...
随机推荐
- BZOJ 2726: [SDOI2012]任务安排 斜率优化 + 凸壳二分 + 卡精
Code: #include<bits/stdc++.h> #define setIO(s) freopen(s".in","r",stdin) # ...
- POJ 1511 Invitation Cards ( 双向单源最短路 || 最小来回花费 )
题意 : 给出 P 个顶点以及 Q 条有向边,求第一个点到其他各点距离之和+其他各点到第一个点的距离之和的最小值 分析 : 不难看出 min( 第一个点到其他各点距离之和+其他各点到第一个点的距离之和 ...
- C#通过文件头判断文件的类型(不是后缀名)
FileStream fs=new FileStream(@"D:\6",FileMode.Open,FileAccess.Read); BinaryReader reader= ...
- Magic Line
Magic Line 玄学过题系列,随机选在所有点左下方的点,然后对其他点斜率排序,取斜率在中间两个点之间 比赛时,左下方点不够随机==,导致没卡过去 #include<bits/stdc++. ...
- JQuery触发hover事件无效时使用js原生的触发事件方法
需求:在开发一个从微信公众号后台管理网页上爬取数据的chrome插件时,有部分页面元素是只显示了部分摘要信息的,需要把鼠标移上去后才能显示全部信息(类似title的弹出显示).这就需要在chrome插 ...
- 不间断电源(UPS)
UPS电源一般指不间断电源 UPS(Uninterruptible Power System/Uninterruptible Power Supply),即不间断电源,是将蓄电池(多为铅酸免维护蓄电池 ...
- mysqldump - 备份 MySQL 数据库
参考资料 备份 备份所有数据库中的所有数据(包括系统数据库) –all-databases 通过 --all-databases 选项备份所有的数据库: mysqldump -uroot -p --a ...
- ruby基本语法(2)
关于数组 Ruby数组中的数据类型可以不相同并且长度也是可变的.(好聪明啊感觉用的久了就会变笨了,除非你本来就是老手)比如下面的例子 Myarray=[1,2,“ruby”] Ruby也支持那种-1的 ...
- Spring自动装配之依赖注入(DI)
依赖注入发生的时间 当Spring IOC 容器完成了Bean 定义资源的定位.载入和解析注册以后,IOC 容器中已经管理类Bean定义的相关数据,但是此时IOC 容器还没有对所管理的Bean 进行依 ...
- golang的数据类型之浮点类型
[root@node3 shangxuetang]# cat float.go package main import "fmt" //演示golang中小数类型使用func ma ...