如何在表达式完成之前逐行解析python代码

时间:2017-09-05 19:20:06

标签: python parsing abstract-syntax-tree pyparsing

我有一个多行文本字符串,我想逐行解析一部分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解决方案,只需检查平衡括号,同时考虑到字符串就足够了吗?我的想法是我会定义一个接受平衡括号/方括号/花括号的语法,其中嵌套的主体可以是任何字符序列或字符串(可能包含parens,但那些不会计入平衡)。然后,我将使用此语法解析行,直到重构的行与原始行完全相等。

有没有人看到上一种方法存在问题/是否有人有更简单的方法来执行此操作而不涉及对pyparsing的依赖?

修改

根据@rici的回答,我想出了一个可以获取行列表的函数,如果行形成一个完整的语句则返回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

1 个答案:

答案 0 :(得分:2)

标准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', '')