
本文介绍如何利用 pyparsing 的 IndentedBlock 类精准解析具有嵌套缩进结构的键值对文本(如 key=1, sub=2, foo=bar),避免因语法歧义导致的层级错乱,实现语义正确的树状解析结果。
本文介绍如何利用 pyparsing 的 `indentedblock` 类精准解析具有嵌套缩进结构的键值对文本(如 `key=1`, ` sub=2`, ` foo=bar`),避免因语法歧义导致的层级错乱,实现语义正确的树状解析结果。
在处理类似配置文件或领域特定语言(DSL)时,缩进常用于表达层级关系(如 YAML 或 Python 风格),但 pyparsing 默认忽略空白,无法直接区分“同级缩进”与“子级缩进”。原始尝试中,将 map=1 和 sub=1 均视为可能触发新缩进块的“定义”,导致 int=99 被错误归入 sub=1 的子块后又因后续 sub=2 出现而中断上下文,最终生成混乱的嵌套结构。
核心解决方案是:弃用手工组合 Forward + IndentedBlock 的模糊匹配逻辑,转而使用 IndentedBlock 对齐真实缩进语义——即仅当某行缩进严格大于其父级时,才视为该父级的子内容。IndentedBlock 内置缩进跟踪机制,能自动识别缩进层级变化,无需手动管理递归或负向前瞻。
以下为推荐实现(兼容 pyparsing v3.1+):
import pyparsing as pp
from pyparsing import common as ppc
from pprint import pprint
data = """\
map=1
sub=1
int=99
foo=bar
sub=2
foo=bar
int=99
bar=qux
"""
# 定义基础元素
EQ = pp.Suppress('=')
key = pp.Word(pp.alphas, pp.alphanums)
value = ppc.number | pp.Word(pp.alphanums)
# 每个键值对作为独立、可命名的组
key_value = pp.Group(key("key") + EQ + value("value"))
# 关键:使用 IndentedBlock 构建层级结构
# recursive=True 允许多层嵌套;grouped=True 自动为每级缩进块包裹为 Group
structure = pp.IndentedBlock(key_value, recursive=True, grouped=True)
parsed = structure.parse_string(data)
pprint(parsed.as_list(), width=32)
输出结果为:
[[['map', 1],
[['sub', 1],
[['int', 99],
['foo', 'bar']],
['sub', 2],
[['foo', 'bar'],
['int', 99],
['bar', 'qux']]]]]
该结构清晰反映语义层级:map=1 是顶层项;其下两个 sub= 条目为同级子项;每个 sub 后续缩进更深的行(如 int=99)才属于其直接子内容。key 和 value 的结果名还支持便捷访问,例如 parsed[0][1][0]["key"] 返回 "sub"。
⚠️ 注意事项:
-
IndentedBlock要求输入文本必须使用空格缩进(不支持 Tab 混合),且缩进需严格递增(如 2→4→6 空格),不可跳变(如 2→6); - 若需支持 Tab 或混合缩进,应先预处理统一为空格(如
text.replace('\t', ' ')); -
recursive=True是必需的,否则仅解析一级缩进; - 避免在
IndentedBlock外部再包裹ZeroOrMore或OneOrMore——IndentedBlock已内置重复解析逻辑。
通过 IndentedBlock,我们绕开了传统基于 Forward 和负向断言的复杂消歧逻辑,转而依托 pyparsing 对缩进语义的原生建模,使解析器更鲁棒、可读性更强,也更贴近人类对缩进结构的直觉理解。











