Engine 引擎——转换器、模板与错误¶
概述¶
engine 模块是 InfinityParse 的运行时后盾:定义 Converter(字段级配置数据)、
Field(字段的转换器分支集合)、Template(编译后的模板)与错误层级。高层入口
scanner 在构造时把注解元数据归一化为 Field 序列,再交给
Template 编译。
Converter——字段级配置¶
@dataclass(frozen=True)
class Converter[T]:
fn: Callable[[str], T] # 转换函数:把捕获文本转成 T
pattern: str = r".+?" # 捕获模式(默认惰性、非空、单行)
strip: bool = True # 转换前是否 strip 捕获文本
name: str | None = None # 错误信息里的显示名
flags: int = 0 # 作用于 pattern 的 re 标志
| 参数 | 类型 | 默认 | 说明 |
|---|---|---|---|
fn |
Callable[[str], T] |
(必需) | 转换函数。抛出的任何异常都会被包装为 ConvertError。 |
pattern |
str |
.+? |
该分支的捕获模式;同一字段的多条分支各有自己的模式。 |
strip |
bool |
True |
转换前对捕获文本做 strip;False 保留原始空白(如 t.Whitespace 需要保留空白)。 |
name |
str \| None |
None |
错误信息里的显示名;缺省回退到 fn.__name__。 |
flags |
int |
0 |
re 标志,作用于该分支自己的模式。支持内联子集 i / m / s / x / a;超出子集的位在编译时抛 ValueError。 |
用法示例:
import json
import re
from typing import Annotated
from infinity_parse import Converter, scan
# 自定义转换函数(例如 JSON 而不是 Python 字面量)
scan[Annotated[dict[str, str], Converter(json.loads, name="json")]]("{}")(
'{"key": "value"}'
) # ({"key": "value"},)
# 自定义模式 + 显示名(让错误信息更好读)
scan[Annotated[int, Converter(int, pattern=r"\d+", name="age")]]("age={}")(
"age=42"
) # (42,)
# strip=False:保留字段内的原始空白
scan[Annotated[str, Converter(str, strip=False)]]("{}!")("hi !") # ("hi ",)
# flags:作用于该分支自己的模式
scan[Annotated[str, Converter(str, pattern=r"[a-z]+", flags=re.IGNORECASE)]]("{}")(
"HeLLo"
) # ("HeLLo",)
Converter是冻结数据类:可安全保存、复用;它参与模板编译缓存,因此fn必须可哈希。
感知转义的字符串字段¶
把模式扩展为转义感知,再配合 json.loads 解码转义序列;非法转义由转换器硬失败
(不会静默吞掉):
import json
from typing import Annotated
from infinity_parse import Converter, scan
parse_name = scan[
Annotated[str, Converter(json.loads, pattern=r'"(?:[^"\\]|\\.)*"', name="name")]
]("{}")
parse_name(r'"kim \"the fox\""') # ('kim "the fox"',)
parse_name(r'"C:\\path\\to\\file"') # ('C:\\path\\to\\file',)
捕获大文本块¶
默认模式 .+? 是惰性的——为「字段后还有锚点」的场景设计,逐字符扩展以找到最短
匹配。对没有后续锚点的大段捕获(如文档正文),惰性量词在 CPython 上代价很高:
1 MB 捕获实测约 8 ms,而显式的贪婪 DOTALL 模式约 1 µs:
from typing import Annotated
from infinity_parse import Converter, scan
Blob = Annotated[str, Converter(str, pattern=r"(?s:.+)")]
parse_doc = scan[Blob]("{}")
parse_doc("任意大小的文本…") # 大输入下开销几乎可忽略
不要修改默认模式——最短匹配(惰性)语义是模板行为的基石。
转换器里嵌扫描器¶
Converter.fn 可以是另一个扫描器——字段整体先由外层模式捕获,再交给内层扫描器解析:
from typing import Annotated
from infinity_parse import Converter, scan
point = scan[int, int]("{} {}")
parse = scan[
Annotated[
tuple[int, int], Converter(point, pattern=r"(?<=\[).+?(?=\])", name="point")
]
]("point=[{}]")
parse("point=[1 2]") # ((1, 2),)
内层失败会以 ConvertError 的形式冒泡,__cause__ 链指向内层 ScanError。
Field——转换器分支集合¶
@dataclass(frozen=True)
class Field:
converters: tuple[Converter[Any], ...] # 按竞争顺序排列;至少一条
- 单分支字段:该分支即字段的全部行为。
- 多分支字段:按声明顺序竞争;首个模式匹配的分支胜出;胜出分支转换失败则整个 扫描失败(不回退)。
- 多分支来自
OneOf或|联合(见 scanner.md)。 Field与Converter一样必须可哈希。
Template——编译后的模板¶
class Template:
def __init__(self, template: str, fields: tuple[Field, ...]) -> None
def fullmatch(self, input_value: str) -> tuple[Any, ...]
def search(self, input_value: str) -> tuple[Any, ...] | None
def findall(self, input_value: str) -> list[tuple[Any, ...]]
构造时一次性完成全部校验,之后匹配无解析开销:
| 校验 | 失败类型 | 信息 |
|---|---|---|
| 占位符数量与字段数量不符 | ValueError |
... has N placeholder(s) but M converter type(s) were given |
单个 { / } |
ValueError |
single '{' in template ... |
| 转换器不可调用 | TypeError |
converters must be callable |
| 字段没有转换器 | ValueError |
each field requires at least one converter |
| 模式非法(正则错误) | re.error |
由 re.compile 抛出 |
| 转换器不可哈希 | TypeError |
— |
编译细节(了解即可):
- 模板被切分为「字面段」与「占位符」;字面段做
re.escape,其中空白串转为\s+。 - 每个字段生成一个命名分组
s0/s1/ …;多分支时每个分支再各有一个命名分组 (s0b0、s0b1…)用于判定胜出分支。 flags通过作用域内联标志(?i:...)绑定到分支自己的模式,不会污染其它分支。- 编译带 LRU 缓存(最多 1024 个模板)。
错误模型¶
graph TD
V[ValueError] --> S[ScanError]
S --> M["MatchError —— 软失败"]
S --> C["ConvertError —— 硬失败"]
| 错误 | 含义 | 典型触发 | 会被 alt 吞掉吗 |
|---|---|---|---|
MatchError |
输入与模板不匹配 | 全串匹配失败 / 搜索无果 | ✅ 会(alt 继续尝试其它分支) |
ConvertError |
字段已匹配但转换失败 | 转换器抛出异常、非法转义等 | ❌ 不会(硬失败,立即中止) |
ConvertError 的信息包含字段序号、文本、偏移与显示名:
scan[int, int]("{} {}")("1 zz")
# ConvertError: cannot convert field 2 ('zz') at offset 2 using int
| 片段 | 含义 |
|---|---|
field 2 |
第 2 个 {}(从 1 开始) |
'zz' |
strip 后用于转换的文本 |
offset 2 |
该字段捕获文本在原始输入中的起始下标(scan 自动补偿 strip 造成的位移) |
using int |
分支显示名(Converter.name 或 fn.__name__) |
- 原始异常保存在
__cause__:如ValueError、SyntaxError、json.JSONDecodeError。 search/findall的偏移同样相对原始输入(不补偿 strip)。- 嵌套扫描器的错误会形成
__cause__链。
两个异常还同时暴露等价的结构化属性,无需解析消息字符串即可程序化消费:
| 异常 | 结构化属性 |
|---|---|
MatchError |
input_value、template(未知时为 None) |
ConvertError |
field、offset、text、converter、input_value、template |
from infinity_parse import ConvertError, scan
try:
scan[int, int]("{} {}")("1 zz")
except ConvertError as error:
error.field # 2
error.offset # 2
error.text # 'zz'
error.converter # 'int'
alt 抛出的 MatchError 同样携带 input_value(见 combinators.md)。
低层构建¶
需要绕开 scan[...] 直接构建时,组合 resolve_field / Field / Template 即可:
from infinity_parse import Converter, Template, resolve_field
from infinity_parse.engine import Field
field = resolve_field(int, [Converter(int, pattern=r"\d+", name="x")])
template = Template("x={}", (field,))
template.fullmatch("x=42") # (42,)
template.search("x=42!") # (42,)
template.findall("x=1, x=2") # [(1,), (2,)]
也可以手工构造分支:
field = Field(converters=(Converter(int, pattern=r"\d+", name="x"),))
注意事项¶
- 转换失败永远不回退:需要「多模式尝试」请显式使用
OneOf分支,并确保每条分支的 模式写准。 flags只支持i/m/s/x/a五个内联标志;其余标志位报ValueError。Template直接使用时返回tuple[Any, ...];要获得精确的推导类型请使用 scanner 入口。Field/Converter是冻结数据类,可安全地作为常量复用。