Использование pyparsing для разбора слова escape-разбиения по нескольким строкам - PullRequest
5 голосов
/ 15 ноября 2009

Я пытаюсь разобрать слова, которые можно разбить на несколько строк с помощью комбинации обратной косой черты и новой строки ("\\n"), используя pyparsing . Вот что я сделал:

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)).

Я пытался посмотреть на этот код в помощнике по копированию , но он выдает ошибку, maximum recursion depth exceeded.

РЕДАКТИРОВАТЬ 2009-11-15: Позже я понял, что pyparsing становится немного щедрым в отношении пустого пространства, и это приводит к некоторым ошибочным предположениям о том, что то, что, как я думал, я анализировал, было намного более свободным , Иными словами, мы не хотим видеть пробелов между какими-либо частями слова, escape и символом EOL.

Я понял, что приведенной выше небольшой примерной строки недостаточно в качестве контрольного примера, поэтому я написал следующие модульные тесты. Код, который проходит эти тесты, должен соответствовать тому, что я интуитивно считаю словом «escape-split» - и « only escape-split-word». Они не будут соответствовать базовому слову, которое не является escape-разбиением. Мы можем & mdash; и я считаю, что & mdash; использовать для этого другую грамматическую конструкцию. Это поддерживает все в порядке, имея два отдельных.

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 голосов
/ 15 ноября 2009

Вы довольно близки с вашим кодом. Любой из этих модов будет работать:

# '|' 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))

Как вы обнаружили в вашем поисковом поиске, переводы BNF-> pyparsing должны выполняться с особым вниманием к использованию возможностей синтаксического анализа вместо недостатков BNF. На самом деле я находился в процессе написания более длинного ответа, углубляясь в вопросы перевода BNF, но вы уже нашли этот материал (я полагаю, в вики).

5 голосов
/ 15 ноября 2009

Пройдя немного побольше, я наткнулся на этот поток справки , где был этот заметный бит

Я часто вижу неэффективные грамматики, когда кто-то реализует грамматику 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))

Это дает преимущество, заключающееся в том, что между любыми элементами не должно быть пробела (за исключением перевода строки после экранирующей обратной косой черты).

...