Evaluating a mathematical expression in a string

stringExp = "2^4"
intVal = int(stringExp)      # Expected value: 16

This returns the following error:

Traceback (most recent call last):
File "<stdin>", line 1, in <module>
ValueError: invalid literal for int()
with base 10: '2^4'

I know that eval can work around this, but isn't there a better and - more importantly - safer method to evaluate a mathematical expression that is being stored in a string?

159409 次浏览

我想我会使用 eval(),但首先要确保字符串是一个有效的数学表达式,而不是恶意的表达式。您可以使用正则表达式进行验证。

eval()还带有额外的参数,您可以使用这些参数来限制它所操作的命名空间,以获得更大的安全性。

Pyparsing 可以用来解析数学表达式,特别是,< a href = “ https://github.com/Pyparsing/Pyparsing/blob/master/example/fourFn.py”rel = “ norefrer”> fourFn.py 展示了如何解析基本的算术表达式。

from __future__ import division
from pyparsing import (Literal, CaselessLiteral, Word, Combine, Group, Optional,
ZeroOrMore, Forward, nums, alphas, oneOf)
import math
import operator


__author__ = 'Paul McGuire'
__version__ = '$Revision: 0.0 $'
__date__ = '$Date: 2009-03-20 $'
__source__ = '''http://pyparsing.wikispaces.com/file/view/fourFn.py
http://pyparsing.wikispaces.com/message/view/home/15549426
'''
__note__ = '''
All I've done is rewrap Paul McGuire's fourFn.py as a class, so I can use it
more easily in other places.
'''




class NumericStringParser(object):
'''
Most of this code comes from the fourFn.py pyparsing example


'''


def pushFirst(self, strg, loc, toks):
self.exprStack.append(toks[0])


def pushUMinus(self, strg, loc, toks):
if toks and toks[0] == '-':
self.exprStack.append('unary -')


def __init__(self):
"""
expop   :: '^'
multop  :: '*' | '/'
addop   :: '+' | '-'
integer :: ['+' | '-'] '0'..'9'+
atom    :: PI | E | real | fn '(' expr ')' | '(' expr ')'
factor  :: atom [ expop factor ]*
term    :: factor [ multop factor ]*
expr    :: term [ addop term ]*
"""
point = Literal(".")
e = CaselessLiteral("E")
fnumber = Combine(Word("+-" + nums, nums) +
Optional(point + Optional(Word(nums))) +
Optional(e + Word("+-" + nums, nums)))
ident = Word(alphas, alphas + nums + "_$")
plus = Literal("+")
minus = Literal("-")
mult = Literal("*")
div = Literal("/")
lpar = Literal("(").suppress()
rpar = Literal(")").suppress()
addop = plus | minus
multop = mult | div
expop = Literal("^")
pi = CaselessLiteral("PI")
expr = Forward()
atom = ((Optional(oneOf("- +")) +
(ident + lpar + expr + rpar | pi | e | fnumber).setParseAction(self.pushFirst))
| Optional(oneOf("- +")) + Group(lpar + expr + rpar)
).setParseAction(self.pushUMinus)
# by defining exponentiation as "atom [ ^ factor ]..." instead of
# "atom [ ^ atom ]...", we get right-to-left exponents, instead of left-to-right
# that is, 2^3^2 = 2^(3^2), not (2^3)^2.
factor = Forward()
factor << atom + \
ZeroOrMore((expop + factor).setParseAction(self.pushFirst))
term = factor + \
ZeroOrMore((multop + factor).setParseAction(self.pushFirst))
expr << term + \
ZeroOrMore((addop + term).setParseAction(self.pushFirst))
# addop_term = ( addop + term ).setParseAction( self.pushFirst )
# general_term = term + ZeroOrMore( addop_term ) | OneOrMore( addop_term)
# expr <<  general_term
self.bnf = expr
# map operator symbols to corresponding arithmetic operations
epsilon = 1e-12
self.opn = {"+": operator.add,
"-": operator.sub,
"*": operator.mul,
"/": operator.truediv,
"^": operator.pow}
self.fn = {"sin": math.sin,
"cos": math.cos,
"tan": math.tan,
"exp": math.exp,
"abs": abs,
"trunc": lambda a: int(a),
"round": round,
"sgn": lambda a: abs(a) > epsilon and cmp(a, 0) or 0}


