web-dev-qa-db-ja.com

あいまいさを処理できる文法をセットアップする方法

私が考案したいくつかのExcelのような数式を解析するための文法を作成しようとしています。ここで、文字列の先頭の特殊文字は別のソースを示しています。たとえば、_$_は文字列を表すことができるため、 "_$This is text_"はプログラムでは文字列入力として扱われ、_&_は関数を表すことができるため、&foo()内部関数fooの呼び出しとして扱うことができます。

私が直面している問題は、文法を適切に構築する方法です。たとえば、これはMWEとして簡略化されたバージョンです。

_grammar = r'''start: instruction

?instruction: simple
            | func

STARTSYMBOL: "!"|"#"|"$"|"&"|"~"
SINGLESTR: (LETTER+|DIGIT+|"_"|" ")*
simple: STARTSYMBOL [SINGLESTR] (WORDSEP SINGLESTR)*
ARGSEP: ",," // argument separator
WORDSEP: "," // Word separator
CONDSEP: ";;" // condition separator
STAR: "*"
func: STARTSYMBOL SINGLESTR "(" [simple|func] (ARGSEP simple|func)* ")"

%import common.LETTER
%import common.Word
%import common.DIGIT
%ignore ARGSEP
%ignore WORDSEP
'''
parser = lark.Lark(grammar, parser='earley')
_

したがって、この文法では、_$This is a string_、&foo()&foo(#arg1)&foo($arg1,,#arg2)および&foo(!w1,w2,w3,,!w4,w5,w6)はすべて次のように解析されます。期待された。しかし、simpleターミナルに柔軟性を追加したい場合は、SINGLESTRトークンの定義をいじる必要がありますが、これは便利ではありません。

私は何を試しましたか

私が通り越せないのは、括弧を含む文字列(funcのリテラル)が必要な場合、現在の状況ではそれらを処理できないことです。

  • かっこをSINGLESTRに追加すると、_Expected STARTSYMBOL_が得られます。これは、funcの定義と混同されており、関数の引数を渡す必要があると考えられているためです。
  • 関数のみのアンパサンド記号を予約するように文法を再定義し、SINGLESTRにかっこを追加すると、かっこを使用して文字列を解析できますが、解析しようとしているすべての関数は_Expected LPAR_を返します。

私の意図は、_$_で始まるものはすべてSINGLESTRトークンとして解析され、&foo($first arg (has) parentheses,,$second arg)のようなものを解析できるようにすることです。

私の解決策は、今のところ、文字列でLEFTPARやRIGHTPARなどの「エスケープ」ワードを使用し、ツリーを処理するときにそれらを括弧に変更するヘルパー関数を記述したことです。したがって、_$This is a LEFTPARtestRIGHTPAR_は正しいツリーを生成し、それを処理すると、これはThis is a (test)に変換されます。

一般的な質問を定式化するには:文法に特有の一部の文字が、状況によっては通常の文字として扱われ、その他の場合は特殊として扱われるように文法を定義できますか?


編集1

jbndlrのコメントに基づいて、開始記号に基づいて個別のモードを作成するように文法を修正しました。

_grammar = r'''start: instruction

?instruction: simple
            | func

SINGLESTR: (LETTER+|DIGIT+|"_"|" ") (LETTER+|DIGIT+|"_"|" "|"("|")")*
FUNCNAME: (LETTER+) (LETTER+|DIGIT+|"_")* // no parentheses allowed in the func name
DB: "!" SINGLESTR (WORDSEP SINGLESTR)*
TEXT: "$" SINGLESTR
MD: "#" SINGLESTR
simple: TEXT|DB|MD
ARGSEP: ",," // argument separator
WORDSEP: "," // Word separator
CONDSEP: ";;" // condition separator
STAR: "*"
func: "&" FUNCNAME "(" [simple|func] (ARGSEP simple|func)* ")"

%import common.LETTER
%import common.Word
%import common.DIGIT
%ignore ARGSEP
%ignore WORDSEP
'''
_

これは、(ある程度)2番目のテストケースに該当します。すべてのsimpleタイプの文字列(括弧を含めることができるTEXT、MD、またはDBトークン)と空の関数を解析できます。たとえば、&foo()または&foo(&bar())は正しく解析されます。関数内に引数を配置した瞬間(どのタイプであっても)、_UnexpectedEOF Error: Expected ampersand, RPAR or ARGSEP_を取得します。概念の証明として、上記の新しい文法のSINGLESTRの定義から括弧を削除すると、すべてが正常に機能しますが、正方形に戻ります。

9
Dima1982
import lark
grammar = r'''start: instruction

?instruction: simple
            | func

MIDTEXTRPAR: /\)+(?!(\)|,,|$))/
SINGLESTR: (LETTER+|DIGIT+|"_"|" ") (LETTER+|DIGIT+|"_"|" "|"("|MIDTEXTRPAR)*
FUNCNAME: (LETTER+) (LETTER+|DIGIT+|"_")* // no parentheses allowed in the func name
DB: "!" SINGLESTR (WORDSEP SINGLESTR)*
TEXT: "$" SINGLESTR
MD: "#" SINGLESTR
simple: TEXT|DB|MD
ARGSEP: ",," // argument separator
WORDSEP: "," // Word separator
CONDSEP: ";;" // condition separator
STAR: "*"
func: "&" FUNCNAME "(" [simple|func] (ARGSEP simple|func)* ")"

%import common.LETTER
%import common.Word
%import common.DIGIT
%ignore ARGSEP
%ignore WORDSEP
'''

parser = lark.Lark(grammar, parser='earley')
parser.parse("&foo($first arg (has) parentheses,,$second arg)")

出力:

Tree(start, [Tree(func, [Token(FUNCNAME, 'foo'), Tree(simple, [Token(TEXT, '$first arg (has) parentheses')]), Token(ARGSEP, ',,'), Tree(simple, [Token(TEXT, '$second arg')])])])

それがあなたが探していたものであることを願っています。

それらは狂った数日でした。ひばりを試しましたが失敗しました。 persimoniouspyparsingも試しました。これらの異なるパーサーはすべて、関数の一部である右括弧を消費する「引数」トークンに同じ問題があり、関数の括弧が閉じられていなかったために最終的に失敗しました。

トリックは、「特別ではない」右括弧をどのように定義するかを理解することでした。上記のコードのMIDTEXTRPARの正規表現を参照してください。私はそれを引数の分離や文字列の終わりが続かない右括弧として定義しました。正規表現拡張(?!...)を使用してこれを行いました。これは、後ろに...が付いていない場合にのみ一致しますが、文字を消費しません。幸いにも、この特別な正規表現拡張内で文字列の終わりを照合することもできます。

編集:

上記のメソッドは、)で終わる引数がない場合にのみ機能します。これは、MIDTEXTRPAR正規表現がそれをキャッチできず、処理する引数がさらにある場合でも、これが関数の終わりであると考えるためです。また、... asdf), ...などのあいまいさが存在する可能性があります。これは、引数内の関数宣言の終わり、または引数内の「テキストのような」)であり、関数宣言が続きます。

