如何逐行解析 python 代码直到表达式完成

How to parse python code line-by-line until an expression is complete

我有一个多行文本字符串,我想以逐行的方式解析 python 代码的一部分,这样我就有一个字符串列表,每个项目代表它自己python 声明。

不幸的是,我刚刚构建了整个文本的 AST,因为部分文本将不包含有效的 Python 语法。每个 python 语句也可以跨越多行。但是,对于每个有效语句,我确实知道它从哪一行开始。但是,我无法区分无效语法和前几行的延续。

例如我可能有这样的东西(我将添加一条注释,指出我知道哪些行开始有效语法,哪些行是无效语法或前一个语句的延续)

foo = bar()                 # valid-start 
this = (                    # valid-start
'perfectly valid syntax'    # unknown
)                           # unknown
44x=but-this-is-bad-syntax  # unknown

此处所需的输出是一个元组列表,第一项表示该语句是有效的 python 还是垃圾,第二项是与该语句对应的文本。

[
    ('PY', 'foo = bar()'),
    ('PY', """this = (                    
    'perfectly valid syntax'    
    )"""),                           
    ('JUNK', '44x=but-this-is-bad-syntax')
]

我考虑过的一个解决方案是检查括号是否平衡,但是当涉及字符串时这会变得很棘手(我还没有说服自己这在所有情况下都有效)。

foo = bar()                 # valid-start 
this = '''                  # valid-start
    this is still ()        # unknown
    perfectly valid )))     # unknown
'''                         # unknown
z = '''                     # unknown
  even though this is valid # unknown
  syntax, I don't want this # unknown
  line grouped'''           # unknown
44x=but-this-is-bad-syntax  # unknown
a = 1                       # valid-start

应该产生类似这样的输出:

[
    ('PY', 'foo = bar()'),
    ('PY', """this = '''                  
    this is still ()        
    perfectly valid )))     
    '''"""),                           
    ('JUNK', "z = '''"),
    ('JUNK', "even though this is valid"),
    ('JUNK', "syntax, I don't want this"),
    ('JUNK', "line grouped'''"),
    ('JUNK', "line grouped"),
    ('JUNK', "44x=but-this-is-bad-syntax"),
    ('PY', "a = 1"),
]

请注意,在最后一个示例中,以 z = ''' 开头的行被标记为未知。即使继续使用它仍然会产生有效语法,我想在它成为有效语法后停止解析以 this = ''' 开头的语句,(即 z = ''' 将不包括在内)

有没有人知道如何做到这一点?

一个 pyparsing 解决方案,在考虑字符串的同时简单地检查平衡括号是否足够?这个想法是我会定义一个接受平衡括号/方括号/大括号的语法,其中嵌套的主体可以是任何字符序列或字符串(可能包含括号,但这些不会被计入平衡)。然后,我将使用此语法解析行,直到重构的行与原始行完全相同。

有没有人发现以前的方法有问题/有没有人有更简单的方法来做到这一点,而不涉及对 pyparsing 的依赖?

编辑

根据@rici 的回答,我想出了一个可以接受行列表的函数,returns 如果这些行构成一个完整的语句则为 True,否则为 False。

import tokenize
from six.moves import cStringIO as StringIO

def is_complete_statement(lines):
    """
    Checks if the lines form a complete python statment.
    """
    try:
        stream = StringIO()
        stream.write('\n'.join(lines))
        stream.seek(0)
        for t in tokenize.generate_tokens(stream.readline):
            pass
    except tokenize.TokenError as ex:
        message = ex.args[0]
        if message.startswith('EOF in multi-line'):
            return False
        raise
    else:
        return True

标准 Python 库包含可标记和解析 Python 输入的模块。即使您的用例不适合内置 Python 解析器(模块 AST),您也可能会发现 tokenize 模块很有用。 (例如,它正确地标记了字符串文字。)

下面是 Python 2.7 中的一个简单演示:

$ cat tokenize.py
from sys import stdin
from tokenize import generate_tokens
from token import tok_name
for t in generate_tokens(stdin.readline):
     print (tok_name[t[0]], t[1])
$ python tokenize.py <<"EOF"
> foo = bar()
> this = '''
>    this is still ()
>    perfectly valid )))
> '''
> if not True:
>    print "false"
> 44x=this-is-bad-syntax but it can be tokenized
> a = 1
> EOF
('NAME', 'foo')
('OP', '=')
('NAME', 'bar')
('OP', '(')
('OP', ')')
('NEWLINE', '\n')
('NAME', 'this')
('OP', '=')
('STRING', "'''\n   this is still ()\n   perfectly valid )))\n'''")
('NEWLINE', '\n')
('NAME', 'if')
('NAME', 'not')
('NAME', 'True')
('OP', ':')
('NEWLINE', '\n')
('INDENT', '   ')
('NAME', 'print')
('STRING', '"false"')
('NEWLINE', '\n')
('DEDENT', '')
('NUMBER', '44')
('NAME', 'x')
('OP', '=')
('NAME', 'this')
('OP', '-')
('NAME', 'is')
('OP', '-')
('NAME', 'bad')
('OP', '-')
('NAME', 'syntax')
('NAME', 'but')
('NAME', 'it')
('NAME', 'can')
('NAME', 'be')
('NAME', 'tokenized')
('NEWLINE', '\n')
('NAME', 'a')
('OP', '=')
('NUMBER', '1')
('NEWLINE', '\n')
('ENDMARKER', '')