def evaluateStack(self, s):
op = s.pop()
if op == 'unary -':
return -self.evaluateStack(s)
if op in "+-*/^":
op2 = self.evaluateStack(s)
op1 = self.evaluateStack(s)
return self.opn[op](op1, op2)
elif op == "PI":
return math.pi  # 3.1415926535
elif op == "E":
return math.e  # 2.718281828
elif op in self.fn:
return self.fn[op](self.evaluateStack(s))
elif op[0].isalpha():
return 0
else:
return float(op)


def eval(self, num_string, parseAll=True):
self.exprStack = []
results = self.bnf.parseString(num_string, parseAll)
val = self.evaluateStack(self.exprStack[:])
return val

你可以像这样使用它

nsp = NumericStringParser()
result = nsp.eval('2^4')
print(result)
# 16.0


result = nsp.eval('exp(2^4)')
print(result)
# 8886110.520507872

在干净的名称空间中使用 eval:

>>> ns = {'__builtins__': None}
>>> eval('2 ** 4', ns)
16

干净的命名空间应该可以防止注入。例如:

>>> eval('__builtins__.__import__("os").system("echo got through")', ns)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "<string>", line 1, in <module>
AttributeError: 'NoneType' object has no attribute '__import__'

否则你会得到:

>>> eval('__builtins__.__import__("os").system("echo got through")')
got through
0

您可能需要访问数学模块:

>>> import math
>>> ns = vars(math).copy()
>>> ns['__builtins__'] = None
>>> eval('cos(pi/3)', ns)
0.50000000000000011

这是一个非常晚的回复,但我认为对将来的参考有用。您可以使用 SymPy 而不是编写您自己的数学解析器(尽管上面的 pyparsing 示例很棒)。我没有多少使用它的经验,但是它包含了一个比任何人为特定应用程序编写的都要强大得多的数学引擎,而且基本的表达式求值非常简单:

>>> import sympy
>>> x, y, z = sympy.symbols('x y z')
>>> sympy.sympify("x**3 + sin(y)").evalf(subs={x:1, y:-3})
0.858879991940133

确实很酷!from sympy import *带来了更多的函数支持,比如触发函数、特殊函数等等,但是我在这里避免了这一点,以便展示哪些函数来自哪里。

eval是邪恶的

eval("__import__('os').remove('important file')") # arbitrary commands
eval("9**9**9**9**9**9**9**9", {'__builtins__': None}) # CPU, memory

注意: 即使你使用设置 __builtins__None,它仍然可能突破使用内省:

eval('(1).__class__.__bases__[0].__subclasses__()', {'__builtins__': None})

使用 ast计算算术表达式

import ast
import operator as op


# supported operators
operators = {ast.Add: op.add, ast.Sub: op.sub, ast.Mult: op.mul,
ast.Div: op.truediv, ast.Pow: op.pow, ast.BitXor: op.xor,
ast.USub: op.neg}


def eval_expr(expr):
"""
>>> eval_expr('2^6')
4
>>> eval_expr('2**6')
64
>>> eval_expr('1 + 2*3**(4^5) / (6 + -7)')
-5.0
"""
return eval_(ast.parse(expr, mode='eval').body)


def eval_(node):
if isinstance(node, ast.Num): # <number>
return node.n
elif isinstance(node, ast.BinOp): # <left> <operator> <right>
return operators[type(node.op)](eval_(node.left), eval_(node.right))
elif isinstance(node, ast.UnaryOp): # <operator> <operand> e.g., -1
return operators[type(node.op)](eval_(node.operand))
else:
raise TypeError(node)

您可以很容易地限制每个操作或任何中间结果的允许范围,例如,限制 a**b的输入参数:

def power(a, b):
if any(abs(n) > 100 for n in [a, b]):
raise ValueError((a,b))
return op.pow(a, b)
operators[ast.Pow] = power

