使用Lark创建JSON解析器
本文来自 Lark 官方教程《JSON parser - Tutorial》
Lark 是一个 Python 实现的语法解析器,接受语法和文本,并生成表示该文本的结构树。
本文使用 Lark 创建 JSON 解析器,简要说明 Lark 的使用方法。
语法
Lark 支持 EBNF 语法格式。 基本格式如下:
rule_name : list of rules and TERMINALS to match
| another possible list of items
| etc.
TERMINAL: "some text to match"
terminal 是一个字符串或一个正则表达式。
对于每条规则,解析器会通过顺序匹配它的所有项目 (右侧),尝试每个备选方案。
JSON 示例中,规则结构比较简单:一个 json 文档要么是一个列表,要么是一个字典,要么是一个字符串/数字/等等。 字典和列表是递归的,包含其它 json 文档 (或称为 “values”)。
使用 EBNF 格式写上述结构:
value: dict
| list
| STRING
| NUMBER
| "true" | "false" | "null"
list : "[" [value ("," value)*] "]"
dict : "{" [pair ("," pair)*] "}"
pair : STRING ":" value
语法解释:
- 括号 (
(...)
) 将规则组合到一起 rule*
表示任意数量,即 0 或多个规则实例[rule]
(或rule?
) 表示可选,即 0 或 1 个规则实例rule+
表示 1 或多个规则实例
上述规则中的 STRING
和 NUMBER
尚未定义,可以使用 Lark 通用库中预定义的规则:
%import common.ESCAPED_STRING -> STRING
%import common.SIGNED_NUMBER -> NUMBER
箭头 (->
) 重命名 terminals。
但在这种情况下只会增加模糊性,因此后续我们将只使用原始名称。
还需要考虑作为文本一部分的空白字符,我们通过简单地匹配然后将其丢弃来处理它们。 我们告诉解析器忽略空白,否则我们必须在语法定义中添加 WS terminals。
%import common.WS
%ignore WS
注意上述规则中 TERMINALS 使用大写字母,rules 使用小写字母。
创建解析器
创建 Lark 对象
from lark import Lark
json_parser = Lark("""
value: dict
| list
| ESCAPED_STRING
| SIGNED_NUMBER
| "true" | "false" | "null"
list : "[" [value ("," value)*] "]"
dict : "{" [pair ("," pair)*] "}"
pair : ESCAPED_STRING ":" value
%import common.ESCAPED_STRING
%import common.SIGNED_NUMBER
%import common.WS
%ignore WS
""", start="value")
测试
text = '{"key": ["item0", "item1", 3.14, true]}'
t = json_parser.parse(text)
print(t)
Tree(Token('RULE', 'value'), [Tree(Token('RULE', 'dict'), [Tree(Token('RULE', 'pair'), [Token('ESCAPED_STRING', '"key"'), Tree(Token('RULE', 'value'), [Tree(Token('RULE', 'list'), [Tree(Token('RULE', 'value'), [Token('ESCAPED_STRING', '"item0"')]), Tree(Token('RULE', 'value'), [Token('ESCAPED_STRING', '"item1"')]), Tree(Token('RULE', 'value'), [Token('SIGNED_NUMBER', '3.14')]), Tree(Token('RULE', 'value'), [])])])])])])
print(t.pretty())
value
dict
pair
"key"
value
list
value "item0"
value "item1"
value 3.14
value
Lark 自动创建表示被解析文本的结构树。
Lark 根据以下条件自动从树中过滤文字:
- 过滤掉没有名称或者名称以下划线 (
_
) 开头的字符串文字 - 保留正则表达式,及时未命名,除非它们的名称以下划线开头
注意到上面结果中 "true"
被过滤掉了,下一节将进一步完善结构树以解决这个问题。
完善结构树
上面示例可以创建一个解析树 (或:AST),但有几个问题:
- “true”,“false” 和 “null” 被过滤掉了
- 有无用的分支,比如 value,会混淆显示结果
解决方案
?value: dict
| list
| string
| SIGNED_NUMBER -> number
| "true" -> true
| "false" -> false
| "null" -> null
...
string : ESCAPED_STRING
箭头 (
->
) 表示别名 alias。别名是规则特定部分的名称。 在本例中命名匹配的 true/false/null,不会丢失信息。 同时也命名 SIGNED_NUMBER 方便后续处理。value 前的问号 (
?value
) 告诉树生成器在只有一个成员的情况下将该分支内联 (inline)。 在本例中,value 只可能有一个成员,所以总会被内联。将 terminal
ESCAPED_STRING
变为 rule,会在树中表示为分支。 与 alias 等价,但string
也可以用在语法的其它地方。
新语法代码:
json_parser = Lark(r"""
?value: dict
| list
| string
| SIGNED_NUMBER -> number
| "true" -> true
| "false" -> false
| "null" -> null
list: "[" [value ("," value)*] "]"
dict: "{" [pair ("," pair)*] "}"
pair: string ":" value
string: ESCAPED_STRING
%import common.ESCAPED_STRING
%import common.SIGNED_NUMBER
%import common.WS
%ignore WS
""", start="value")
测试输出
text = '{"key": ["item0", "item1", 3.14, true]}'
t = json_parser.parse(text)
print(t)
Tree(Token('RULE', 'dict'), [Tree(Token('RULE', 'pair'), [Tree(Token('RULE', 'string'), [Token('ESCAPED_STRING', '"key"')]), Tree(Token('RULE', 'list'), [Tree(Token('RULE', 'string'), [Token('ESCAPED_STRING', '"item0"')]), Tree(Token('RULE', 'string'), [Token('ESCAPED_STRING', '"item1"')]), Tree('number', [Token('SIGNED_NUMBER', '3.14')]), Tree('true', [])])])])
将上述输出重新格式化,可以看到更明显的树形结构:
Tree(Token('RULE', 'dict'), [
Tree(Token('RULE', 'pair'), [
Tree(Token('RULE', 'string'), [Token('ESCAPED_STRING', '"key"')]),
Tree(Token('RULE', 'list'), [
Tree(Token('RULE', 'string'), [Token('ESCAPED_STRING', '"item0"')]),
Tree(Token('RULE', 'string'), [Token('ESCAPED_STRING', '"item1"')]),
Tree('number', [Token('SIGNED_NUMBER', '3.14')]),
Tree('true', [])
])
])
])
print(t.pretty())
dict
pair
string "key"
list
string "item0"
string "item1"
number 3.14
true
可以看到 value
被内联,true
被正常识别,字符串 (string
) 和数字 (number
) 都有使用了别名
评估结构树
有棵树非常好,但我们真正想要的是一个 JSON 对象。 实现这一操作的方法是使用 Transformer 评估树。
转换器是一个类,类方法对应分支名。 对于每个分支,相应的方法会被调用,将分支的子节点作为参数,返回值会替换树中的分支。
下面是一个部分转换器,处理列表和字典:
from lark import Lark, Transformer
class MyTransformer(Transformer):
def list(self, items):
return list(items)
def pair(self, key_value):
k, v = key_value
return k, v
def dict(self, items):
return dict(items)
测试:
text = '{"key": ["item0", "item1", 3.14, true]}'
t = json_parser.parse(text)
f = MyTransformer().transform(t)
import pprint
pprint.pprint(f)
{Tree(Token('RULE', 'string'), [Token('ESCAPED_STRING', '"key"')]): [Tree(Token('RULE', 'string'), [Token('ESCAPED_STRING', '"item0"')]),
Tree(Token('RULE', 'string'), [Token('ESCAPED_STRING', '"item1"')]),
Tree('number', [Token('SIGNED_NUMBER', '3.14')]),
Tree('true', [])]}
上述 f
是一个字典。
完整的转换器如下所示,为每个非内联的规则都编写一个函数,而内联规则 (value) 因为仅可能有 1 个成员而一定内联,不需要再编写对应函数。
class TreeToJson(Transformer):
def string(self, s):
(s, ) = s
return s[1: -1]
def number(self, n):
(n, ) = n
return float(n)
list = list
pair = tuple
dict = dict
null = lambda self, _: None
true = lambda self, _: True
false = lambda self, _: False
函数中第二个参数是一个列表。 注意对字符串的处理,将引号去掉。
测试
text = '{"key": ["item0", "item1", 3.14, true]}'
t = json_parser.parse(text)
f = MyTransformer().transform(t)
print(f)
{'key': ['item0', 'item1', 3.14, True]}
返回解析后的 JSON 对象。
下图展示生成结构树和转换为 Dict 对象的过程:
左图:解析JSON字符串生成的结构树;右图:转换器生成的 Dict 对象
参考
Lark 源代码:https://github.com/lark-parser/lark