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

6 投票
2 回答
2943 浏览
提问于 2025-04-15 16:01

我正在尝试用 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 对空格的处理有点宽松,这导致我对解析内容的理解有些偏差。也就是说,我们希望在单词的各个部分、转义符和行结束符之间没有空格。

我意识到上面的这个小示例字符串作为测试用例是不够的,所以我写了以下单元测试。通过这些测试的代码应该能够匹配我直观上认为的转义分割单词——而且仅仅是转义分割的单词。它们不会匹配那些没有转义分割的基本单词。我们可以——而且我认为应该——使用不同的语法结构来处理这些。这样把两者分开处理会让事情更清晰。

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

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转换的问题,但你已经找到了这些资料(我想是在维基上)。

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']

我学到的主要信息是,不能简单地走进摩尔多

开个玩笑。

真正想表达的是,不能简单地把BNF一对一地翻译成pyparsing。应该使用一些迭代类型的技巧。

编辑 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))

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

撰写回答