Использование 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, но это выдает мне ошибку, maximum recursion depth exceeded
.
РЕДАКТИРОВАТЬ 2009-11-15: Позже я понял, что pyparsing становится немного щедрым в отношении пробелов, и это приводит к некоторым ошибочным предположениям о том, что то, для чего, как я думал, я анализировал, было намного слабее.То есть мы не хотим видеть пробелов ни между одной из частей слова, escape-символом и символом EOL.
Я понял, что приведенной выше небольшой строки примера недостаточно в качестве тестового примера, поэтому я написал следующие модульные тесты.Код, который проходит эти тесты, должен соответствовать тому, что я интуитивно воспринимаю как слово с разделением на экранирование — и Только разделенное на побег слово.Они не будут соответствовать базовому слову, которое не является escape-split.Мы можем — и я считаю, должны — использовать для этого другую грамматическую конструкцию.Это позволяет поддерживать порядок, поскольку они разделены на две части.
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
Поковырявшись еще немного, я наткнулся на этот поток справки где был этот примечательный момент
Я часто вижу неэффективные грамматики, когда кто-то реализует грамматику pyparsing непосредственно из определения BNF.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))
Преимущество этого заключается в том, что между элементами нет пробелов (за исключением перевода строк после экранирующей обратной косой черты).
Другие советы
Вы довольно близки к своему коду.Любой из этих модов будет работать:
# '|' 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))
Как вы обнаружили в своем поиске в Google, переводы BNF->pyparsing должны выполняться с особым учетом использования функций pyparsing вместо недостатков BNF.На самом деле я был в процессе написания более длинного ответа, вдаваясь в подробности перевода BNF, но вы уже нашли этот материал (я полагаю, в вики).