使用pyparsing解析跨多行的单词转义分隔符

6

我正在尝试使用pyparsing解析可以通过反斜杠-换行符组合("\\n")跨多行的单词。这是我的做法:

from pyparsing import *

continued_ending = Literal('\\') + lineEnd
word = Word(alphas)
split_word = word + Suppress(continued_ending)
multi_line_word = Forward()
multi_line_word << (word | (split_word + multi_line_word))

print multi_line_word.parseString(
'''super\\
cali\\
fragi\\
listic''')

我得到的输出是['super'],而期望的输出是['super','cali','fragi','listic']。更好的方法是将它们全部作为一个单词连接起来(我认为可以使用multi_line_word.parseAction(lambda t: ''.join(t))实现)。
我尝试在pyparsing helper中查看此代码,但出现错误maximum recursion depth exceeded编辑2009-11-15:后来我意识到pyparsing对于空格有些慷慨,这导致了一些错误的假设,我认为我解析的内容更加宽松。也就是说,我们希望在单词的任何部分、转义字符和EOL字符之间都没有空格。
我意识到上面的小例子字符串不足以作为测试用例,因此我编写了以下单元测试。通过这些测试的代码应该能够匹配我在直觉上认为的转义分割词——而且 只有 转义分割词。它们不会匹配不是转义分割的基本单词。我们可以——而且我相信应该——为此使用不同的语法结构。这样将两者分开可以使它们更整洁。
import unittest
import pyparsing

# Assumes you named your module 'multiline.py'
import multiline

class MultiLineTests(unittest.TestCase):

    def test_continued_ending(self):

        case = '\\\n'
        expected = ['\\', '\n']
        result = multiline.continued_ending.parseString(case).asList()
        self.assertEqual(result, expected)


    def test_continued_ending_space_between_parse_error(self):

        case = '\\ \n'
        self.assertRaises(
            pyparsing.ParseException,
            multiline.continued_ending.parseString,
            case
        )


    def test_split_word(self):

        cases = ('shiny\\', 'shiny\\\n', ' shiny\\')
        expected = ['shiny']
        for case in cases:
            result = multiline.split_word.parseString(case).asList()
            self.assertEqual(result, expected)


    def test_split_word_no_escape_parse_error(self):

        case = 'shiny'
        self.assertRaises(
            pyparsing.ParseException,
            multiline.split_word.parseString,
            case
        )


    def test_split_word_space_parse_error(self):

        cases = ('shiny \\', 'shiny\r\\', 'shiny\t\\', 'shiny\\ ')
        for case in cases:
            self.assertRaises(
                pyparsing.ParseException,
                multiline.split_word.parseString,
                case
            )


    def test_multi_line_word(self):

        cases = (
                'shiny\\',
                'shi\\\nny',
                'sh\\\ni\\\nny\\\n',
                ' shi\\\nny\\',
                'shi\\\nny '
                'shi\\\nny captain'
        )
        expected = ['shiny']
        for case in cases:
            result = multiline.multi_line_word.parseString(case).asList()
            self.assertEqual(result, expected)


    def test_multi_line_word_spaces_parse_error(self):

        cases = (
                'shi \\\nny',
                'shi\\ \nny',
                'sh\\\n iny',
                'shi\\\n\tny',
        )
        for case in cases:
            self.assertRaises(
                pyparsing.ParseException,
                multiline.multi_line_word.parseString,
                case
            )


if __name__ == '__main__':
    unittest.main()
2个回答

6

在进一步查看后,我找到了这个帮助主题,其中有一个值得注意的部分

当有人直接从BNF定义实现pyparsing语法时,我经常看到低效的语法。BNF没有“一个或多个”、“零或多个”或“可选”的概念...

有了这个想法,我改变了这两行内容。

multi_line_word = Forward()
multi_line_word << (word | (split_word + multi_line_word))

multi_line_word = ZeroOrMore(split_word) + word

这样做可以输出我想要的内容:['super', 'cali', fragi', 'listic']
接下来,我添加了一个解析操作,将这些令牌连接在一起:
multi_line_word.setParseAction(lambda t: ''.join(t))

这将产生最终输出['supercalifragilistic']
我得出的结论是,人们不能简单地走进摩多
开个玩笑。
重点是,不能仅仅使用pyparsing实现BNF的一对一翻译,需要使用迭代类型的一些技巧。 编辑2009-11-25:为了补偿更为严格的测试用例,我将代码修改为以下内容:
no_space = NotAny(White(' \t\r'))
# make sure that the EOL immediately follows the escape backslash
continued_ending = Literal('\\') + no_space + lineEnd
word = Word(alphas)
# make sure that the escape backslash immediately follows the word
split_word = word + NotAny(White()) + Suppress(continued_ending)
multi_line_word = OneOrMore(split_word + NotAny(White())) + Optional(word)
multi_line_word.setParseAction(lambda t: ''.join(t))

这样做的好处是确保元素之间没有空格(除了转义反斜杠后的换行符)。

1
使用 Combine 也强制不插入空格。 - PaulMcG
有趣。我尝试了以下代码: multi_line_word = Combine(Combine(OneOrMore(split_word)) + Optional(word)) 但是在处理 'sh\\\n iny' 这种情况时,它没有引发异常,而是返回了 ['sh']。我有什么遗漏吗? - gotgenes
好的,你的单词不仅是跨越“\”换行符的字母,而且在字母“i”之前有一个空格,这算作一个单词分隔符,因此Combine在“sh”后停止。你可以使用adjacent=False构造函数参数修改Combine,但要注意 - 你可能会将整个文件吸入一个单词!或者,如果你想要折叠任何前导空格,你可以重新定义你的continued_ending定义为线结束后的任何空格。 - PaulMcG
我更喜欢multi_line_word.parseString('sh\\\n iny')抛出ParseException,而不是将'sh'识别为其标记。在这种情况下,'sh''iny'是两个单词,而不是一个破碎单词的一部分,因为'iny'部分与EOL不连续。因此,multi_line_word不应该识别它。它应该举起双手说,“这不是一个有效的破碎单词!” - gotgenes

5

你的代码已经很接近了,以下这些修改都可以实现:

# '|' means MatchFirst, so you had a left-recursive expression
# reversing the order of the alternatives makes this work
multi_line_word << ((split_word + multi_line_word) | word)

# '^' means Or/MatchLongest, but beware using this inside a Forward
multi_line_word << (word ^ (split_word + multi_line_word))

# an unusual use of delimitedList, but it works
multi_line_word = delimitedList(word, continued_ending)

# in place of your parse action, you can wrap in a Combine
multi_line_word = Combine(delimitedList(word, continued_ending))

正如您在搜索pyparsing时发现的那样,BNF-> pyparsing翻译应该特别注意使用pyparsing功能来代替BNF的缺点。我本来要写一篇更长的答案,讨论更多的BNF翻译问题,但是您已经找到了这些材料(我想是在维基上)。


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