如何在EBNF/Lark中处理可选Token的存在与缺失问题
解析铁路时刻表时间:Lark中可选元素的Transformer处理方案
背景与需求
刚接触Python的Lark库,想用EBNF解析器替代大量繁琐的if语句,当前任务是解析铁路时刻表的时间字符串:
12:34:列车停靠12/34:列车不停靠12d34:列车仅可下客(还有至少4种类似变体)12:34h:精确到半分钟,代表12:34:30,其中h的处理遇到了问题
已实现代码
数据类与枚举定义
from dataclasses import dataclass from enum import Enum, auto @dataclass(frozen=True) class TTime: class StopMode(Enum): STOPPING = auto() PASSING = auto() SET_DOWN = auto() # 其他模式省略 hour: int minute: int second: int = 0 stopmode: StopMode = StopMode.PASSING
Transformer实现
from lark import Lark, Transformer class TreeToTTime(Transformer): def hour(self, n): return int("".join(n)) def minute(self, n): return int("".join(n)) def stopmode(self, n): (n,) = n return n def stopping(self, _): return TTime.StopMode.STOPPING def passing(self, _): return TTime.StopMode.PASSING def setdown(self, _): return TTime.StopMode.SET_DOWN def halfminute(self, _): return True def ttime(self, args): hour, stopmode, minute, halfminute = args second = 30 if halfminute else 0 return TTime(hour, minute, second, stopmode)
解析器定义
ttime_parser = Lark( r""" ttime : hour stopmode minute halfminute? hour : ("0".."2")? DIGIT minute : ("0".."5") DIGIT stopmode : stopping | passing | setdown stopping : ":" passing : "/" setdown : "d" halfminute : "h" | "H" %import common.DIGIT %import common.WS %ignore WS """, start="ttime", ) def parse_ttime(text): tree = ttime_parser.parse(text) return TreeToTTime().transform(tree)
测试代码
import pytest @pytest.mark.parametrize( "text,expected", [ (" 0:00", TTime(0, 0, 0, TTime.StopMode.STOPPING)), ("13:20", TTime(13, 20, 0, TTime.StopMode.STOPPING)), ("00/00", TTime(0, 0, 0, TTime.StopMode.PASSING)), ("12d34", TTime(12, 34, 0, TTime.StopMode.SET_DOWN)), ("01:23h", TTime(1, 23, 30, TTime.StopMode.STOPPING)), ], ) def test_parse_times(text, expected): assert expected == parse_ttime(text)
核心问题
当前ttime方法的args参数长度不固定:
- 无
h时(如12:34),args为(12, StopMode.STOPPING, 34)(3个元素) - 有
h时(如12:34h),args为(12, StopMode.STOPPING, 34, True)(4个元素)
希望无论halfminute是否存在,args都能包含4个元素(缺失时halfminute为False),不想用hour, stopmode, minute, halfminute, *_ = args + (False,)这种依赖参数顺序的写法。
可行解决方案
方案1:显式判断参数长度
直接在ttime方法中根据args长度处理,逻辑清晰且不依赖参数顺序:
def ttime(self, args): hour, stopmode, minute = args[:3] halfminute = args[3] if len(args) == 4 else False second = 30 if halfminute else 0 return TTime(hour, minute, second, stopmode)
方案2:用语法规则封装可选元素
修改EBNF规则,把可选的halfminute封装成一个默认返回False的非终结符:
# 更新解析器规则 ttime_parser = Lark( r""" ttime : hour stopmode minute optional_halfminute hour : ("0".."2")? DIGIT minute : ("0".."5") DIGIT stopmode : stopping | passing | setdown stopping : ":" passing : "/" setdown : "d" halfminute : "h" | "H" optional_halfminute : halfminute | %empty %import common.DIGIT %import common.WS %ignore WS """, start="ttime", ) # 在Transformer中添加对应处理 class TreeToTTime(Transformer): # ... 其他方法不变 ... def optional_halfminute(self, args): # 有元素则返回True,无则返回False return bool(args) def ttime(self, args): hour, stopmode, minute, halfminute = args second = 30 if halfminute else 0 return TTime(hour, minute, second, stopmode)
这个方案把可选元素的默认值处理放到了语法层面,符合Lark的结构化解析思路,ttime方法可以直接解构固定长度的参数。
方案3:使用Lark的命名参数绑定
利用Lark的语法树属性绑定功能,直接给ttime传递命名参数,自动处理默认值:
# 更新解析器规则,使用->语法绑定属性 ttime_parser = Lark( r""" ttime : hour stopmode minute halfminute? -> ttime(hour=$hour, stopmode=$stopmode, minute=$minute, halfminute=$halfminute?) hour : ("0".."2")? DIGIT minute : ("0".."5") DIGIT stopmode : stopping | passing | setdown stopping : ":" passing : "/" setdown : "d" halfminute : "h" | "H" %import common.DIGIT %import common.WS %ignore WS """, start="ttime", ) # 修改Transformer的ttime方法,接收关键字参数 class TreeToTTime(Transformer): # ... 其他方法不变 ... def ttime(self, _, hour, stopmode, minute, halfminute=False): second = 30 if halfminute else 0 return TTime(hour, minute, second, stopmode)
这个方案最直观,直接通过命名参数获取值,缺失时自动使用默认的False,完全不需要关心参数顺序。
内容的提问来源于stack exchange,提问作者Jack Deeth
相关产品推荐
相关产品推荐

