使用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 或多个规则实例

上述规则中的 STRINGNUMBER 尚未定义,可以使用 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
  1. 箭头 (->) 表示别名 alias。别名是规则特定部分的名称。 在本例中命名匹配的 true/false/null,不会丢失信息。 同时也命名 SIGNED_NUMBER 方便后续处理。

  2. value 前的问号 (?value) 告诉树生成器在只有一个成员的情况下将该分支内联 (inline)。 在本例中,value 只可能有一个成员,所以总会被内联。

  3. 将 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 对象

参考

官方教程:JSON parser - Tutorial

Lark 源代码:https://github.com/lark-parser/lark