Не удалось исправить ошибку pyparsing …

обзор

Итак, я в середине рефакторинга проекта, и я отделяю кучу кода разбора. Код, который я касаюсь, – это pyparsing.

Я очень плохо разбираюсь в пипарировании, даже потратив много времени на чтение официальной документации. У меня возникают проблемы, потому что (1) pyparsing принимает (намеренно) неортодоксальный подход к разбору, и (2) я работаю над кодом, который я не писал, с плохими комментариями и неэлементарным набором существующих грамматик.

(Я не могу связаться с оригинальным автором.)

Неудачное испытание

Я использую PyVows для проверки моего кода. Один из моих тестов следующий (я думаю, это ясно, даже если вы не знакомы с PyVows, дайте мне знать, если это не так):

def test_multiline_command_ends(self, topic): output = parsed_input('multiline command ends\n\n',topic) expect(output).to_equal( r'''['multiline', 'command ends', '\n', '\n'] - args: command ends - multiline_command: multiline - statement: ['multiline', 'command ends', '\n', '\n'] - args: command ends - multiline_command: multiline - terminator: ['\n', '\n'] - terminator: ['\n', '\n']''') 

Но когда я запускаю тест, я получаю следующее в терминале:

Неудачные результаты тестирования

 Expected topic("['multiline', 'command ends']\n- args: command ends\n- command: multiline\n- statement: ['multiline', 'command ends']\n - args: command ends\n - command: multiline") to equal "['multiline', 'command ends', '\\n', '\\n']\n- args: command ends\n- multiline_command: multiline\n- statement: ['multiline', 'command ends', '\\n', '\\n']\n - args: command ends\n - multiline_command: multiline\n - terminator: ['\\n', '\\n']\n- terminator: ['\\n', '\\n']" 

Заметка:

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

Ожидаемое поведение

Первая строка вывода должна соответствовать второй, но это не так. В частности, он не включает два символа новой строки в этом первом объекте списка.

Поэтому я получаю следующее:

 "['multiline', 'command ends']\n- args: command ends\n- command: multiline\n- statement: ['multiline', 'command ends']\n - args: command ends\n - command: multiline" 

Когда я получу это:

 "['multiline', 'command ends', '\\n', '\\n']\n- args: command ends\n- multiline_command: multiline\n- statement: ['multiline', 'command ends', '\\n', '\\n']\n - args: command ends\n - multiline_command: multiline\n - terminator: ['\\n', '\\n']\n- terminator: ['\\n', '\\n']" 

Ранее в коде есть также следующее утверждение:

 pyparsing.ParserElement.setDefaultWhitespaceChars(' \t') 

… Который, я думаю, должен помешать именно такой ошибке. Но я не уверен.

Даже если проблема не может быть идентифицирована с уверенностью, просто сужение там, где проблема, будет ОГРОМНОЙ помощью.

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


Edit: Итак, я должен опубликовать код парсера для этого, не так ли? (Спасибо за подсказку, @andrew cooke!)

Код Parser

Вот __init__ для моего объекта парсера.

