6

概要

だから、私はプロジェクトをリファクタリングしている最中で、解析コードの束を分離しています。私が関心を持っているコードは pyparsing です。

公式ドキュメントを読むのに多くの時間を費やした後でも、私は pyparse について非常によく理解していません。私が問題を抱えているのは、(1) pyparsing が (意図的に) 非正統的な構文解析アプローチを採用していること、および (2) 私が書いていないコードに取り組んでいて、コメントが貧弱で、既存の文法が初歩的でないためです。

(原作者とも連絡が取れません。)

失敗したテスト

PyVowsを使用してコードをテストしています。私のテストの1つは次のとおりです(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']"


ノート:

出力はターミナルに対するものであるため、予想される出力 (2 番目の出力) には余分なバックスラッシュが含まれています。これは正常です。このリファクタリングが始まる前に、テストは問題なく実行されました。

予想される行動

出力の 1 行目は 2 行目と一致するはずですが、一致しません。具体的には、最初のリスト オブジェクトに 2 つの改行文字が含まれていません。

だから私はこれを得ています:

"['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')

…まさにこの種のエラーを防ぐべきだと思います。確信はないけど。


問題が特定できなくても、どこに問題があるのか​​を絞り込むだけでも大きな助けになります。

これを修正するために 1、2 歩踏み出す方法を教えてください。


編集:だから、ええと、これのパーサーコードを投稿する必要がありますよね?(ヒントをありがとう、@andrew cook !)

パーサー コード

これ__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)
4

2 に答える 2

6

問題は、デフォルトで改行をスキップするpyparsingの組み込みの空白スキップであると思われます。改行が重要であることをpyparseに伝えるために使用されますが、この設定は への呼び出しsetDefaultWhitespaceCharsに作成されるすべての式にのみ影響します。問題は、pyparsing が、for 、forなどのように、インポート時に多数の便利な式を定義することによって支援しようとすることです。ただし、これらはすべてインポート時に作成されるため、元のデフォルトの空白文字 ( .setDefaultWhitespaceCharsemptyEmpty()lineEndLineEnd()'\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() 

元の作成者は、'|' よりも '^' を使用することに過度に積極的だった可能性があります。'^' を使用するのは、1 つの式を誤って解析してしまう可能性がある場合に限ってください。実際には、選択肢のリストの後半に続く長い式を解析したはずです。たとえば、次のようになります。

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

数字の Word と単独の Word を混同することはありません'*'Or(または'^'演算子) は、pyparsing にすべての選択肢を評価してから、最も長く一致するものを選択するように指示します。を解析する'*'場合、それがより長い整数にも一致するかどうかを確認する必要はありません。また、整数を解析する場合、それが lone としても渡される可能性があるかどうかを確認する必要もありません'*'。これを次のように変更します。

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

parse アクションを使用して文字列を '' に置き換える方法は、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: '') 

なる:

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

(次のリリースでは、[] が不要になるように、これらのイニシャライザを緩めてジェネレータ式を受け入れるようにします。)

今のところ見ることができるのはそれだけです。お役に立てれば。

于 2012-04-11T03:53:53.993 に答える
3

それを私が直した!

パイパーシングに問題はありませんでした。

私はそうだった。☹</p>

解析コードを別のオブジェクトに分離することで、問題を作成しました。元々、2番目の属性の内容に基づいて「それ自体を更新」するために使用される属性。これはすべて1つの「神のクラス」に含まれていたため、正常に機能しました。

コードを別のオブジェクトに分離するだけで、最初の属性はインスタンス化時に設定されましたが、依存していた2番目の属性が変更されても「それ自体が更新」されなくなりました。


詳細

属性multiln_command(—aarghと混同しないでmultiln_commandsください、なんと紛らわしい名前付けです!)は、文法の定義をpyparsingしました。属性が変更された場合は、multiln_commandその文法が更新されているはずmultiln_commandsです。

これらの2つの属性の名前は似ていますが、目的は大きく異なりますが、類似しているため、問題を突き止めるのは間違いなく困難でした。multiln_command名前をに変更していませんmultiln_grammar

でも!☺</h1>

@Paul McGuireの素晴らしい答えに感謝します。そして、それが私(および他の人)の将来の悲しみを救うことを願っています。私が問題を引き起こした(そしてそれを過誤の問題と誤診した)ことは少し愚かだと感じますが、この質問をすることで(ポールのアドバイスの形で)良いことができてうれしいです。


皆さん、ハッピー解析。:)

于 2012-04-11T18:39:50.617 に答える