Python 计算字符串中的数学表达式

Python 计算字符串中的数学表达式,python,math,Python,Math,这将返回以下错误: stringExp = "2^4" intVal = int(stringExp) # Expected value: 16 回溯(最近一次呼叫最后一次): 文件“”,第1行,在 ValueError:int()的文本无效 以10为基数:“2^4” 我知道eval可以解决这个问题,但是没有更好更重要更安全的方法来计算存储在字符串中的数学表达式吗?我想我会使用eval(),但会先检查字符串是否是有效的数学表达式,而不是恶意的东西。您可以使用正则表达式进行验证 ev

这将返回以下错误:

stringExp = "2^4"
intVal = int(stringExp)      # Expected value: 16
回溯(最近一次呼叫最后一次):
文件“”,第1行,在
ValueError:int()的文本无效
以10为基数:“2^4”

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

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

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

可用于解析数学表达式。特别地, 演示如何解析基本算术表达式。下面,我将fourFn重写为一个数值解析器类,以便于重用

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

在干净的命名空间中使用
eval

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

result = nsp.eval('exp(2^4)')
print(result)
# 8886110.520507872
干净的命名空间应该防止注入。例如:

>>> ns = {'__builtins__': None}
>>> eval('2 ** 4', ns)
16
您可能希望授予访问数学模块的权限:

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

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

>>> import math
>>> ns = vars(math).copy()
>>> ns['__builtins__'] = None
>>> eval('cos(pi/3)', ns)
0.50000000000000011
真的很酷!Symphy import*中的
带来了更多的功能支持,如trig函数、特殊函数等,但我在这里避免了这一点,以说明从何而来。

eval
是邪恶的 注意:即使您使用set
\uuu内置项
设置为
None
,也可以使用内省来中断:

eval("__import__('os').remove('important file')") # arbitrary commands
eval("9**9**9**9**9**9**9**9", {'__builtins__': None}) # CPU, memory
使用
ast
或限制中间结果的大小:

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
例子
一些比
eval()和*)更安全的替代方案:

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

警告:请注意,此函数使用
eval
,因此不应在未初始化的输入上使用


好的,eval的问题是它太容易逃出沙箱,即使你去掉了
\uuuuu内置项。逃离沙箱的所有方法归结为使用
getattr
对象。\uuuuu getattribute\uuuuu
(通过
操作符)通过某个允许的对象(
”。\uuuuu类\uuuuu基\uuuuu[0]。\uuu子类\uuuuuuu
或类似的对象)获取对某个危险对象的引用<通过将
\uuu内置项\uuu
设置为
None
可以消除code>getattr
<代码>对象。uuu getattribute_uuu
是一个困难的问题,因为它不能简单地删除,因为
对象
是不可变的,而且删除它会破坏一切。但是,
\uuu getattribute\uuu
只能通过
操作符访问,因此从您的输入中清除该属性就足以确保eval无法逃脱其沙箱。
在处理公式时,十进制的唯一有效用法是在其前面或后面加上
[0-9]
,因此我们只需删除
的所有其他实例

>>> 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:

请注意,虽然python通常将
1+1.
视为
1+1.0
,但这将删除尾随的
,并留下
1+1
。您可以将
EOF
添加到允许遵循的内容列表中,但为什么要麻烦呢?

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

import re
inp = re.sub(r"\.(?![0-9])","", inp)
val = eval(inp, {'__builtins__':None})
导入ast,数学
局部变量={key:vars(math).items()中(key,value)的值,如果键[0]!='\u'}
更新({“abs”:abs,“complex”:complex,“min”:min,“max”:max,“pow”:pow,“round”:round})
类访问者(ast.NodeVisitor):
def访问(自我,节点):
如果不存在(节点,self.whitelist):
提升值错误(节点)
return super().访问(节点)
白名单=(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.FLOWORDIV、ast.Mod、,
ast.LShift、ast.RShift、ast.Invert、ast.Call、ast.Name)
def evaluate(expr,locals={}):
如果有(expr中的元素表示“\n#”中的元素):raise VALUE ERROR(expr)
尝试:
node=ast.parse(expr.strip(),mode='eval')
Visitor().visit(节点)
返回eval(编译(节点,“,”eval“,{uuuuu内置项:无},局部变量)
例外情况除外:提升值错误(expr)
因为它通过白名单而不是黑名单工作,所以它是安全的。它可以访问的唯一函数和变量是那些您显式授予它访问权限的函数和变量。我用数学相关函数填充了一个dict,因此如果需要,您可以轻松地提供对这些函数的访问,但您必须显式地使用它

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

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

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)

评估(“7+9*(2原因
eval
exec
非常危险,因为默认的
compile
函数将为任何有效的python表达式生成字节码,而默认的
eval
exec
将执行任何有效的python字节码。到目前为止,所有的答案都集中在限制可以执行的字节码可以生成(通过清理输入)或使用AST构建自己的特定于域的语言

相反,您可以轻松创建一个简单的
eval
函数,该函数无法执行
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)
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:
import re
inp = re.sub(r"\.(?![0-9])","", inp)
val = eval(inp, {'__builtins__':None})
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)
>>> evaluate("7 + 9 * (2 << 2)")
79
>>> evaluate("6 // 2 + 0.0")
3.0
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]
stringExp = "1 + cos(2)"
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'))
f'{2**4}'
=> '16'
$ python -m pytest 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])
            )
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)
import re, sys

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

def simple_eval(expr):
    c = compile(expr, 'userinput', 'eval')
    m = _re_simple_eval.fullmatch(c.co_code)
    if not m:
        raise ValueError(f"Not a simple algebraic expresion: {expr}")
    return c.co_consts[int.from_bytes(m.group(1), sys.byteorder)]
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