Cách thiết lập một ngữ pháp có thể xử lý sự mơ hồ


9

Tôi đang cố gắng tạo một ngữ pháp để phân tích một số công thức giống như Excel mà tôi đã nghĩ ra, trong đó một ký tự đặc biệt ở đầu chuỗi biểu thị một nguồn khác. Ví dụ,$ có thể biểu thị một chuỗi, vì vậy " $This is text" sẽ được coi là đầu vào chuỗi trong chương trình và &có thể biểu thị một hàm, do đó &foo()có thể được coi là một lệnh gọi đến hàm bên trong foo.

Vấn đề tôi gặp phải là làm thế nào để xây dựng ngữ pháp đúng cách. Ví dụ: Đây là phiên bản đơn giản hóa dưới dạng 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')

Như vậy, với ngữ pháp này, mọi thứ như: $This is a string, &foo(), &foo(#arg1), &foo($arg1,,#arg2)&foo(!w1,w2,w3,,!w4,w5,w6) tất cả đều được phân tích như mong đợi. Nhưng nếu tôi muốn thêm linh hoạt hơn cho simplethiết bị đầu cuối của mình , thì tôi cần bắt đầu loay hoay với SINGLESTRđịnh nghĩa mã thông báo không thuận tiện.

Tôi đã thử những gì

Phần mà tôi không thể vượt qua là nếu tôi muốn có một chuỗi bao gồm dấu ngoặc đơn (nghĩa đen func), thì tôi không thể xử lý chúng trong tình huống hiện tại của mình.

  • Nếu tôi thêm dấu ngoặc đơn vào SINGLESTR, thì tôi nhận đượcExpected STARTSYMBOL , bởi vì nó bị lẫn với funcđịnh nghĩa và nó nghĩ rằng một đối số hàm nên được thông qua, điều này có ý nghĩa.
  • Nếu tôi xác định lại ngữ pháp để chỉ dành riêng biểu tượng dấu và cho các hàm và thêm dấu ngoặc vào SINGLESTR, thì tôi có thể phân tích một chuỗi bằng dấu ngoặc đơn, nhưng mọi hàm tôi đang cố phân tích đều choExpected LPAR .

Ý định của tôi là bất cứ điều gì bắt đầu bằng một $sẽ được phân tích cú pháp như là mộtSINGLESTR mã thông báo mã thông báo và sau đó tôi có thể phân tích những thứ như thế &foo($first arg (has) parentheses,,$second arg).

Hiện tại, giải pháp của tôi là tôi đang sử dụng các từ 'thoát' như LEFTPAR và RIGHTPAR trong chuỗi của mình và tôi đã viết các hàm trợ giúp để thay đổi chúng thành dấu ngoặc đơn khi tôi xử lý cây. Vì vậy, $This is a LEFTPARtestRIGHTPARtạo ra cây chính xác và khi tôi xử lý nó, thì điều này sẽ được dịch sang This is a (test).

Để hình thành một câu hỏi chung: Tôi có thể định nghĩa ngữ pháp của mình theo cách mà một số ký tự đặc biệt đối với ngữ pháp được coi là ký tự bình thường trong một số tình huống và đặc biệt trong mọi trường hợp khác không?


CHỈNH SỬA 1

Dựa trên nhận xét từ jbndlrtôi đã sửa đổi ngữ pháp của mình để tạo các chế độ riêng dựa trên biểu tượng bắt đầu:

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

Điều này rơi (phần nào) trong trường hợp thử nghiệm thứ hai của tôi. Tôi có thể phân tích tất cả các simpleloại chuỗi (mã thông báo TEXT, MD hoặc DB có thể chứa dấu ngoặc đơn) và các hàm trống; ví dụ, &foo()hoặc &foo(&bar())phân tích chính xác. Khoảnh khắc tôi đặt một đối số trong một hàm (bất kể là loại nào), tôi nhận được một UnexpectedEOF Error: Expected ampersand, RPAR or ARGSEP. Như một bằng chứng về khái niệm, nếu tôi loại bỏ các dấu ngoặc đơn khỏi định nghĩa SINGLESTR trong ngữ pháp mới ở trên, thì mọi thứ sẽ hoạt động như bình thường, nhưng tôi lại quay lại hình vuông.


Bạn có các ký tự xác định những gì đến sau chúng (của bạn STARTSYMBOL) và bạn thêm dấu phân cách và dấu ngoặc đơn ở những nơi cần phải rõ ràng; Tôi không thấy bất kỳ sự mơ hồ nào ở đây. Bạn vẫn phải chia STARTSYMBOLdanh sách của mình thành các mục riêng lẻ để có thể phân biệt.
jbndlr

Tôi sẽ đăng một câu trả lời thực sự sớm, đã làm việc trên nó vài ngày nay.
iliar

Tôi đã cung cấp một câu trả lời. Mặc dù chỉ còn 2 giờ nữa cho đến khi tiền thưởng hết hạn, bạn vẫn có thể tự thưởng tiền thưởng trong thời gian ân hạn sau 24 giờ. Nếu câu trả lời của tôi không tốt, vui lòng cho tôi biết sớm và tôi sẽ sửa nó.
iliar

Câu trả lời:


3
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)")

Đầu ra:

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

Tôi hy vọng đó là những gì bạn đang tìm kiếm.

