评估字符串中的数学表达式

Pie*_*ter 99 python math

stringExp = "2^4"
intVal = int(stringExp)      # Expected value: 16
Run Code Online (Sandbox Code Playgroud)

这将返回以下错误:

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

我知道eval可以解决这个问题,但是不是有更好的 - 更重要的 - 更安全的方法来评估存储在字符串中的数学表达式吗?

jfs*_*jfs 168

eval 是邪恶的

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

注意:即使你使用set __builtins__,None它仍然可以使用内省突破:

eval('(1).__class__.__bases__[0].__subclasses__()', {'__builtins__': None})
Run Code Online (Sandbox Code Playgroud)

使用.评估算术表达式 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)
Run Code Online (Sandbox Code Playgroud)

您可以轻松限制每个操作或任何中间结果的允许范围,例如,限制输入参数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
Run Code Online (Sandbox Code Playgroud)

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

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_)
Run Code Online (Sandbox Code Playgroud)

>>> 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:
Run Code Online (Sandbox Code Playgroud)

  • 很酷的帖子,谢谢.我采用了这个概念,并试图建立一个易于使用的库:https://github.com/danthedeckie/simpleeval (23认同)
  • 另一个使用这种方法的图书馆:http://newville.github.io/asteval/ (8认同)
  • 请注意,`ast.parse`不安全.例如`ast.parse('()'*1000000,'<string>','single')`崩溃解释器. (2认同)
  • @AnttiHaapala 您能否提供一个无法使用 `len(expr)` 检查修复的示例?或者您的观点是 Python 实现中存在错误,因此一般不可能编写安全的代码? (2认同)

unu*_*tbu 98

Pyparsing可用于解析数学表达式.特别是,fourFn.py 显示了如何解析基本算术表达式.下面,我将fourFn重新编译为数字解析器类,以便于重用.

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
Run Code Online (Sandbox Code Playgroud)

你可以像这样使用它

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

result = nsp.eval('exp(2^4)')
print(result)
# 8886110.520507872
Run Code Online (Sandbox Code Playgroud)


Mar*_*ski 13

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

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

警告:请注意,此函数使用eval,因此不应在未过滤的输入上使用.


Per*_*ins 7

好的,所以eval的问题在于它可以轻松地逃脱它的沙箱,即使你摆脱了它__builtins__.所有用于转义沙箱的方法都归结为使用getattrobject.__getattribute__(通过.运算符)通过一些允许的对象(''.__class__.__bases__[0].__subclasses__或类似的)获得对某些危险对象的引用. getattr通过设置__builtins__来消除None. object.__getattribute__是困难的,因为它不能简单地被删除,因为它object是不可变的,因为删除它会破坏一切.但是,__getattribute__只能通过.操作员访问,因此从输入中清除它足以确保eval无法逃脱其沙箱.
在处理公式时,十进制的唯一有效用途是在它之前或之后[0-9],所以我们只删除所有其他实例..

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

请注意,虽然蟒蛇一般把1 + 1.作为1 + 1.0,这将删除尾随.并留下你1 + 1.您可以添加),, and EOF允许遵循的事项列表.,但为什么要这么麻烦?

  • 无论目前有关删除“.”的论点是否正确,如果未来版本的 Python 引入新语法,允许以其他方式访问不安全的对象或函数,则可能会出现安全漏洞。由于 f 字符串,该解决方案在 Python 3.6 中已经不安全,它允许以下攻击:`f"{eval('()' + chr(46) + '__class__')}"`。基于白名单而不是黑名单的解决方案会更安全,但实际上最好完全不使用“eval”来解决这个问题。 (4认同)

Kev*_*vin 7

您可以使用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)
Run Code Online (Sandbox Code Playgroud)

因为它通过白名单而不是黑名单工作,所以它是安全的.它可以访问的唯一函数和变量是您明确授予它访问权限的函数和变量.我使用与数学相关的函数填充了一个dict,因此您可以根据需要轻松提供对它们的访问,但您必须明确使用它.

如果字符串尝试调用尚未提供的函数,或者调用任何方法,则会引发异常,并且不会执行该异常.

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

>>> evaluate("7 + 9 * (2 << 2)")
79
>>> evaluate("6 // 2 + 0.0")
3.0
Run Code Online (Sandbox Code Playgroud)

以上代码仅在Python 3上进行了测试.

如果需要,可以在此函数上添加超时装饰器.


Per*_*ins 7

原因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]
Run Code Online (Sandbox Code Playgroud)

它的工作方式很简单,任何常量数学表达式在编译期间都会被安全地评估并存储为常量.由compile返回的代码对象包括d,其是字节码LOAD_CONST,后跟要加载的常量的数量(通常是列表中的最后一个),后跟S,即字节码RETURN_VALUE.如果此快捷方式不起作用,则表示用户输入不是常量表达式(包含变量或函数调用或类似).

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

