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


113
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可以解决此问题,但是难道没有更好,更重要的是更安全的方法来评估存储在字符串中的数学表达式吗?


6
^是XOR运算符。期望值为6。您可能需要pow(2,4)。
kgiannakakis

25
或更多以Python方式2 ** 4
fortran

1
如果您不想使用eval,那么唯一的解决方案是实现适当的语法解析器。看看pyparsing
kgiannakakis

Answers:


108

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

你可以这样使用

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

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

180

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

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:

29
非常酷的帖子,谢谢。我已经采用了这个概念,并试图创建一个易于使用的库:github.com/danthedeckie/simpleeval
Daniel Fairhead

可以扩展功能import math吗?
Hotschke 2014年

2
请注意,这ast.parse是不安全的。例如,ast.parse('()' * 1000000, '<string>', 'single')使解释器崩溃。
Antti Haapala

1
@AnttiHaapala很好的例子。这是Python解释器中的错误吗?无论如何,大的输入都是简单的处理,例如使用if len(expr) > 10000: raise ValueError
jfs

1
@AnttiHaapala您能提供一个无法使用len(expr)支票固定的示例吗?还是您的观点是Python实施中存在错误,因此通常不可能编写安全的代码?
jfs 2016年


10

好的,所以eval的问题在于,即使您摆脱,它也很容易逃脱其沙箱__builtins__。逃避沙箱的所有方法归结为使用getattrobject.__getattribute__(通过.操作员)通过某个允许的对象(''.__class__.__bases__[0].__subclasses__或类似对象)获得对某个危险对象的引用。 getattr通过设置消除__builtins__Noneobject.__getattribute__之所以困难,是因为它不能简单地删除,这既是因为它object是不可变的,又是因为删除它会破坏一切。但是,__getattribute__只能通过.操作员进行访问,因此清除您的输入内容就足以确保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到允许遵循的事物列表中.,但是为什么要麻烦呢?


这里可以找到一个有趣的讨论相关问题。
djvg

3
目前关于删除的论点.是否正确,如果将来的Python版本引入新的语法以允许通过其他方式访问不安全的对象或函数,则这将留下安全漏洞的可能性。由于f字符串,此解决方案在Python 3.6中已经是不安全的,它会导致以下攻击:f"{eval('()' + chr(46) + '__class__')}"。基于白名单而不是黑名单的解决方案将更安全,但实际上最好完全解决此问题eval
kaya3

关于将来的语言功能引入新的安全性问题,这是一个很好的观点。
珀金斯

8

您可以使用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)

因为它通过白名单而不是黑名单工作,所以它是安全的。它可以访问的唯一函数和变量是您明确为其提供访问权限的函数和变量。我在字典中填充了与数学相关的函数,因此您可以根据需要轻松访问这些函数,但是必须显式使用它。

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

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

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

上面的代码仅在Python 3上进行了测试。

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


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]

它的工作方式很简单,在编译过程中可以安全地评估任何常数数学表达式并将其存储为常数。由compile返回的代码对象由组成d,它是的字节码LOAD_CONST,其后是要加载的常量的编号(通常是列表中的最后一个),然后是S,它是用于加载的常量的字节码。RETURN_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将任意复杂的事物简化为一系列简单的说明)。


6

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

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


3
但是,当然,不要依赖于正则表达式来验证任意数学表达式。
高性能Mark

@ High-Performance Mark:是的,我想这取决于他所考虑的数学表达式。。。例如,只是简单的算术数字和+-*/**()或更复杂的东西
蒂姆·古德曼

@Tim-是我担心的()或(((((((()))))))))。实际上,我认为OP应该担心它们,因为OP的问题使我的眉头没有皱纹。
高性能马克

2
eval()即使您限制名称空间,例如控制eval("9**9**9**9**9**9**9**9", {'__builtins__': None})CPU,内存,也不要控制输入,不要使用。
jfs 2012年

3
限制eval的名称空间不会增加安全性
Antti Haapala'3

5

这是一个很晚的答复,但我认为对将来的参考很有用。您可以使用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

确实很酷!A from sympy import *带来了更多的功能支持,例如触发功能,特殊功能等,但是我在这里避免了显示来自何处的信息。


3
sympy是“安全的”吗?似乎有很多 帖子表明它是eval()的包装,可以用相同的方式加以利用。也evalf用不了numpy的ndarrays。
Mark Mikofski 2013年

14
没有sympy对于不可信的输入是不安全的。试试sympy.sympify("""[].__class__.__base__.__subclasses__()[158]('ls')""")这个subprocess.Popen()我传给ls而不是的电话rm -rf /。在其他计算机上,索引可能会有所不同。这是Ned Batchelder攻击的
Mark Mikofski 2013年

1
实际上,它根本不会增加安全性。
Antti Haapala'3

4

[我知道这是一个老问题,但是当它们弹出时,有必要指出新的有用的解决方案]

自python3.6起,此功能现已内置于语言中,即“ f-strings”

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

例如(注意f前缀):

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

7
非常有趣的链接。但是我想f字符串在这里使编写源代码更加容易,而问题似乎是关于在变量内部使用字符串(可能来自不受信任的源)。在这种情况下,不能使用f字符串。
伯恩哈德

有什么方法可以影响f'{2 {operator} 4}',现在您可以指定操作员执行2 + 4或2 * 4或2-4等操作
Skyler

这实际上等同于做str(eval(...)),肯定比做更安全eval
kaya3 '19

似乎与exec / eval一样...
Victor VosMottor感谢Monica

0

使用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

35
的eval( “(1).__类__.__碱基__ [0] .__亚类__()[81]('回波得到through'.split())”,{“ 内建 ':无})#escapes沙箱
珀金斯

6
Python 3.4:eval("""[i for i in (1).__class__.__bases__[0].__subclasses__() if i.__name__.endswith('BuiltinImporter')][0]().load_module('sys').modules['sys'].modules['os'].system('/bin/sh')""", {'__builtins__': None})执行bourne shell ...
Antti Haapala

8
这是不安全的。恶意代码仍然可以执行。
费米悖论

This is not safe-好吧,我认为这与整体使用bash一样安全。顺便说一句:eval('math.sqrt(2.0)')<-“数学”。如上所述,是必需的。
Hannu

0

这是我不使用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])
            )

solution.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)
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.