数值错误:在使用ast.literal_eval时出现格式错误的字符串。

24

众所周知,使用eval()存在潜在的安全风险,因此推荐使用ast.literal_eval(node_or_string)

然而,在Python 2.7中,当运行此示例时,它会返回ValueError: malformed string

>>> ast.literal_eval("4 + 9")

然而在 Python 3.3 中,这个例子的表现如同预期:

>>> ast.literal_eval('4+9')
13

为什么它运行在Python 3上而不是Python 2?我如何在Python 2.7中修复它而不使用风险极大的eval()函数?

=>

为什么它只支持Python 3而不是Python 2?如何在Python 2.7中修复,而无需使用风险高的eval()函数?


4
这绝对不是字面值,所以我会说Python 3.3在这里错了。也许这是Peephole优化的意外后果?编辑:不是的,它在“literal_eval”中被特殊处理和常量折叠。但仅适用于加法和减法,没有乘法、除法或其他运算符。 - user395760
请参见https://dev59.com/2GUq5IYBdhLWcg3wTuqi。 - Antti Haapala -- Слава Україні
5个回答

37

在Python 2中,这不起作用的原因在于其实现了literal_eval。原始实现仅在右侧操作数为复数时对加法和减法进行数字计算。这在语法上是表示复数的必要条件。

在Python 3中进行了更改,以支持任何有效的数字表达式出现在加法和减法两侧。然而,对literal_eval的使用仍然限制在加法和减法上。

这主要是因为literal_eval被认为是一个函数,可以将单个常量字面值(表示为字符串)转换为Python对象。有点像简单内置类型的反向repr。实际表达式评估未包括在内,而它在Python 3中的工作只是其实现带来的好处之一。

为了评估实际表达式,而不必使用eval(我们不想使用),我们可以编写自己的表达式评估算法,该算法运行在AST上。这很简单,尤其是对于数字的简单算术运算(例如构建自己的计算器等)。我们只需将字符串解析为AST,然后通过查看不同的节点类型并应用正确的操作来评估生成的树。

像这样:

import ast, operator

binOps = {
    ast.Add: operator.add,
    ast.Sub: operator.sub,
    ast.Mult: operator.mul,
    ast.Div: operator.div,
    ast.Mod: operator.mod
}

def arithmeticEval (s):
    node = ast.parse(s, mode='eval')

    def _eval(node):
        if isinstance(node, ast.Expression):
            return _eval(node.body)
        elif isinstance(node, ast.Str):
            return node.s
        elif isinstance(node, ast.Num):
            return node.n
        elif isinstance(node, ast.BinOp):
            return binOps[type(node.op)](_eval(node.left), _eval(node.right))
        else:
            raise Exception('Unsupported type {}'.format(node))

    return _eval(node.body)

正如你所看到的,这个实现非常简单。当然,它还不支持幂运算和某些一元节点之类的更复杂的内容,但添加它们并不太困难。而且它工作得很好:

>>> arithmeticEval('4+2')
6
>>> arithmeticEval('4*1+2*6/3')
8

您甚至可以在稍后引入更复杂的内容(例如用于诸如sin()之类的函数调用)。


我认为在Python3中,operator.div已被删除。您可以使用operator.truediv代替。 - Keith

6
为了支持复数(自问题4907以来),才进行了这项工作。例如,解析器将1 + 2j解析为一个整数字面值、一个加法运算和一个虚数字面值的表达式;但由于复数是一种内置类型,因此希望ast.literal_eval支持复数语法。2.x和3.x之间的行为变化是为了支持反向编写复数,例如1j + 2;它允许任意的加法或减法表达式是一个(大多数是无意的)副作用。
如果您想解析任意算术表达式,应该解析成语法树(使用ast.parse),使用白名单进行验证,然后进行评估。

1
+1,感谢关于复数的信息!我没有想到加法是将它们作为文字必要语法的一部分。谢谢。 - poke

3

使用源代码,卢克!

http://hg.python.org/cpython/file/2.7/Lib/ast.py#l40 http://hg.python.org/cpython/file/3.2/Lib/ast.py#l39

你可以在这里找到答案。具体来说,2.7版本对于第70行的BinOp右节点有奇怪的限制,必须是复杂的。

>>> sys.version
'2.7.3 (default, Sep 26 2013, 20:03:06) \n[GCC 4.6.3]'
>>> ast.literal_eval('9 + 0j')
(9 + 0j)
>>> ast.literal_eval('0j + 9')
ValueError: malformed string

我猜测2.7的意图是允许literal_eval处理复杂的文字,例如像9 + 0j这样的数字,而不是简单的整数加法。然后在Python 3中,literal_eval被加强以处理这些情况。

