pyparsing中嵌套结构的技巧
我在用PyParsing解析嵌套结构时遇到了困难。我查阅了很多关于PyParsing的'嵌套'示例,但还是不知道怎么解决我的问题。
这是我内部结构的样子:
texture_unit optionalName
{
texture required_val
prop_name1 prop_val1
prop_name2 prop_val1
}
而这是我外部结构的样子,不过它可以包含零个或多个内部结构。
pass optionalName
{
prop_name1 prop_val1
prop_name2 prop_val1
texture_unit optionalName
{
// edit 2: showing use of '.' character in value
texture required_val.file.name optional_val // edit 1: forgot this line in initial post.
// edit 2: showing potentially multiple values
prop_name3 prop_val1 prop_val2
prop_name4 prop_val1
}
}
我已经成功解析了内部结构。以下是我为此写的代码。
prop_ = pp.Group(pp.Word(pp.alphanums+'_')+pp.Group(pp.OneOrMore(pp.Word(pp.alphanums+'_'+'.'))))
texture_props_ = pp.Group(pp.Literal('texture') + pp.Word(pp.alphanums+'_'+'.')) + pp.ZeroOrMore(prop_)
texture_ = pp.Forward()
texture_ << pp.Literal('texture_unit').suppress() + pp.Optional(pp.Word(pp.alphanums+'_')).suppress() + pp.Literal('{').suppress() + texture_props_ + pp.Literal('}').suppress()
这是我尝试解析外部结构的代码,
pass_props_ = pp.ZeroOrMore(prop_)
pass_ = pp.Forward()
pass_ << pp.Literal('pass').suppress() + pp.Optional(pp.Word(pp.alphanums+'_'+'.')).suppress() + pp.Literal('{').suppress() + pass_props_ + pp.ZeroOrMore(texture_) + pp.Literal('}').suppress()
当我执行:
pass_.parseString( testPassStr )控制台显示了“}”是预期的错误。
我觉得这和C结构示例非常相似,但我不确定缺少了什么关键部分。我也想知道在使用nestedExpr时,如何控制最终的数据结构。
2 个回答
这里有两个问题:
- 在你的语法中,你把
texture
这个字面量标记为在texture_unit
块中是必须的,但在你的第二个例子中没有出现texture
。 - 在第二个例子中,
pass_props_
和texture_unit optionalName
重复了。之后,pp.Literal('}')
期待的是}
,但实际上给的是{
。这就是错误的原因。
我们可以通过这样修改 pass_
规则来检查这个问题:
pass_ << pp.Literal('pass').suppress() + pp.Optional(pp.Word(pp.alphanums+'_'+'.')).suppress() + \
pp.Literal('{').suppress() + pass_props_
print pass_.parseString(s2)
这样会给我们以下输出:
[['prop_name', ['prop_val', 'prop_name', 'prop_val', 'texture_unit', 'optionalName']]]
我们可以看到 pass_props_
和 texture_unit optionalName
是重复的。
所以,我们想要做的是:prop_
可以包含字母、数字、下划线 _
和点 .
,但不能与 texture_unit
字面量匹配。我们可以用 regex
和 负向前瞻 来实现:
prop_ = pp.Group( pp.Regex(r'(?!texture_unit)[a-z0-9_]+')+ pp.Group(pp.OneOrMore(pp.Regex(r'(?!texture_unit)[a-z0-9_.]+'))) )
最后,工作示例看起来是这样的:
import pyparsing as pp
s1 = '''texture_unit optionalName
{
texture required_val
prop_name prop_val
prop_name prop_val
}'''
prop_ = pp.Group( pp.Regex(r'(?!texture_unit)[a-z0-9_]+')+ pp.Group(pp.OneOrMore(pp.Regex(r'(?!texture_unit)[a-z0-9_.]+'))) )
texture_props_ = pp.Group(pp.Literal('texture') + pp.Word(pp.alphanums+'_'+'.')) + pp.ZeroOrMore(prop_)
texture_ = pp.Forward()
texture_ = pp.Literal('texture_unit').suppress() + pp.Word(pp.alphanums+'_').suppress() +\
pp.Literal('{').suppress() + pp.Optional(texture_props_) + pp.Literal('}').suppress()
print texture_.parseString(s1)
s2 = '''pass optionalName
{
prop_name1 prop_val1.name
texture_unit optionalName1
{
texture required_val1
prop_name2 prop_val12
prop_name3 prop_val13
}
texture_unit optionalName2
{
texture required_va2l
prop_name2 prop_val22
prop_name3 prop_val23
}
}'''
pass_props_ = pp.ZeroOrMore(prop_ )
pass_ = pp.Forward()
pass_ = pp.Literal('pass').suppress() + pp.Optional(pp.Word(pp.alphanums+'_'+'.')).suppress() +\
pp.Literal('{').suppress() + pass_props_ + pp.ZeroOrMore(texture_ ) + pp.Literal('}').suppress()
print pass_.parseString(s2)
输出:
[['texture', 'required_val'], ['prop_name', ['prop_val', 'prop_name', 'prop_val']]]
[['prop_name1', ['prop_val1.name']], ['texture', 'required_val1'], ['prop_name2', ['prop_val12', 'prop_name3', 'prop_val13']], ['texture', 'required_va2l'], ['prop_name2', ['prop_val22', 'prop_name3', 'prop_val23']]]
我想要的答案和使用“Forward”解析器有关,这在Cstruct的例子中有展示(链接在原帖中)。
定义嵌套结构的语法时,最难的部分是要定义所有可能的成员类型,这些成员类型还需要包括结构本身,而这个结构还没有被定义。
定义嵌套结构的pyparsing语法的“窍门”是延迟定义结构,但在定义结构成员时包含一个“前向声明”的版本,这样成员也可以包含结构。然后将结构语法完成为成员的列表。
struct = Forward()
member = blah | blah2 | struct
struct << ZeroOrMore( Group(member) )
这里也有讨论: Pyparsing:将半JSON嵌套的纯文本数据解析为列表
原帖(我的)描述的测试数据和语法不够具体,导致本该失败的地方却匹配上了。@NorthCat正确地发现了语法中不想要的匹配。然而,建议定义很多“负向前瞻”似乎难以管理。
与其定义不该匹配的内容,我的解决方案是明确列出所有可能的匹配。匹配的内容是成员关键字,使用'oneOf('用空格分隔的单词列表')。一旦我列出了所有可能的匹配,我意识到我的结构并不是嵌套结构,而实际上是一个有限深度的结构,每个深度都有不同的语法描述。所以,我的成员定义不需要使用前向声明的技巧。
我的成员定义的结束符和Cstruct例子中的不同。我的成员定义不是像C++那样以';'(分号)结束,而是需要在行末结束。在pyparsing中,你可以用'LineEnd'解析器来指定行的结束。所以,我将我的成员定义为不包括'LineEnd'的值列表,像这样,注意最后一个定义中使用的“非”(~)操作符:
EOL = LineEnd().suppress()
ident = Word( alphas+"_", alphanums+"_$@#." )
integer = Word(nums)
real = Combine(Optional(oneOf('+ -')) + Word(nums) + '.' + Optional(Word(nums)))
propVal = real | integer | ident
propList = Group(OneOrMore(~EOL + propVal))