或者限制中间结果的大小:

import functools


def limit(max_=None):
"""Return decorator that limits allowed returned values."""
def decorator(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
ret = func(*args, **kwargs)
try:
mag = abs(ret)
except TypeError:
pass # not applicable
else:
if mag > max_:
raise ValueError(ret)
return ret
return wrapper
return decorator


eval_ = limit(max_=10**100)(eval_)

例子

>>> evil = "__import__('os').remove('important file')"
>>> eval_expr(evil) #doctest:+IGNORE_EXCEPTION_DETAIL
Traceback (most recent call last):
...
TypeError:
>>> eval_expr("9**9")
387420489
>>> eval_expr("9**9**9**9**9**9**9**9") #doctest:+IGNORE_EXCEPTION_DETAIL
Traceback (most recent call last):
...
ValueError:

eval()sympy.sympify().evalf()*的一些更安全的替代品:

* SymPy sympify也是不安全的,根据文档中的以下警告。

警告: 注意,这个函数使用 eval,因此不应该在未经过消毒的输入上使用。

Eval 的问题在于,即使去掉了 __builtins__,它也很容易逃脱沙箱。所有逃离沙盒的方法都可以归结为使用 getattrobject.__getattribute__(通过 .操作符)通过某个允许的对象(''.__class__.__bases__[0].__subclasses__或类似对象)获得对某个危险对象的引用。通过将 __builtins__设置为 None可以消除 getattrobject.__getattribute__是困难的一个,因为它不能简单地被删除,既因为 object是不可变的,也因为删除它会打破一切。但是,getattr0只能通过 .操作符访问,因此从您的输入中进行清除就足以确保 eval 无法逃脱其沙盒。
在处理公式时,十进制的唯一有效用法是在它的前面或后面加上 [0-9],所以我们只需删除 .的所有其他实例。

import re
inp = re.sub(r"\.(?![0-9])","", inp)
val = eval(inp, {'__builtins__':None})

请注意,虽然 python 通常将 1 + 1.视为 1 + 1.0,但这将删除后面的 .,并将 1 + 1留给您。你可以将 )EOF添加到允许遵循 .的事情列表中,但是为什么要麻烦呢?

您可以使用 ast 模块并编写一个 NodeVisitor 来验证每个节点的类型是否是白名单的一部分。

import ast, math


locals =  {key: value for (key,value) in vars(math).items() if key[0] != '_'}
locals.update({"abs": abs, "complex": complex, "min": min, "max": max, "pow": pow, "round": round})


class Visitor(ast.NodeVisitor):
def visit(self, node):
if not isinstance(node, self.whitelist):
raise ValueError(node)
return super().visit(node)


whitelist = (ast.Module, ast.Expr, ast.Load, ast.Expression, ast.Add, ast.Sub, ast.UnaryOp, ast.Num, ast.BinOp,
ast.Mult, ast.Div, ast.Pow, ast.BitOr, ast.BitAnd, ast.BitXor, ast.USub, ast.UAdd, ast.FloorDiv, ast.Mod,
ast.LShift, ast.RShift, ast.Invert, ast.Call, ast.Name)


def evaluate(expr, locals = {}):
if any(elem in expr for elem in '\n#') : raise ValueError(expr)
try:
node = ast.parse(expr.strip(), mode='eval')
Visitor().visit(node)
return eval(compile(node, "<string>", "eval"), {'__builtins__': None}, locals)
except Exception: raise ValueError(expr)

因为它通过白名单而不是黑名单工作,所以它是安全的。它唯一可以访问的函数和变量是那些您显式地赋予它访问权限的函数和变量。我用与数学相关的函数填充了一个 dict,这样您就可以轻松地提供对这些函数的访问,如果您愿意的话,但是您必须显式地使用它。

如果字符串试图调用尚未提供的函数,或调用任何方法,将引发异常,并且不会执行异常。

因为它使用了 Python 内置的解析器和计算器,所以它也继承了 Python 的优先级和提升规则。

