python Lark教程 2)基础语法与解析
在第一章中,我们已经写过一个小巧的四则运算解析器。那只是“冰山一角”。如果你想真正掌握 Lark,就必须深入理解它的 语法规则。
本章将带你逐步认识 Lark 的语法系统:
- 规则的基本结构
- 终结符与非终结符
- 正则表达式在 Lark 中的使用
- 可选、重复、递归 等语法特性
- 解析树的生成与使用
2.1 Lark 语法文件结构
Lark 的语法采用接近 EBNF(扩展巴科斯范式) 的形式定义规则。一个语法文件通常包含以下几个部分:
- 起始规则(start):程序入口。
- 非终结符:由其他规则组合而成。
- 终结符(Token):由正则表达式或具体字符匹配。
- 特殊指令:如
%import、%ignore。
示例
start: expr
?expr: expr "+" term -> add
| expr "-" term -> sub
| term
?term: term "*" factor -> mul
| term "/" factor -> div
| factor
?factor: NUMBER
| "(" expr ")"
%import common.NUMBER
%import common.WS
%ignore WS
这里我们可以看出:
start是入口。expr、term、factor是非终结符。NUMBER是终结符,由 Lark 内置的正则规则导入。%ignore WS让我们忽略空格。
2.2 终结符(Token)
在 Lark 中,终结符用大写字母命名。
它们通常用正则表达式来定义。
示例:定义一个标识符
IDENTIFIER: /[a-zA-Z_][a-zA-Z0-9_]*/
示例:定义数字
NUMBER: /\d+(\.\d+)?/
特殊的 %import
Lark 内置了很多常用的 Token,可以直接导入:
%import common.INT // 整数
%import common.FLOAT // 浮点数
%import common.WS // 空格
%import common.CNAME // 类似 Python 标识符
忽略空格
%ignore WS
这样就不用在语法里到处写空格规则了。
2.3 非终结符
非终结符用小写字母命名。
它们是语法的核心,用来组合终结符和其他非终结符。
示例:表达式
expr: term "+" term
可选的问号(?)
在规则名前加 ?,表示解析树中是否保留该规则。
?expr: term "+" term
这里的 expr 会在生成的解析树中 自动折叠,简化输出。
2.4 操作符与优先级
复杂语法常常需要处理 运算优先级。
在 Lark 中,可以通过规则的递归定义实现优先级。
示例:四则运算
?expr: expr "+" term -> add
| expr "-" term -> sub
| term
?term: term "*" factor -> mul
| term "/" factor -> div
| factor
?factor: NUMBER
| "(" expr ")"
这里:
expr最高层(处理加减)。term中层(处理乘除)。factor最底层(数字或括号)。
因此,1 + 2 * 3 会被正确解析为 1 + (2 * 3)。
2.5 正则表达式与 Lark
Lark 支持标准的 Python 正则表达式,用于定义终结符。
常见例子
-
数字:
NUMBER: /\d+/ -
字母开头的变量:
VAR: /[a-zA-Z_][a-zA-Z0-9_]*/ -
字符串:
STRING: /"[^"]*"/ | /'[^']*'/
注意事项
- 正则写在
/.../之间。 .代表任意字符,*代表重复。- 避免写过度复杂的正则,解析器效率会下降。
2.6 可选、重复与递归
Lark 提供了 EBNF 风格的语法修饰符,非常适合表达常见规则。
可选(?)
signed_number: "-"? NUMBER
表示可以有符号,也可以没有。
零次或多次(*)
items: item*
表示可以有零个或多个 item。
一次或多次(+)
items: item+
表示至少有一个 item。
递归(嵌套结构)
list: "[" [elements] "]"
elements: value ("," value)*
可以解析 ["a", "b", "c"] 这种结构。
2.7 示例:解析一个 JSON 子集
现在我们来实现一个 简化版 JSON 解析器。
语法
?start: value
?value: STRING
| NUMBER
| object
| array
| "true" -> true
| "false" -> false
| "null" -> null
array : "[" [value ("," value)*] "]"
object : "{" [pair ("," pair)*] "}"
pair : STRING ":" value
%import common.CNAME
%import common.WS
%import common.NUMBER
%import common.ESCAPED_STRING -> STRING
%ignore WS
解析
from lark import Lark
json_parser = Lark(grammar, parser="lalr")
tree = json_parser.parse('{"name": "Alice", "age": 25, "isStudent": false}')
print(tree.pretty())
输出:
object
pair
STRING "name"
STRING "Alice"
pair
STRING "age"
NUMBER 25
pair
STRING "isStudent"
false
转换为 Python 对象
from lark import Transformer
class JsonTransformer(Transformer):
def STRING(self, s):
return s[1:-1] # 去掉引号
def NUMBER(self, n):
return float(n)
def pair(self, kv):
return (kv[0], kv[1])
def object(self, items):
return dict(items)
def array(self, items):
return list(items)
def true(self, _):
return True
def false(self, _):
return False
def null(self, _):
return None
transformer = JsonTransformer()
result = transformer.transform(tree)
print(result)
输出:
{'name': 'Alice', 'age': 25.0, 'isStudent': False}
这样,我们就写了一个迷你 JSON 解析器。
2.8 解析树的调试与可视化
在开发语法时,调试是很重要的环节。
Lark 提供了几种常见调试方式:
-
tree.pretty()
直接打印树状结构。 -
print(tree)
得到紧凑的表示。 -
保存为 Graphviz 格式:
tree.save("tree.dot")然后用
dot -Tpng tree.dot -o tree.png转换为图片。
2.9 常见错误与排查
1. UnexpectedToken
- 说明输入文本与语法不匹配。
- 解决:检查正则或规则书写。
2. Infinite Recursion
- 语法递归写错,导致死循环。
- 解决:确保规则能“收敛”。
3. 多义性(Ambiguity)
- 一个输入有多个可能解析树。
- 解决:加上优先级规则,或使用
lalr算法。
2.10 小结
在本章中,我们学习了:
- Lark 语法文件的基本结构。
- 终结符(Token)的定义与正则表达式用法。
- 非终结符的定义与可选折叠。
- EBNF 特性:可选、重复、递归。
- 一个完整的 JSON 子集解析器示例。
- 如何调试与排查常见错误。
到这里,你已经能够 自己写一个完整的语法,并将输入文本解析为结构化数据。
更多推荐


所有评论(0)