Pyparsing:空格有时重要,有时不重要
我想为一个包含多个部分的文件创建一种语法(就像下面的PARAGRAPH一样)。
一个部分以它的关键词开始(比如PARAGRAPH),后面跟着一个标题,内容在接下来的几行中,每一行内容就是这一部分的一行。就像一个有标题、列和行的表格。
在下面的例子(tablefile)中,我会限制每个部分只有一列和一行。
Tablefile的自上而下的BNF:
tablefile := paragraph*
paragraph := PARAGRAPH title CR
TAB content
title, content := \w+
Pyparsing语法:
因为我需要处理换行和制表符,所以我需要把默认的空白字符设置为' '。
def grammar():
'''
Bottom-up grammar definition
'''
ParserElement.setDefaultWhitespaceChars(' ')
TAB = White("\t").suppress()
CR = LineEnd().setName("Carriage Return").suppress()
PARAGRAPH = 'PARAGRAPH'
title = Word(alphas)
content = Word(alphas)
paragraph = (PARAGRAPH + title + CR
+ TAB + content)
tablefile = OneOrMore(paragraph)
tablefile.parseWithTabs()
return tablefile
应用到例子中
这个简单的例子很容易匹配:
PARAGRAPH someTitle
thisIsContent
这个就不太行:
PARAGRAPH someTitle
thisIsContent
PARAGRAPH otherTitle
thisIsOtherContent
它在第一个内容后面期待PARAGRAPH
,但遇到了换行(记得setDefaultWhitespaceChars(' ')
)。我是否必须在paragraph
的末尾加上CR?
?有没有更好的方法来忽略这样的最后换行?
另外,我希望允许制表符和空格在文件中的任何地方出现而不造成干扰。唯一需要的行为是段落内容以TAB
开始,并且PARAGRAPH
要在行的开头。这也意味着在段落中和段落之间跳过空行(包含制表符和空格或什么都没有的行)。
因此,我添加了这一行:
tablefile.ignore(LineStart() + ZeroOrMore(White(' \t')) + LineEnd())
但是我刚才提到的每个需求似乎都与我将默认空白字符设置为' '
的需求相悖,让我陷入了死胡同。
实际上,这会导致一切都崩溃:
tablefile.ignore(CR)
tablefile.ignore(TAB)
将PARAGRAPH和TAB粘到行的开头
如果我希望\t
在文本中的任何地方都被忽略,但在行的开头不被忽略,我就必须把它们添加到默认的空白字符中。
因此,我找到了一种方法,可以禁止在行的开头出现任何空白字符。通过使用leaveWhitespace
方法,这个方法会保留它在匹配标记之前遇到的空白字符。因此,我可以将一些标记粘到行的开头。
ParserElement.setDefaultWhitespaceChars('\t ')
SOL = LineStart().suppress()
EOL = LineEnd().suppress()
title = Word()
content = Word()
PARAGRAPH = Keyword('PARAGRAPH').leaveWhitespace()
TAB = Literal('\t').leaveWhitespace()
paragraph = (SOL + PARAGRAPH + title + EOL
+ SOL + TAB + content + EOL)
通过这个解决方案,我解决了文本中任意位置的TAB问题。
分隔段落
经过一番思考,我找到了PaulMcGuire的解决方案(delimitedList
),但遇到了一些问题。
实际上,这里有两种不同的方式来声明两个段落之间的换行符。在我看来,它们应该是等价的,但实际上却不是?
崩溃测试(如果你运行它,别忘了把空格换成制表符):
PARAGRAPH titleone
content1
PARAGRAPH titletwo
content2
两个例子之间的共同部分:
ParserElement.setDefaultWhitespaceChars('\t ')
SOL = LineStart().suppress()
EOL = LineEnd().suppress()
title = Word()
content = Word()
PARAGRAPH = Keyword('PARAGRAPH').leaveWhitespace()
TAB = Literal('\t').leaveWhitespace()
第一个例子,工作正常:
paragraph = (SOL + PARAGRAPH + title + EOL
+ SOL + TAB + content + EOL)
tablefile = ZeroOrMore(paragraph)
第二个例子,不工作:
paragraph = (SOL + PARAGRAPH + title + EOL
+ SOL + TAB + content)
tablefile = delimitedList(paragraph, delim=EOL)
它们不应该是等价的吗?第二个抛出了异常:
Expected end of text (at char 66), (line:4, col:1)
这对我来说不是个大问题,因为我最终可以退回去在我语法的每个段落样式部分的末尾加上EOL。但我想强调这一点。
忽略包含空白的空行
我还有一个需求,就是忽略包含空白(' \t'
)的空行。
一个简单的语法可以是:
ParserElement.setDefaultWhitespaceChars(' \t')
SOL = LineStart().suppress()
EOL = LineEnd().suppress()
word = Word('a')
entry = SOL + word + EOL
grammar = ZeroOrMore(entry)
grammar.ignore(SOL + EOL)
最后,文件可以每行包含一个单词,任何地方都有空白,并且应该忽略空行。
幸运的是,它确实这样做了。但它不受默认空白声明的影响,包含空格或制表符的空行会导致解析器抛出解析异常。
这种行为绝对不是我预期的。这是指定的行为吗?在这个简单的尝试中是否有bug?
我在这个讨论中看到,PaulMcGuire并没有尝试忽略空行,而是试图将它们标记化,就像在makefile样式的语法解析器中一样(NL = LineEnd().suppress()
)。
makefile_parser = ZeroOrMore( symbol_assignment
| task_definition
| NL )
我现在唯一的解决方案是预处理文件,去掉空行中的空白,因为pyparsing会正确忽略没有空白的空行。
import os
preprocessed_file = os.tmpfile()
with open(filename, 'r') as file:
for line in file:
# Use rstrip to preserve heading TAB at start of a paragraph line
preprocessed_file.write(line.rstrip() + '\n')
preprocessed_file.seek(0)
grammar.parseFile(preprocessed_file, parseAll=True)
1 个回答
你的BNF(巴科斯-诺尔形式)只包含了CR(回车),但是你解析代码时却用LF(换行)来结束。这是你想要的效果吗?BNF支持 LF(Unix系统)、CR(Mac系统)和CRLF(Windows系统)的行结束符:
Rule_|_Def.__|_Meaning___
CR | %x0D | carriage return
LF | %x0A | linefeed
CRLF | CR LF | Internet standard newline