2
2.7版本中的奇怪行为是在此修订版中添加的,似乎旨在解决问题4907 - Gareth Rees
1
最让我困惑的是,为什么开发人员没有将Python 3中的ast.literal_eval实现到Python 2中呢? - K DawG
1
因为2 + 3不是一个字面量。如果我猴子补丁+做一些不同的事情,那该怎么办呢?我同意这里使用2.7的行为。在我的看来,+/-的工作只是处理复数的一个丑陋的副作用。如果支持加法,那么为什么不支持除法或乘法等操作呢? - wim
事实上,这在3.6版本中最终被认为是一个bug,并且在3.7版本中已经修复。[issue31778] (https://bugs.python.org/issue31778) - wim

3

使用pyparsing拼凑一个简单的表达式求值器并不太难。

假设您想要计算以下类型表达式,包括括号:

2+3
4.0^2+5*(2+3+4)
1.23+4.56-7.890
(1+2+3+4)/5
1e6^2/1e7

这是对SimpleCalc示例的简化:
import pyparsing as pp
import re

ex='''\
2+3
4.0^2+5*(2+3+4)
1.23+4.56-7.890
(1+2+3+4)/5
1e6^2/1e7'''

e = pp.CaselessLiteral('E')
dec, plus, minus, mult, div, expop=map(pp.Literal,'.+-*/^')
addop  = plus | minus
multop = mult | div
lpar, rpar=map(pp.Suppress,'()')
p_m = plus | minus

num = pp.Word(pp.nums) 
integer = pp.Combine( pp.Optional(p_m) + num )
floatnumber = pp.Combine( integer +
                       pp.Optional( dec + pp.Optional(num) ) +
                       pp.Optional( e + integer ) )

stack=[]
def pushFirst(s, l, t):
    stack.append( t[0] )

expr=pp.Forward()
atom = ((floatnumber | integer).setParseAction(pushFirst) | 
         ( lpar + expr.suppress() + rpar )
       )

factor = pp.Forward()
factor << atom + pp.ZeroOrMore( ( expop + factor ).setParseAction( pushFirst ) )

term = factor + pp.ZeroOrMore( ( multop + factor ).setParseAction( pushFirst ) )
expr << term + pp.ZeroOrMore( ( addop + term ).setParseAction( pushFirst ) )    

pattern=expr+pp.StringEnd()

opn = { "+" : ( lambda a,b: a + b ),
        "-" : ( lambda a,b: a - b ),
        "*" : ( lambda a,b: a * b ),
        "/" : ( lambda a,b: a / b ),
        "^" : ( lambda a,b: a ** b ) }

def evaluateStack(stk):
    op = stk.pop()
    if op in "+-*/^":
        op2 = evaluateStack(stk)
        op1 = evaluateStack(stk)
        return opn[op](op1, op2)
    elif re.search('^[-+]?[0-9]+$',op):
        return int(op)
    else:
        return float(op)     

for line in ex.splitlines():
    parse=pattern.parseString(line)   
    s=stack[:]
    print('"{}"->{} = {}'.format(line,s,evaluateStack(stack)))   

输出:

"2+3"->['2', '3', '+'] = 5
"4.0^2+5*(2+3+4)"->['4.0', '2', '^', '5', '2', '3', '+', '4', '+', '*', '+'] = 61.0
"1.23+4.56-7.890"->['1.23', '4.56', '+', '7.890', '-'] = -2.1000000000000005
"(1+2+3+4)/5"->['1', '2', '+', '3', '+', '4', '+', '5', '/'] = 2.0
"1e6^2/1e7"->['1E6', '2', '^', '1E7', '/'] = 100000.0

+1 有趣的解决方案...使用pyparser的优势是什么?它更安全还是更快? - K DawG
主要优点是更多的输入和输出控制。使用解析器与使用AST literal_eval一样安全。它将验证您的输入是否符合预期,并且只会执行您告诉它执行的内容。这是一个很棒的工具。AST也是一个很棒的工具。 - dawg

1

以下是@poke提供的更新版本,允许在py3.x或其他一元操作符中使用负数。例如,“-3”会计算为-3,而不是出现错误。

import ast, operator

binOps = {
ast.Add: operator.add,
ast.Sub: operator.sub,
ast.Mult: operator.mul,
ast.Div: operator.truediv,
ast.Mod: operator.mod
}

unOps = {
ast.USub: operator.neg
}

node = ast.parse(s, mode='eval')

def arithmetic_eval(s):
    binOps = {
    ast.Add: operator.add,
    ast.Sub: operator.sub,
    ast.Mult: operator.mul,
    ast.Div: operator.truediv,
    ast.Mod: operator.mod
    }

    unOps = {
    ast.USub: operator.neg
    }

    node = ast.parse(s, mode='eval')

    def _eval(node):
        if isinstance(node, ast.Expression):
            return _eval(node.body)
        elif isinstance(node, ast.Str):
            return node.s
        elif isinstance(node, ast.Num):
            return node.n
        elif isinstance(node, ast.BinOp):
            return binOps[type(node.op)](_eval(node.left), _eval(node.right))
        elif isinstance(node, ast.UnaryOp):
            return unOps[type(node.op)](_eval(node.operand))
        else:
            raise Exception('Unsupported type {}'.format(node))

    return _eval(node.body)

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接