【问题标题】:Python Lex-Yacc (PLY) Error recovery at the end of inputPython Lex-Yacc (PLY) 输入结束时的错误恢复
【发布时间】:2014-10-13 10:41:03
【问题描述】:

问题

我正在尝试使用 Python Lex-Yacc (PLY) 实现容错解析器,但在输入字符串末尾使用错误恢复规则时遇到问题。

如何从输入的意外结束中恢复?

示例

这个示例语法产生A END A END A END A END...形式的字符串

Statement   : Expressions

Expressions : Expression Expressions
            | 

Expression  : A END

如果省略了 END 令牌,我想执行错误恢复,因此解析器将识别像 A A A ENDA A A 这样的字符串。

我的方法

我添加了一个错误恢复规则,它允许我接受像A A A END 这样的输入

Expression : A END
           | A error

这允许我接受以下输入: A A A END

但是如果省略最后一个 END 标记 (A A A),我仍然会遇到语法错误并且无法恢复。


示例层代码

from __future__ import print_function

# Tokens
tokens = ('A', 'END')

t_A   = r'A'
t_END = r'END'
t_ignore = " "

def t_error(t):
    print("Illegal character '%s'" % t.value[0])
    t.lexer.skip(1)

# Build the lexer
import ply.lex as lex
lex.lex()

# Rules
def p_statement_expr(p):
    '''statement : expressions'''
    print("parsed:", p[1])

def p_expressions(p):
    '''expressions : expression expressions'''
    p[0] = [p[1]] + p[2]

def p_expressions_empty(p):
    '''expressions : '''
    p[0] = list()

def p_expression_pharse(p):
    '''expression : A END
                  | A error'''
    p[0] = 'A'

def p_error(p):
    if p:
        print("Syntax error at '%s'" % p.value)
    else:
        print("Syntax error at EOI")

import ply.yacc as yacc
yacc.yacc()

while 1:
    try:
        s = raw_input('query > ')   # use input() on Python 3
    except EOFError:
        break
    yacc.parse(s)

【问题讨论】:

  • 你说你要宽容。但是对于您只想发出警告并认为所有 A 都是有效的,即使没有结束,只保留第一个 A 并忽略其他直到结束,只保留最后一个 A 并忽略以前的 A。最后还是一样,是拒绝终端A还是处理为有效?
  • 我想发出警告并接受所有As 为有效,即使没有END

标签: yacc lex parser-generator ply error-recovery


【解决方案1】:

我将它添加为一个新的答案(并且知道它为时已晚 :-( )因为它是一种非常不同的方法。如果我们使用 flex,它会容易得多,因为它有<<EOF>> 标记仅在文件末尾匹配的概念。考虑到这一点后,我意识到通过使用proxy 围绕词法分析器。由于__getattr__ 特殊方法,Python 允许轻松实现代理。

我只是添加

  • 将在文件末尾发送的新令牌 EOF
  • 一个围绕词法分析器的token 方法的代理,在文件末尾返回特殊的EOF 标记,然后是普通的None
  • 结束statement规则的eof令牌

并且仍然反转规则 expressions : expressions expression 而不是 expressions : expression expressions 以允许立即减少

代码变成:

from __future__ import print_function

# Tokens
tokens = ('A', 'END', 'EOF')

t_A   = r'A'
t_END = r'END'
t_ignore = " "

def t_error(t):
    print("Illegal character '%s'" % t.value[0])
    t.lexer.skip(1)

# Build the lexer
import ply.lex as lex

orig_lexer = lex.lex()

class ProxyLexer(object):
    def __init__(self, lexer, eoftoken):
        self.end = False
        self.lexer = lexer
        self.eof = eoftoken
    def token(self):
        tok = self.lexer.token()
        if tok is None:
            if self.end :
                self.end = False
            else:
                self.end = True
                tok = lex.LexToken()
                tok.type = self.eof
                tok.value = None
                tok.lexpos = self.lexer.lexpos
                tok.lineno = self.lexer.lineno
        # print ('custom', tok)
        return tok
    def __getattr__(self, name):
        return getattr(self.lexer, name)

lexer = ProxyLexer(orig_lexer, 'EOF')

# Rules
def p_statement_expr(p):
    '''statement : expressions EOF'''
    print("parsed:", p[1])

def p_expressions(p):
    '''expressions : expressions expression'''
    p[0] = p[1] + [p[2]]

def p_expressions_empty(p):
    '''expressions : '''
    p[0] = list()

def p_expression_pharse(p):
    '''expression : A END
                  | A error'''
    p[0] = 'A'

def p_error(p):
    if p:
        print("Syntax error at '%s'" % p.value)
    else:
        print("Syntax error at EOI")

import ply.yacc as yacc
parser = yacc.yacc()

while 1:
    try:
        s = raw_input('query > ')   # use input() on Python 3
    except EOFError:
        break
    parser.parse(s, lexer = lexer)

这样:

  • 原语法不变
  • 错误恢复方法仍然非常简单,并且不依赖于其余的语法
  • 它可以轻松扩展到复杂的解析器

【讨论】:

    【解决方案2】:

    当您想接受所有元素时,您可以显式声明 A 后面不跟 END 的规则,并使用 yacc 和 PLY 友好地处理模棱两可的规则这一事实。

    你可以简单地有一个正常的规则:

    Expression : A END
    

    低于将发出警告的较低优先级规则(稍后会出现)

    Expression : A
    

    这样,所有的 A 都将被接受,不会出现任何语法错误,并且对于任何没有后跟 END 的 A 都会发出警告,包括流程末尾的一个。为了更容易找到违规的A,我在警告中添加了符号在流程中的位置。

    编辑:

    修改脚本以正确处理其他语法错误(例如AENDENDAEND),并通过将expressions : expression expressions替换为expressions : expressions expression来立即减少expressions

    这是修改后的脚本(在 python 3.4 中测试,只需将 raw_input 替换为 input):

    from __future__ import print_function
    
    # Tokens
    tokens = ('A', 'END')
    
    t_A   = r'A'
    t_END = r'END'
    t_ignore = " "
    
    def t_error(t):
        print("Illegal character '%s'" % t.value[0])
        t.lexer.skip(1)
    
    # Build the lexer
    import ply.lex as lex
    lex.lex()
    
    # Rules
    def p_statement_expr(p):
        '''statement : expressions'''
        print("parsed:", p[1])
    
    def p_expressions(p):
        '''expressions : expressions expression'''
        p[0] = p[1] + [p[2]]
    
    def p_expressions_err(p):
        '''expressions : expressions error'''
        p[0] = p[1]
    
    def p_expressions_empty(p):
        '''expressions : '''
        p[0] = list()
    
    def p_expression_pharse(p):
        '''expression : A END'''
        p[0] = 'A'
    
    # add a separate rule BELOW previous one to display a warning
    def p_expression_pharse_warn(p):
        '''expression : A'''
        print("Warning at absolute position %d (line %d)" % (p.lexpos(1), p.lineno(1)))
        p[0] = 'A'
    
    def p_error(p):
        if p:
            print("Syntax error at '%s'" % p.value)
        else:
            print("Syntax error at EOI")
    
    
    import ply.yacc as yacc
    yacc.yacc()
    
    while 1:
        try:
            s = raw_input('query > ')   # use input() on Python 3
        except EOFError:
            break
        yacc.parse(s)
    

    编辑:以下是避免附加规则的错误尝试:它比上述版本更复杂且效率更低。请看下面我的结论

    根据评论编辑:

    我理解你的观点,你不想增加语法规则。除了最后一个令牌外,它可以是容错的。如果您的最后一个令牌有误,它将不会跟随任何内容,并且永远不会被规则expression : A error 捕获。

    但是这里有一个容错解析器,如果出现错误,它会保留除最后一个标记之外的所有内容:

    from __future__ import print_function
    
    # Tokens
    tokens = ('A', 'END')
    
    t_A   = r'A'
    t_END = r'END'
    t_ignore = " "
    
    def t_error(t):
        print("Illegal character '%s'" % t.value[0])
        t.lexer.skip(1)
    
    # Build the lexer
    import ply.lex as lex
    lex.lex()
    
    # Rules
    def p_statement_expr(p):
        '''statement : expressions'''
        # print("parsed:", p[1])
    
    def p_expressions(p):
        '''expressions : expressions expression'''
        p[0] = p[1] + [p[2]]
        result.append(p[2])
    
    def p_expressions_empty(p):
        '''expressions : '''
        p[0] = list()
    
    def p_expression_pharse(p):
        '''expression : A END
                      | A error'''
        p[0] = 'A'
    
    def p_error(p):
        if p:
            global lasterr
            print("Syntax error at '%s' (%d)" % (p.value, p.lexpos))
        else:
            print("Syntax error at EOI")
    
    import ply.yacc as yacc
    yacc.yacc()
    
    while 1:
        try:
            s = input('query > ')   # use input() on Python 3
        except EOFError:
            break
        result = []
        yacc.parse(s)
        print('Result', result)
    

    原则是通过expressions : expressions expression而不是expressions : expression expressions进行整理,并将所有内容保存在一个全局变量中。

    输入 A END A A END A A A END 会给出

    Result ['A', 'A', 'A', 'A', 'A', 'A']
    

    加上:A END A A END A A A END,它给出了

    Result ['A', 'A', 'A', 'A', 'A']
    

    (除最后一个之外的所有标记)

    使用真正的 flex - bison 解决方案,可以使用在输入末尾匹配的特殊 <<EOF>> 标记,在最后一个标记之后总是有另一个标记。不幸的是,它没有在 PLY 中实现,唯一真正的解决方案是引入一个单独接受 A 令牌的规则。对于真正的解析器,它还保证您实际上正在处理正确的令牌:我使用了

    def p_expression_pharse(p):
        '''expression : A END'''
        p[0] = 1 + p.lexpos(1)
    
    # add a separate rule BELOW previous one to display a warning
    def p_expression_pharse_warn(p):
        '''expression : A'''
        print("Warning at absolute position %d (line %d)" % (p.lexpos(1), p.lineno(1)))
        p[0] = -1 - p.lexpos(1)
    

    唯一标识结果字符串中的标记,我得到正确的位置。

    而且……错误处理很简单……

    讨论 TL/DR:

    我承认我错过了上次令牌错误恢复的要点。这是因为在我在实际用例中看到的所有解析器中,错误恢复包括拒绝语法不正确的部分(因此不能直接使用)并在下一个正确的 token 组上重新同步解析器。在我所看到的所有情况中,如果可以使用部分句子,它一定不是由错误恢复机制处理,而是由语法规则处理,其中很容易描述适当的动作。

    如果您只是想保留违规输入以供以后处理,我认为这不是取决于语法的操作问题,我会简单地记下违规令牌的位置,或者最多记下正确的位置 last分析标记(一个完整元素的结尾),第一个错误恢复标记的开始,并说中间的内容不正确。

    但这与这里的要求有很大不同......

    【讨论】:

    • 感谢您的回答,但它有点忽略了要点。我确实知道,我可以扩充我的语法以匹配这种特殊情况,但是随着语法变得越来越大,语法中将有太多不同的错误情况需要定义。
    • [...] But here is a fault tolerant parser that keeps everything except last token if case of error on that one 但现在它完全没有抓住重点,因为主要问题是输入结束时的错误恢复。
    • @Jen-Ya :如果您想接受 A 作为最后一个令牌,您必须单独使用 A 创建一个规则,这是我的第一个解决方案!事实上,在一个真正的解析器中,如果一个标记留在堆栈中,解决方案将是对最后一个标记使用追赶。但是对于这样一个简单的例子,它降级到我的第一个解决方案......
    • @Jen-Ya :我想到了我所做的,我真的认为正确的解决方案是第一个。唯一的问题是错误处理仍然是强制性的。我现在已经修好了。请查看我最近的编辑。
    【解决方案3】:

    这适用于我能想象到的所有示例

    from __future__ import print_function
    
    # Tokens
    tokens = ('A', 'END')
    
    t_A   = r'A'
    t_END = r'END'
    t_ignore = " "
    
    def t_error(t):
        print("Illegal character '%s'" % t.value[0])
        t.lexer.skip(1)
    
    # Build the lexer
    import ply.lex as lex
    lex.lex()
    
    # Rules
    def p_statement_expr(p):
        '''statement : expressions'''
        #
        print("parsed:", p[1])
    
    def p_expressions(p):
        '''expressions : expression expressions'''
        p[0] = p[1] + p[2]
    
    def p_expressions_empty(p):
        '''expressions : '''
        p[0] = list()
    
    def p_expression_pharse(p):
        '''expression : A END'''
        p[0] = ['A']
    
    def p_expression_error(p):
        '''expression : A error'''
        p[0] = ['A']
        if p[2] is not None:
            p[0] += p[2]
    
    def p_error(p):
        if p is None:
            print("Syntax error at EOI")
            e = yacc.YaccSymbol()
            e.type = 'error'
            e.value = None
            yacc.errok()
            return e
        elif p.type == 'error':
            yacc.errok()
            return
        elif hasattr(p, 'value'):
            print("Syntax error at '%s'" % p.value)
            e = yacc.YaccSymbol()
            e.type = 'error'
            e.value = p.value
            yacc.errok()
            return e
    
    
    
    
    import ply.yacc as yacc
    yacc.yacc()
    
    while 1:
        try:
            s = raw_input('query > ')   # use input() on Python 3
        except EOFError:
            break
        yacc.parse(s)
    

    【讨论】:

    • 谢谢,这是正确的方向。为什么无法在 EOI 上将令牌值设置为 error 而不是 END?我想从不同的可能错误中恢复,但不能确定在每种情况下都需要 END 令牌。
    • 返回错误会导致 p_expression_error(p) 执行。这将在输出中添加两个“A”。最好的办法是能够从 p_error 返回多个令牌。然后我们就可以返回 'error' 和 'A'(准确地说是 p.value)。
    • 我已经编辑了我的答案以包含您评论中的建议。希望这个更适合你。
    • 这里有什么遗漏吗?你能帮助我们回答你自己的问题吗?
    • @Jen-Ya 你试过我编辑的版本了吗?对我来说,它将 A A A 解析为 ['A', 'A', 'A'] 。我正在使用 ply 3.4 和 python 2.7.3。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2023-03-19
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多