Я знаю, что это кошмар. Вот почему я реорганизую проект. ☺

 def __init__(self, Cmd_object=None, *args, **kwargs): # @NOTE # This is one of the biggest pain points of the existing code. # To aid in readability, I CAPITALIZED all variables that are # not set on `self`. # # That means that CAPITALIZED variables aren't # used outside of this method. # # Doing this has allowed me to more easily read what # variables become a part of other variables during the # building-up of the various parsers. # # I realize the capitalized variables is unorthodox # and potentially anti-convention. But after reaching out # to the project's creator several times over roughly 5 # months, I'm still working on this project alone... # And without help, this is the only way I can move forward. # # I have a very poor understanding of the parser's # control flow when the user types a command and hits ENTER, # and until the author (or another pyparsing expert) # explains what's happening to me, I have to do silly # things like this. :-| # # Of course, if the impossible happens and this code # gets cleaned up, then the variables will be restored to # proper capitalization. # # —Zearin # http://github.com/zearin/ # 2012 Mar 26 if Cmd_object is not None: self.Cmd_object = Cmd_object else: raise Exception('Cmd_object be provided to Parser.__init__().') # @FIXME # Refactor methods into this class later preparse = self.Cmd_object.preparse postparse = self.Cmd_object.postparse self._allow_blank_lines = False self.abbrev = True # Recognize abbreviated commands self.case_insensitive = True # Commands recognized regardless of case # make sure your terminators are not in legal_chars! self.legal_chars = u'!#$%.:?@_' + PYP.alphanums + PYP.alphas8bit self.multiln_commands = [] if 'multiline_commands' not in kwargs else kwargs['multiln_commands'] self.no_special_parse = {'ed','edit','exit','set'} self.redirector = '>' # for sending output to file self.reserved_words = [] self.shortcuts = { '?' : 'help' , '!' : 'shell', '@' : 'load' , '@@': '_relative_load' } # self._init_grammars() # # def _init_grammars(self): # @FIXME # Add Docstring # ---------------------------- # Tell PYP how to parse # file input from '< filename' # ---------------------------- FILENAME = PYP.Word(self.legal_chars + '/\\') INPUT_MARK = PYP.Literal('<') INPUT_MARK.setParseAction(lambda x: '') INPUT_FROM = FILENAME('INPUT_FROM') INPUT_FROM.setParseAction( self.Cmd_object.replace_with_file_contents ) # ---------------------------- #OUTPUT_PARSER = (PYP.Literal('>>') | (PYP.WordStart() + '>') | PYP.Regex('[^=]>'))('output') OUTPUT_PARSER = (PYP.Literal( 2 * self.redirector) | \ (PYP.WordStart() + self.redirector) | \ PYP.Regex('[^=]' + self.redirector))('output') PIPE = PYP.Keyword('|', identChars='|') STRING_END = PYP.stringEnd ^ '\nEOF' TERMINATORS = [';'] TERMINATOR_PARSER = PYP.Or([ (hasattr(t, 'parseString') and t) or PYP.Literal(t) for t in TERMINATORS ])('terminator') self.comment_grammars = PYP.Or([ PYP.pythonStyleComment, PYP.cStyleComment ]) self.comment_grammars.ignore(PYP.quotedString) self.comment_grammars.setParseAction(lambda x: '') self.comment_grammars.addParseAction(lambda x: '') self.comment_in_progress = '/*' + PYP.SkipTo(PYP.stringEnd ^ '*/') # QuickRef: Pyparsing Operators # ---------------------------- # ~ creates NotAny using the expression after the operator # # + creates And using the expressions before and after the operator # # | creates MatchFirst (first left-to-right match) using the # expressions before and after the operator # # ^ creates Or (longest match) using the expressions before and # after the operator # # & creates Each using the expressions before and after the operator # # * creates And by multiplying the expression by the integer operand; # if expression is multiplied by a 2-tuple, creates an And of # (min,max) expressions (similar to "{min,max}" form in # regular expressions); if min is None, intepret as (0,max); # if max is None, interpret as expr*min + ZeroOrMore(expr) # # - like + but with no backup and retry of alternatives # # * repetition of expression # # == matching expression to string; returns True if the string # matches the given expression # # << inserts the expression following the operator as the body of the # Forward expression before the operator # ---------------------------- DO_NOT_PARSE = self.comment_grammars | \ self.comment_in_progress | \ PYP.quotedString # moved here from class-level variable self.URLRE = re.compile('(https?://[-\\w\\./]+)') self.keywords = self.reserved_words + [fname[3:] for fname in dir( self.Cmd_object ) if fname.startswith('do_')] # not to be confused with `multiln_parser` (below) self.multiln_command = PYP.Or([ PYP.Keyword(c, caseless=self.case_insensitive) for c in self.multiln_commands ])('multiline_command') ONELN_COMMAND = ( ~self.multiln_command + PYP.Word(self.legal_chars) )('command') #self.multiln_command.setDebug(True) # Configure according to `allow_blank_lines` setting if self._allow_blank_lines: self.blankln_termination_parser = PYP.NoMatch else: BLANKLN_TERMINATOR = (2 * PYP.lineEnd)('terminator') #BLANKLN_TERMINATOR('terminator') self.blankln_termination_parser = ( (self.multiln_command ^ ONELN_COMMAND) + PYP.SkipTo( BLANKLN_TERMINATOR, ignore=DO_NOT_PARSE ).setParseAction(lambda x: x[0].strip())('args') + BLANKLN_TERMINATOR )('statement') # CASE SENSITIVITY for # ONELN_COMMAND and self.multiln_command if self.case_insensitive: # Set parsers to account for case insensitivity (if appropriate) self.multiln_command.setParseAction(lambda x: x[0].lower()) ONELN_COMMAND.setParseAction(lambda x: x[0].lower()) self.save_parser = ( PYP.Optional(PYP.Word(PYP.nums)^'*')('idx') + PYP.Optional(PYP.Word(self.legal_chars + '/\\'))('fname') + PYP.stringEnd) AFTER_ELEMENTS = PYP.Optional(PIPE + PYP.SkipTo( OUTPUT_PARSER ^ STRING_END, ignore=DO_NOT_PARSE )('pipeTo') ) + \ PYP.Optional(OUTPUT_PARSER + PYP.SkipTo( STRING_END, ignore=DO_NOT_PARSE ).setParseAction(lambda x: x[0].strip())('outputTo') ) self.multiln_parser = (((self.multiln_command ^ ONELN_COMMAND) + PYP.SkipTo( TERMINATOR_PARSER, ignore=DO_NOT_PARSE ).setParseAction(lambda x: x[0].strip())('args') + TERMINATOR_PARSER)('statement') + PYP.SkipTo( OUTPUT_PARSER ^ PIPE ^ STRING_END, ignore=DO_NOT_PARSE ).setParseAction(lambda x: x[0].strip())('suffix') + AFTER_ELEMENTS ) #self.multiln_parser.setDebug(True) self.multiln_parser.ignore(self.comment_in_progress) self.singleln_parser = ( ( ONELN_COMMAND + PYP.SkipTo( TERMINATOR_PARSER ^ STRING_END ^ PIPE ^ OUTPUT_PARSER, ignore=DO_NOT_PARSE ).setParseAction(lambda x:x[0].strip())('args'))('statement') + PYP.Optional(TERMINATOR_PARSER) + AFTER_ELEMENTS) #self.multiln_parser = self.multiln_parser('multiln_parser') #self.singleln_parser = self.singleln_parser('singleln_parser') self.prefix_parser = PYP.Empty() self.parser = self.prefix_parser + (STRING_END | self.multiln_parser | self.singleln_parser | self.blankln_termination_parser | self.multiln_command + PYP.SkipTo( STRING_END, ignore=DO_NOT_PARSE) ) self.parser.ignore(self.comment_grammars) # a not-entirely-satisfactory way of distinguishing # '<' as in "import from" from # '<' as in "lesser than" self.input_parser = INPUT_MARK + \ PYP.Optional(INPUT_FROM) + \ PYP.Optional('>') + \ PYP.Optional(FILENAME) + \ (PYP.stringEnd | '|') self.input_parser.ignore(self.comment_in_progress) 

    2 Solutions collect form web for “Не удалось исправить ошибку pyparsing …”

    Я подозреваю, что проблема заключается в пропуске встроенного прокрутки pyparsing, который по умолчанию пропускает новые строки. Несмотря на то, что setDefaultWhitespaceChars используется для указания того, что новые строки значительны, этот параметр влияет только на все выражения, созданные после вызова setDefaultWhitespaceChars . Проблема заключается в том, что pyparsing пытается помочь, определив ряд удобных выражений при импорте, например empty для Empty() , lineEnd для LineEnd() и так далее. Но так как все они созданы во время импорта, они определяются с исходными символами пробела по умолчанию, которые включают '\n' .

    Возможно, я должен просто сделать это в setDefaultWhitespaceChars , но вы тоже можете очистить это для себя. Сразу после вызова setDefaultWhitespaceChars переопределите эти выражения на уровне модуля в pyparsing:

     PYP.ParserElement.setDefaultWhitespaceChars(' \t') # redefine module-level constants to use new default whitespace chars PYP.empty = PYP.Empty() PYP.lineEnd = PYP.LineEnd() PYP.stringEnd = PYP.StringEnd() 

    Я думаю, что это поможет восстановить значимость встроенных новых строк.

    Некоторые другие биты вашего кода парсера:

      self.blankln_termination_parser = PYP.NoMatch 

    должно быть

      self.blankln_termination_parser = PYP.NoMatch() 

    Возможно, ваш первоначальный автор чрезмерно агрессивен, используя '^' over '|'. Используйте только «^», если есть вероятность разбора одного выражения случайно, если вы действительно проанализировали более длинный, который следует позже в списке альтернатив. Например, в:

      self.save_parser = ( PYP.Optional(PYP.Word(PYP.nums)^'*')('idx') 

    Нет никакой возможной путаницы между Word числовых цифр или одиночным '*' . Or (или оператор '^' ) сообщает pyparsing, чтобы попытаться оценить все альтернативы, а затем выбрать самый длинный соответствующий – в случае равенства, выбрал самую левую альтернативу в списке. Если вы разбираете '*' , нет необходимости видеть, может ли это соответствовать длинному целому числу, или если вы разбираете целое число, не нужно видеть, может ли он также проходить как одиночный '*' . Поэтому измените это на:

      self.save_parser = ( PYP.Optional(PYP.Word(PYP.nums)|'*')('idx') 

    Использование действия синтаксического анализа для замены строки с помощью '' более просто записывается с использованием оболочки PYP.Suppress или, если хотите, вызывает expr.suppress() который возвращает Suppress(expr) . В сочетании с предпочтением '|' над '^', это:

      self.comment_grammars = PYP.Or([ PYP.pythonStyleComment, PYP.cStyleComment ]) self.comment_grammars.ignore(PYP.quotedString) self.comment_grammars.setParseAction(lambda x: '') 

    becomse:

      self.comment_grammars = (PYP.pythonStyleComment | PYP.cStyleComment ).ignore(PYP.quotedString).suppress() 

    Ключевые слова имеют встроенную логику, чтобы автоматически избегать двусмысленности, так что Or совершенно не нужна с ними:

      self.multiln_command = PYP.Or([ PYP.Keyword(c, caseless=self.case_insensitive) for c in self.multiln_commands ])('multiline_command') 

    должно быть:

      self.multiln_command = PYP.MatchFirst([ PYP.Keyword(c, caseless=self.case_insensitive) for c in self.multiln_commands ])('multiline_command') 

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

    Это все, что я могу сейчас увидеть. Надеюсь это поможет.

    Я починил это!

    Пираринг не был виноват!

    Я был. ☹

    Разделив код синтаксического анализа на другой объект, я создал проблему. Первоначально атрибут использовался для «обновления самого себя» на основе содержимого второго атрибута. Так как все это всегда содержалось в одном «классе богов», оно отлично работало.

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

    конкретика

    Атрибут multiln_command (не путать с multiln_commands -aargh, каким путающим именованием!) Является определением грамматики pyparsing. multiln_command должен обновить свою грамматику, если изменились команды multiln_commands .

    Хотя я знал, что эти два атрибута имеют похожие имена, но очень разные цели, сходство определенно затрудняло отслеживание проблемы. Я не переименовал multiln_command в multiln_grammar .

    Однако! ☺

    Я благодарен за замечательный ответ @Paul McGuire, и я надеюсь, что это спасет меня (и других) некоторое горе в будущем. Хотя я чувствую себя немного глупо, что я вызвал эту проблему (и неправильно разобрал ее как проблему пипарирования), я рад, что некоторые хорошие (в форме совета Павла) стали задавать этот вопрос.


    Счастливый парсинг, все. 🙂

    Python - лучший язык программирования в мире.