Pyparsing:空格有时重要,有时不重要

37 投票
1 回答
5031 浏览
提问于 2025-04-18 02:01

我想为一个包含多个部分的文件创建一种语法(就像下面的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())。

有没有适用于自定义BNF解析器的Python模块?

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 个回答

2

你的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

撰写回答