この問題は、質問であなたが説明しているものが文脈自由文法( https://en.wikipedia.org/wiki/Context-free_grammar )ではなく、ひばりが存在します。代わりに、状況依存の文法です( https://en.wikipedia.org/wiki/Context-sensitive_grammar )。

コンテキスト依存の文法である理由は、関数内にネストされていること、およびネストのレベルがいくつあるかをパーサーが「覚えて」、文法の構文内でこのメモリを何らかの方法で使用できるようにする必要があるためです。

EDIT2:

また、状況依存であり、問​​題を解決しているように見えますが、機能するバリアが見つかるまですべての可能な関数バリアを解析しようとするため、ネストされた関数の数が指数関数的に複雑になります。それは文脈自由ではないので、それは指数関数的複雑さを持っている必要があると思います。


_funcPrefix = '&'
_debug = False

class ParseException(Exception):
    pass

def GetRecursive(c):
    if isinstance(c,ParserBase):
        return c.GetRecursive()
    else:
        return c

class ParserBase:
    def __str__(self):
        return type(self).__name__ + ": [" + ','.join(str(x) for x in self.contents) +"]"
    def GetRecursive(self):
        return (type(self).__name__,[GetRecursive(c) for c in self.contents])

class Simple(ParserBase):
    def __init__(self,s):
        self.contents = [s]

class MD(Simple):
    pass

class DB(ParserBase):
    def __init__(self,s):
        self.contents = s.split(',')

class Func(ParserBase):
    def __init__(self,s):
        if s[-1] != ')':
            raise ParseException("Can't find right parenthesis: '%s'" % s)
        lparInd = s.find('(')
        if lparInd < 0:
            raise ParseException("Can't find left parenthesis: '%s'" % s)
        self.contents = [s[:lparInd]]
        argsStr = s[(lparInd+1):-1]
        args = list(argsStr.split(',,'))
        i = 0
        while i<len(args):
            a = args[i]
            if a[0] != _funcPrefix:
                self.contents.append(Parse(a))
                i += 1
            else:
                j = i+1
                while j<=len(args):
                    nestedFunc = ',,'.join(args[i:j])
                    if _debug:
                        print(nestedFunc)
                    try:
                        self.contents.append(Parse(nestedFunc))
                        break
                    except ParseException as PE:
                        if _debug:
                            print(PE)
                        j += 1
                if j>len(args):
                    raise ParseException("Can't parse nested function: '%s'" % (',,'.join(args[i:])))
                i = j

def Parse(arg):
    if arg[0] not in _starterSymbols:
        raise ParseException("Bad prefix: " + arg[0])
    return _starterSymbols[arg[0]](arg[1:])

_starterSymbols = {_funcPrefix:Func,'$':Simple,'!':DB,'#':MD}

P = Parse("&foo($first arg (has)) parentheses,,&f($asdf,,&nested2($23423))),,&second(!arg,wer))")
print(P)

import pprint
pprint.pprint(P.GetRecursive())
2
iliar

問題は、関数の引数が括弧で囲まれていて、引数の1つに括弧が含まれている可能性があることです。
考えられる解決策の1つは、文字列の一部である場合、前に(または)バックスペースを使用することです。

  SINGLESTR: (LETTER+|DIGIT+|"_"|" ") (LETTER+|DIGIT+|"_"|" "|"\("|"\)")*

文字列定数が二重引用符で囲まれている文字列定数の一部として二重引用符( ")を含めるためにCで使用される同様のソリューション。

  example_string1='&f(!g\()'
  example_string2='&f(#g)'
  print(parser.parse(example_string1).pretty())
  print(parser.parse(example_string2).pretty())

出力は

   start
     func
       f
       simple   !g\(

   start
     func
      f
      simple    #g
1