Những người đã được điên vài ngày. Tôi đã thử lark và thất bại. Tôi cũng đã thử persimoniouspyparsing . Tất cả các trình phân tích cú pháp khác nhau này đều có cùng một vấn đề với mã thông báo 'đối số' sử dụng dấu ngoặc đơn đúng là một phần của hàm, cuối cùng không thành công vì dấu ngoặc đơn của hàm không đóng.

Bí quyết là tìm ra cách bạn xác định dấu ngoặc đơn đúng "không đặc biệt". Xem biểu thức chính quy cho MIDTEXTRPARmã ở trên. Tôi đã định nghĩa nó là một dấu ngoặc đơn bên phải không được theo sau bởi phân tách đối số hoặc ở cuối chuỗi. Tôi đã làm điều đó bằng cách sử dụng phần mở rộng biểu thức chính quy (?!...)chỉ khớp với nếu nó không được theo sau ...nhưng không tiêu thụ ký tự. May mắn thay, nó thậm chí còn cho phép kết thúc chuỗi bên trong phần mở rộng biểu thức chính quy đặc biệt này.

BIÊN TẬP:

Phương thức được đề cập ở trên chỉ hoạt động nếu bạn không có đối số kết thúc bằng a), vì khi đó biểu thức chính quy MIDTEXTRPAR sẽ không bắt được điều đó) và sẽ nghĩ rằng đó là kết thúc của hàm mặc dù có nhiều đối số cần xử lý. Ngoài ra, có thể có sự mơ hồ như ... asdf) ,, ..., nó có thể là kết thúc của một khai báo hàm bên trong một đối số hoặc 'giống như văn bản') trong một đối số và khai báo hàm tiếp tục.

Vấn đề này có liên quan đến thực tế là những gì bạn mô tả trong câu hỏi của bạn không phải là ngữ pháp không ngữ cảnh ( https://en.wikipedia.org/wiki/Context-free_grammar ) mà các trình phân tích cú pháp như lark tồn tại. Thay vào đó là một ngữ pháp nhạy cảm theo ngữ cảnh ( https://en.wikipedia.org/wiki/Context-sensitive_grammar ).

Lý do khiến nó trở thành một ngữ pháp nhạy cảm theo ngữ cảnh là bởi vì bạn cần trình phân tích cú pháp để 'nhớ' rằng nó được lồng trong một hàm và có bao nhiêu cấp độ lồng nhau và có bộ nhớ này trong cú pháp của ngữ pháp theo một cách nào đó.

EDIT2:

Ngoài ra, hãy xem trình phân tích cú pháp sau đây nhạy cảm với ngữ cảnh và dường như để giải quyết vấn đề, nhưng có độ phức tạp theo thời gian theo số mũ của các hàm lồng nhau, vì nó cố gắng phân tích tất cả các rào cản chức năng có thể cho đến khi nó tìm thấy một hàm hoạt động. Tôi tin rằng nó phải có một sự phức tạp theo cấp số nhân vì nó không có ngữ cảnh.


_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())

1
Cảm ơn bạn, điều này hoạt động như dự định! Được thưởng tiền thưởng vì bạn không cần phải thoát khỏi dấu ngoặc đơn bằng mọi cách. Bạn đã đi thêm một dặm và nó cho thấy! Vẫn còn trường hợp cạnh của một đối số 'văn bản' kết thúc bằng dấu ngoặc đơn, nhưng tôi sẽ phải sống với đối số đó. Bạn cũng đã giải thích sự mơ hồ một cách rõ ràng và tôi sẽ chỉ cần kiểm tra thêm một chút nữa, nhưng tôi nghĩ với mục đích của mình, nó sẽ hoạt động rất tốt. Cảm ơn vì cũng đã cung cấp thêm thông tin về ngữ pháp nhạy cảm theo ngữ cảnh. Tôi rất trân trọng điều này!
Dima1982

@ Dima1982 Cảm ơn bạn rất nhiều!
iliar

@ Dima1982 Hãy xem bản chỉnh sửa, tôi đã tạo một trình phân tích cú pháp có thể giải quyết vấn đề của bạn với chi phí phức tạp theo thời gian theo cấp số nhân. Ngoài ra, tôi đã nghĩ về nó và nếu vấn đề của bạn có giá trị thực tế, thoát khỏi dấu ngoặc đơn có thể là giải pháp đơn giản nhất. Hoặc làm cho hàm ngoặc đơn một cái gì đó khác, chẳng hạn như phân định phần cuối của danh sách đối số hàm &chẳng hạn.
iliar

1

Vấn đề là các đối số của hàm được đặt trong ngoặc đơn trong đó một trong các đối số có thể chứa dấu ngoặc đơn.
Một trong những giải pháp khả thi là sử dụng backspace \ before (hoặc) khi nó là một phần của String

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

Giải pháp tương tự được sử dụng bởi C, để bao gồm dấu ngoặc kép (") như là một phần của hằng chuỗi trong đó hằng số chuỗi được đặt trong dấu ngoặc kép.

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

Đầu ra là

   start
     func
       f
       simple   !g\(

   start
     func
      f
      simple    #g

Tôi nghĩ rằng nó khá giống với giải pháp thay thế "(" và ")" của OP bằng LEFTPAR và RIGHTPAR.
iliar
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.