>>> evaluate("7 + 9 * (2 << 2)")
79
>>> evaluate("6 // 2 + 0.0")
3.0

上面的代码只在 Python3上测试过。

如果需要,可以在此函数上添加超时修饰符。

evalexec如此危险的原因是,默认的 compile函数将为任何有效的 python 表达式生成字节码,而默认的 evalexec将执行任何有效的 python 字节码。到目前为止,所有的答案都集中在限制可以生成的字节码(通过清理输入)或使用 AST 构建自己的特定于领域的语言。

相反,您可以轻松地创建一个简单的 eval函数,该函数不能做任何有害的事情,并且可以轻松地在运行时检查所使用的内存或时间。当然,如果是简单的数学,比有一个捷径。

c = compile(stringExp, 'userinput', 'eval')
if c.co_code[0]==b'd' and c.co_code[3]==b'S':
return c.co_consts[ord(c.co_code[1])+ord(c.co_code[2])*256]

其工作原理很简单,任何常量数学表达式都可以在编译期间安全地进行计算,并作为常量存储。通过编译返回的代码对象由 d组成,dLOAD_CONST的字节码,接着是要加载的常量数(通常是列表中的最后一个) ,然后是 SRETURN_VALUE的字节码。如果这个快捷方式不起作用,这意味着用户输入不是常量表达式(包含变量或函数调用或类似的)。

这也为一些更复杂的输入格式打开了大门,例如:

stringExp = "1 + cos(2)"

这需要实际计算字节码,这仍然很简单。Python 字节码是一种面向堆栈的语言,所以一切都是简单的 TOS=stack.pop(); op(TOS); stack.put(TOS)或类似的东西。关键是只实现安全的操作码(加载/存储值、数学操作、返回值) ,而不是不安全的操作码(属性查找)。如果您希望用户能够调用函数(不使用上述快捷方式的全部原因) ,那么简单地让您的 CALL_FUNCTION实现只允许“安全”列表中的函数。

from dis import opmap
from Queue import LifoQueue
from math import sin,cos
import operator


globs = {'sin':sin, 'cos':cos}
safe = globs.values()


stack = LifoQueue()


class BINARY(object):
def __init__(self, operator):
self.op=operator
def __call__(self, context):
stack.put(self.op(stack.get(),stack.get()))


class UNARY(object):
def __init__(self, operator):
self.op=operator
def __call__(self, context):
stack.put(self.op(stack.get()))




def CALL_FUNCTION(context, arg):
argc = arg[0]+arg[1]*256
args = [stack.get() for i in range(argc)]
func = stack.get()
if func not in safe:
raise TypeError("Function %r now allowed"%func)
stack.put(func(*args))


def LOAD_CONST(context, arg):
cons = arg[0]+arg[1]*256
stack.put(context['code'].co_consts[cons])


def LOAD_NAME(context, arg):
name_num = arg[0]+arg[1]*256
name = context['code'].co_names[name_num]
if name in context['locals']:
stack.put(context['locals'][name])
else:
stack.put(context['globals'][name])


def RETURN_VALUE(context):
return stack.get()


opfuncs = {
opmap['BINARY_ADD']: BINARY(operator.add),
opmap['UNARY_INVERT']: UNARY(operator.invert),
opmap['CALL_FUNCTION']: CALL_FUNCTION,
opmap['LOAD_CONST']: LOAD_CONST,
opmap['LOAD_NAME']: LOAD_NAME
opmap['RETURN_VALUE']: RETURN_VALUE,
}


def VMeval(c):
context = dict(locals={}, globals=globs, code=c)
bci = iter(c.co_code)
for bytecode in bci:
func = opfuncs[ord(bytecode)]
if func.func_code.co_argcount==1:
ret = func(context)
else:
args = ord(bci.next()), ord(bci.next())
ret = func(context, args)
if ret:
return ret


def evaluate(expr):
return VMeval(compile(expr, 'userinput', 'eval'))

