评估一个string中的mathexpression式

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

这将返回以下错误:

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

我知道eval可以解决这个问题,但是如何评估一个正在存储在一个string中的mathexpression式并不是一个更好,更重要的更安全的方法?

Pyparsing可以用来parsingmathexpression式。 特别是, fourFn.py展示了如何parsing基本的算术expression式。 下面,我已经将fourFn重新包装为一个数字parsing器类,以便于重用。

 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是邪恶的

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

注意:即使你将set __builtins__设置为None ,仍然有可能使用自省:

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

使用ast评估算术expression式

 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> eg, -1 return operators[type(node.op)](eval_(node.operand)) else: raise TypeError(node) 

您可以轻松地限制每个操作或任何中间结果的允许范围,例如限制a**binput参数:

 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().evalf() *

  • asteval
  • numexpr

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

警告:请注意,此函数使用eval ,因此不应用于unsanitizedinput。

好的,所以eval的问题在于它可以很容易地逃离它的沙箱,即使你抛弃了__builtins__ 。 所有逃避沙箱的方法归结为使用getattrobject.__getattribute__ (通过.运算符)通过一些允许的对象( ''.__class__.__bases__[0].__subclasses__或类似的)获得对某个危险对象的引用。 通过将__builtins__设置为None来消除getattrobject.__getattribute__是困难的,因为它不能简单的被删除,因为object是不可变的,因为删除它会破坏一切。 但是, __getattribute__只能通过. 运营商,所以从您的input清除足以确保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列表中允许遵循的事情. ,但是为什么呢?

evalexec的原因非常危险,默认的compile函数会为任何有效的pythonexpression式生成字节码,并且默认的eval或者exec会执行任何有效的python字节码。 所有迄今为止的答案都集中在限制可以生成的字节码(通过消毒input)或使用AST构build自己的域特定语言。

相反,您可以轻松地创build一个简单的eval函数,该函数无法做任何恶意的事情,并且可以轻松地对运行时间进行内存检查或使用时间。 当然,如果它是简单的math,比有一个捷径。

 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] 

这个工作的方式很简单,任何常数的mathexpression式在编译过程中被安全的评估并且被存储为一个常量。 编译器返回的代码对象包括d ,它是LOAD_CONST的字节码,后面是要加载的常量(通常是列表中的最后一个)的编号,后面是S ,这是RETURN_VALUE的字节码。 如果这个快捷方式不起作用,这意味着用户input不是一个常量expression式(包含一个variables或函数调用或类似的)。

这也打开了一些更复杂的input格式的大门。 例如:

 stringExp = "1 + cos(2)" 

这需要实际评估字节码,这仍然非常简单。 Python字节码是一种面向堆栈的语言,所以一切都是简单的TOS=stack.pop(); op(TOS); stack.put(TOS) TOS=stack.pop(); op(TOS); stack.put(TOS) TOS=stack.pop(); op(TOS); stack.put(TOS)或类似的。 关键是只实现安全的操作码(加载/存储值,math运算,返回值)而不是不安全的操作(属性查找)。 如果你希望用户能够调用函数(整个原因不使用上面的快捷方式),简单的让你的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个是math相关的)。 添加STORE_FAST和其他一些可以允许input像'x=5;return x+x或类似,轻而易举。 它甚至可以用来执行用户创build的函数,只要用户创build的函数是通过VMeval自己执行的(不要使它们可调用!!!或者它们可以用作callback的地方)。 处理循环需要支持goto字节码,这意味着从迭代器变为while和保持指向当前指令的指针,但不是太难。 为了抵抗DOS,主循环应该检查自计算开始以来已经过了多less时间,并且某些运算符应该在某个合理的限制( BINARY_POWER是最明显的)上拒绝input。

虽然这种方法比简单expression式的简单语法分析器稍长一些(参见上面关于刚刚获取编译常量的内容),但它很容易扩展到更复杂的input,并且不需要处理语法( compile任意复杂的任何东西并减less它到一系列简单的指令)。

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

 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) 

因为它通过白名单,而不是黑名单,它是安全的。 它可以访问的唯一函数和variables是您明确赋予其访问权限的variables。 我填充了与math相关的函数的一个字典,以便您可以轻松地提供对这些的访问权限,但是您必须明确地使用它。

如果string尝试调用尚未提供的函数或调用任何方法,则会引发exception,并且不会执行。

因为它使用Python内置的parsing器和评估器,所以它也inheritance了Python的优先级和升级规则。

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

上面的代码只在Python 3上testing过。

如果需要的话,你可以在这个函数上添加一个超时装饰器。

这是一个非常迟的答复,但我认为对未来的参考很有用。 而不是编写自己的mathparsing器(尽pipe上面的pyparsing示例很好),您可以使用SymPy。 我没有太多的经验,但它包含了比任何人都可能为特定应用程序编写的function更强大的math引擎,基本的expression式评估非常简单:

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

确实非常酷! from sympy import *带来了更多的function支持,比如trig函数,特殊函数等,但是我在这里避免了显示来自哪里的内容。

我想我会使用eval() ,但首先检查,以确保该string是一个有效的mathexpression式,而不是恶意的东西。 你可以使用正则expression式进行validation。

eval()也可以使用额外的参数来限制它所使用的命名空间,以提高安全性。

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

自从python3.6以来,这个function现在被embedded到语言中 ,创造了“f-string”

请参阅: PEP 498 – string插值

例如(注意f前缀):

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

如果你不想使用eval,那么唯一的解决scheme是实现适当的语法分析器。 看看pyparsing 。

在干净的命名空间中使用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 

您可能想要访问math模块:

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

Python已经有一个函数来安全地计算包含文字expression式的string:

http://docs.python.org/2/library/ast.html#ast.literal_eval

如果你已经使用了wolframalpha,他们有一个python api,它允许你评估expression式。 可能有点慢,但至less非常准确。

https://pypi.python.org/pypi/wolframalpha