stringExp = "1 + cos(2)"
Run Code Online (Sandbox Code Playgroud)

这需要实际评估字节码,这仍然非常简单.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'))
Run Code Online (Sandbox Code Playgroud)

显然,这个版本的实际版本会更长一些(有119个操作码,其中24个是数学相关的).添加STORE_FAST和其他几个允许输入相似'x=5;return x+x或类似,轻松地.它甚至可以用来执行用户创建的函数,只要用户创建的函数本身是通过VMeval执行的(不要让它们可以调用!!!或者它们可以用作某个地方的回调).处理循环需要支持goto字节码,这意味着从for迭代器更改为while维护指向当前指令的指针,但不是太难.对于DOS的阻力,主循环应该检查自计算开始以来经过了多长时间,并且某些操作员应该拒绝超过某个合理限制的输入(BINARY_POWER最明显).

虽然这种做法有些长于一个简单的语法解析器简单表达式(见上文关于仅仅霎那编译常数),它很容易地扩展到更复杂的输入,并且不需要处理语法(compile采取任何任意复杂的,它减少了一系列简单的指示).


Mes*_*ion 7

基于Perkins 的惊人方法,我更新并改进了他的简单代数表达式(无函数或变量)的“快捷方式”。现在它可以在 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}")
Run Code Online (Sandbox Code Playgroud)

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

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))
Run Code Online (Sandbox Code Playgroud)
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
Run Code Online (Sandbox Code Playgroud)

测试错误输入:

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
Run Code Online (Sandbox Code Playgroud)
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
Run Code Online (Sandbox Code Playgroud)

  • 这太棒了。比我的方法干净得多。请注意,将 lambda: 10 作为输入提供会导致未处理的异常,因为您的正则表达式*找到*返回语句,但它与常量返回不匹配。有一些解决方案(查找“MAKE_FUNCTION”操作码,检测“LOAD_CONST”太长)。另外值得注意的是,这仍然容易受到 DoS 攻击(需要外部缓解)。 (2认同)

and*_*ley 6

这是一个非常迟到的回复,但我认为有用,以备将来参考.而不是编写自己的数学解析器(虽然上面的pyparsing示例很棒),您可以使用SymPy.我没有很多经验,但它包含了比任何人可能为特定应用程序编写的更强大的数学引擎,并且基本的表达式评估非常简单:

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

非常酷!A from sympy import *带来了更多功能支持,例如触发功能,特殊功能等,但我在这里避免使用它来显示来自哪里的内容.

  • 对于不信任的输入,没有任何同情是不安全的.试试`sympy.sympify("""[] .__ class __.__ base __.__ subclasses __()[158]('ls')""")`这调用`subprocess.Popen()`我传递`ls`而不是` rm -rf /`.该索引在其他计算机上可能会有所不同.这是[Ned Batchelder exploit]的变种(http://nedbatchelder.com/blog/201206/eval_really_is_dangerous.html) (13认同)
  • 同情"安全"吗?似乎有[众多](https://groups.google.com/d/topic/sympy/YrkrEreij1c/discussion)[帖子](http://stackoverflow.com/questions/16718644/how-safe-is- sympys-sympifystring-evalf)表明它是eval()的包装器,可以以相同的方式被利用."evalf"也不会占用numpy ndarray. (2认同)

shx*_*hx2 6

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

从python3.6开始,这种功能现在内置到语言中,创造了“f-strings”

请参阅:PEP 498 -- 文字字符串插值

例如(注意f前缀):

f'{2**4}'
=> '16'
Run Code Online (Sandbox Code Playgroud)

  • 非常有趣的链接。但我想 f 字符串在这里是为了让编写源代码更容易,而问题似乎是关于使用变量内部的字符串(可能来自不受信任的来源)。在这种情况下不能使用 f 字符串。 (9认同)
  • 这实际上相当于只执行 `str(eval(...))`,所以它肯定不比 `eval` 更安全。 (5认同)

Tim*_*man 5

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

eval() 还采用了其他参数,您可以使用这些参数来限制其操作的命名空间,以提高安全性。

  • 但是,当然,不要依赖正则表达式来验证任意数学表达式。 (3认同)
  • 即使您限制名称空间,也不要使用`eval()`,例如[`eval(“ 9 ** 9 ** 9 ** 9 ** 9 ** 9 ** 9 ** 9 ** 9“,{'__builtins__':None})`](http://stackoverflow.com/a/9558001/4279)消耗CPU,内存。 (2认同)
  • 限制eval **的名称空间不会增加安全性**。 (2认同)