显然,真正的版本会稍微长一点(有119个操作码,其中24个与数学有关)。添加 STORE_FAST和一些其他的将允许像 'x=5;return x+x或类似的输入,非常容易。它甚至可以用来执行用户创建的函数,只要用户创建的函数本身是通过 VMeval 执行的(不要让它们可调用! ! !或者它们可能被用作某个地方的回调)。处理循环需要支持 goto字节码,这意味着从 for迭代器更改为 while并维护指向当前指令的指针,但这并不太难。为了抵抗 DOS,主循环应该检查从计算开始到现在已经过去了多长时间,并且某些操作符应该拒绝超过一些合理限制的输入(BINARY_POWER是最明显的)。

虽然这种方法比用于简单表达式的简单语法分析器稍微长一些(见上文关于获取已编译常量的内容) ,但它很容易扩展到更复杂的输入,而且不需要处理语法(compile采用任意复杂的内容,并将其简化为一系列简单的指令)。

[我知道这是一个老问题,但值得指出新的有用的解决方案,因为他们弹出]

自从 python3.6以来,这个功能现在是 建立在语言之上,命名为 “ f 弦”

见: PEP 498——字符串插值

例如(请注意 f前缀) :

f'{2**4}'
=> '16'

下面是我解决这个问题的方法,不使用 eval。可以在 Python2和 Python3中使用。它不能在负数中使用。

$ python -m pytest test.py

Test.py

from solution import Solutions


class SolutionsTestCase(unittest.TestCase):
def setUp(self):
self.solutions = Solutions()


def test_evaluate(self):
expressions = [
'2+3=5',
'6+4/2*2=10',
'3+2.45/8=3.30625',
'3**3*3/3+3=30',
'2^4=6'
]
results = [x.split('=')[1] for x in expressions]
for e in range(len(expressions)):
if '.' in results[e]:
results[e] = float(results[e])
else:
results[e] = int(results[e])
self.assertEqual(
results[e],
self.solutions.evaluate(expressions[e])
)

解决方案 py

class Solutions(object):
def evaluate(self, exp):
def format(res):
if '.' in res:
try:
res = float(res)
except ValueError:
pass
else:
try:
res = int(res)
except ValueError:
pass
return res
def splitter(item, op):
mul = item.split(op)
if len(mul) == 2:
for x in ['^', '*', '/', '+', '-']:
if x in mul[0]:
mul = [mul[0].split(x)[1], mul[1]]
if x in mul[1]:
mul = [mul[0], mul[1].split(x)[0]]
elif len(mul) > 2:
pass
else:
pass
for x in range(len(mul)):
mul[x] = format(mul[x])
return mul
exp = exp.replace(' ', '')
if '=' in exp:
res = exp.split('=')[1]
res = format(res)
exp = exp.replace('=%s' % res, '')
while '^' in exp:
if '^' in exp:
itm = splitter(exp, '^')
res = itm[0] ^ itm[1]
exp = exp.replace('%s^%s' % (str(itm[0]), str(itm[1])), str(res))
while '**' in exp:
if '**' in exp:
itm = splitter(exp, '**')
res = itm[0] ** itm[1]
exp = exp.replace('%s**%s' % (str(itm[0]), str(itm[1])), str(res))
while '/' in exp:
if '/' in exp:
itm = splitter(exp, '/')
res = itm[0] / itm[1]
exp = exp.replace('%s/%s' % (str(itm[0]), str(itm[1])), str(res))
while '*' in exp:
if '*' in exp:
itm = splitter(exp, '*')
res = itm[0] * itm[1]
exp = exp.replace('%s*%s' % (str(itm[0]), str(itm[1])), str(res))
while '+' in exp:
if '+' in exp:
itm = splitter(exp, '+')
res = itm[0] + itm[1]
exp = exp.replace('%s+%s' % (str(itm[0]), str(itm[1])), str(res))
while '-' in exp:
if '-' in exp:
itm = splitter(exp, '-')
res = itm[0] - itm[1]
exp = exp.replace('%s-%s' % (str(itm[0]), str(itm[1])), str(res))


return format(exp)

基于 珀金斯的绝妙方法,我更新和改进了他的简单代数表达式(没有函数或变量)的“快捷方式”。现在它可以在 Python 3.6 + 上运行,避免了一些陷阱:

