Pyparsing:空格有时很重要......有时不重要

37

我想为一个包含多个部分(如下面的“段落”)的文件创建语法。

每个部分以其关键字(例如PARAGRAPH)开头,后跟一个标题(此处为title),并在以下行中包含其内容,其中一行内容是该部分的一行。 目前,它就像是一个带有标题、列和行的表格。

在下面的示例(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)

将段落和制表符粘贴到行首

如果我希望在文本中忽略\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)

它们不应该是等价的吗?第二个引发异常:

在字符66处预期文本结束(行:4,列: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
一旦您将默认的空格字符定义为“ ”,那么您必须声明解析器将遇到的每个CR。您可以将其放在“段落”的末尾,或将tablefile定义为“tablefile = delimitedList(paragraph,delim = CR)”。 - PaulMcG
1
你不直接将内容放入XML文件中,然后使用XSLT/CSS输出和格式化以适应不同的媒介,有特定的原因吗?毕竟,这就是XML的用途:数据存储和/或交换。 - Agi Hammerthief
我正在处理其他人编写的文件。每个文件都包含数千行代码。但感谢您的建议。我的解决方法已经很好地解决了问题。 - carrieje
10
一条风格注意事项:绝不要使用\进行代码行换行,因为它会非常容易地导致错误。只需将表达式括在括号中,就不必手动添加换行符(我的意思是,在()内部的所有空格都不被视为字符(包括换行符))。此外,从排版上讲,应该用二元运算符开始新行,而不是操作数(Knuth也这么说,因此不遵守是违反计算机科学法律的罪行),所以应该写成something <newline> + otherstuff,而不是something + <newline> otherstuff - Bakuriu
1
谢谢你分享你的经验!我希望能看到更多像这样的评论 :-) 我已经相应地编辑了问题,现在我的日常编码风格会更加一致。再次感谢。另外,我希望有一天@PaulMcGuire能再次遇到这个问题,并告诉我这是pyparsing的一个bug还是正常行为。 - carrieje
显示剩余2条评论
1个回答

2
你的BNF只包含CR,但你解析代码时使用LF终止。这是有意为之吗?BNF支持 LF(Unix),CR(Mac)和CRLF(Win)行尾符号。
Rule_|_Def.__|_Meaning___
CR   | %x0D  | carriage return
LF   | %x0A  | linefeed
CRLF | CR LF | Internet standard newline

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接