You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.17 01:44:56