import re


# Kept outside simple_eval() just for performance
_re_simple_eval = re.compile(rb'd([\x00-\xFF]+)S\x00')


def simple_eval(expr):
try:
c = compile(expr, 'userinput', 'eval')
except SyntaxError:
raise ValueError(f"Malformed expression: {expr}")
m = _re_simple_eval.fullmatch(c.co_code)
if not m:
raise ValueError(f"Not a simple algebraic expression: {expr}")
try:
return c.co_consts[int.from_bytes(m.group(1), sys.byteorder)]
except IndexError:
raise ValueError(f"Expression not evaluated as constant: {expr}")

测试,使用其他答案中的一些例子:

for expr, res in (
('2^4',                         6      ),
('2**4',                       16      ),
('1 + 2*3**(4^5) / (6 + -7)',  -5.0    ),
('7 + 9 * (2 << 2)',           79      ),
('6 // 2 + 0.0',                3.0    ),
('2+3',                         5      ),
('6+4/2*2',                    10.0    ),
('3+2.45/8',                    3.30625),
('3**3*3/3+3',                 30.0    ),
):
result = simple_eval(expr)
ok = (result == res and type(result) == type(res))
print("{} {} = {}".format("OK!" if ok else "FAIL!", expr, result))
OK! 2^4 = 6
OK! 2**4 = 16
OK! 1 + 2*3**(4^5) / (6 + -7) = -5.0
OK! 7 + 9 * (2 << 2) = 79
OK! 6 // 2 + 0.0 = 3.0
OK! 2+3 = 5
OK! 6+4/2*2 = 10.0
OK! 3+2.45/8 = 3.30625
OK! 3**3*3/3+3 = 30.0

测试不良输入:

for expr in (
'foo bar',
'print("hi")',
'2*x',
'lambda: 10',
'2**1234',
):
try:
result = simple_eval(expr)
except ValueError as e:
print(e)
continue
print("OK!")  # will never happen
Malformed expression: foo bar
Not a simple algebraic expression: print("hi")
Expression not evaluated as constant: 2*x
Expression not evaluated as constant: lambda: 10
Expression not evaluated as constant: 2**1234

使用 lark 解析器库 https://stackoverflow.com/posts/67491514/edit

from operator import add, sub, mul, truediv, neg, pow
from lark import Lark, Transformer, v_args


calc_grammar = f"""
?start: sum
?sum: product
| sum "+" product   -> {add.__name__}
| sum "-" product   -> {sub.__name__}
?product: power
| product "*" power  -> {mul.__name__}
| product "/" power  -> {truediv.__name__}
?power: atom
| power "^" atom -> {pow.__name__}
?atom: NUMBER           -> number
| "-" atom         -> {neg.__name__}
| "(" sum ")"


%import common.NUMBER
%import common.WS_INLINE


%ignore WS_INLINE
"""




@v_args(inline=True)
class CalculateTree(Transformer):
add = add
sub = sub
neg = neg
mul = mul
truediv = truediv
pow = pow
number = float




calc_parser = Lark(calc_grammar, parser="lalr", transformer=CalculateTree())
calc = calc_parser.parse




def eval_expr(expression: str) -> float:
return calc(expression)




print(eval_expr("2^4"))
print(eval_expr("-1*2^4"))
print(eval_expr("-2^3 + 1"))
print(eval_expr("2**4"))  # Error


我来这里寻找一个数学表达式解析器。通过阅读一些答案和查找库,我偶然发现了我现在正在使用的 Py 表达式。它基本上可以处理很多运算符和公式构造,但是如果您缺少某些内容,您可以轻松地向它添加新的运算符/函数。

基本语法是:

from py_expression.core import Exp
exp = Exp()


parsed_formula = exp.parse('a+4')


result = exp.eval(parsed_formula, {"a":2})

到目前为止,我遇到的唯一问题是,它没有内置的数学常数,也没有一个机制来添加它们。然而,我刚刚提出了一个解决方案: https://github.com/FlavioLionelRita/py-expression/issues/7