【问题标题】:Evaluating a mathematical expression in a string计算字符串中的数学表达式
【发布时间】:2015-12-03 08:59:13
【问题描述】:
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 可以解决这个问题,但难道没有更好、更重要的是更安全的方法来评估存储在字符串中的数学表达式吗?

【问题讨论】:

  • ^ 是 XOR 运算符。预期值为 6。您可能需要 pow(2,4)。
  • 或更多python 2**4
  • 如果不想使用eval,那么唯一的解决办法就是实现相应的语法解析器。看看pyparsing。
  • 简单操作可以查看这段代码github.com/louisfisch/mathematical-expressions-parser
  • 要么您应该采用@fortran 的方法,要么您需要拥有自己的解析器和自定义运算符的评估器。

标签: python math


【解决方案1】:

eval 是邪恶的

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

注意:即使你使用 set __builtins__ to 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:

【讨论】:

  • 非常酷的帖子,谢谢。我接受了这个概念,并尝试制作一个易于使用的库:github.com/danthedeckie/simpleeval
  • 可以扩展import math的功能吗?
  • 请注意ast.parse 不安全。例如ast.parse('()' * 1000000, '&lt;string&gt;', 'single') 使解释器崩溃。
  • @AnttiHaapala 很好的例子。它是 Python 解释器中的错误吗?无论如何,大输入很容易处理,例如,使用if len(expr) &gt; 10000: raise ValueError。
  • @AnttiHaapala 您能否提供一个使用len(expr) 检查无法修复的示例?或者您的观点是 Python 实现中存在错误,因此一般不可能编写安全代码?
【解决方案2】:

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

【讨论】:

    【解决方案3】:

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

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

    警告:请注意,此函数使用eval,因此不应用于未经处理的输入。

    【讨论】:

      【解决方案4】:

      好的,所以 eval 的问题是它可以很容易地逃离它的沙箱,即使你摆脱了__builtins__。逃避沙箱的所有方法都归结为使用getattr 或object.__getattribute__(通过. 运算符)通过某些允许的对象(''.__class__.__bases__[0].__subclasses__ 或类似的)获取对某些危险对象的引用。通过将__builtins__ 设置为None,可以消除getattr。 object.__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 添加到允许关注. 的列表中,但何必呢?

      【讨论】:

      • 一个相关的问题和有趣的讨论可以找到here。
      • 目前关于删除 . 的论点是否正确,如果未来版本的 Python 引入允许访问不安全对象或函数的新语法,这会留下潜在的安全漏洞大大地。这个解决方案在 Python 3.6 中已经不安全,因为 f-strings 允许以下攻击:f"{eval('()' + chr(46) + '__class__')}"。基于白名单而不是黑名单的解决方案会更安全,但实际上最好在没有eval 的情况下解决这个问题。
      • 关于未来语言功能引入新的安全问题,这是一个很好的观点。
      【解决方案5】:

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

      以上代码仅在 Python 3 上测试过。

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

      【讨论】:

        【解决方案6】:

        eval 和exec 如此危险的原因是默认的compile 函数将为任何有效的python 表达式生成字节码,而默认的eval 或exec 将执行任何有效的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 采取任何措施任意复杂并将其简化为一系列简单指令)。

        【讨论】:

        • 感谢这个惊人的快捷方式!但是有一个错误,至少在 Python 3.6 中:c.co_code[0]==b'd' 总是计算为 False,因为奇怪的是,b'foo'[0] 计算为 int,而不是 bytes。解决方法是使用 c.co_code[0:1]==b'd' 或 chr(c.co_code[0])=='d'
        • 在尝试使快捷方式工作时,我偶然发现了其他问题,因此我创建了一个适用于 Python 3.6+ 的函数并尝试涵盖一些极端情况:stackoverflow.com/a/65945969/624066
        • 我大约 5 年前写了这篇文章......我什至不记得我的目标是什么 python 版本,很可能是 2.7。更快的比较是c.co_code[0]==100。我几乎从不需要just数学输入,所以第二种方法是我实际继续使用的方法。
        【解决方案7】:

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

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

        【讨论】:

        • 但是,当然,不要依赖正则表达式来验证任意数学表达式。
        • @High-Performance Mark:是的,我想这取决于他心中的数学表达式。 . .例如,只是简单的数字算术和+,-,*,/,**,(,) 或更复杂的东西
        • @Tim -- 我担心的是 (),或者更确切地说是 (((((()))))))。事实上,我认为 OP 应该担心他们,我的眉毛没有被 OP 的问题所困扰。
        • 如果你不控制输入,即使你限制了命名空间,也不要使用eval(),例如eval("9**9**9**9**9**9**9**9", {'__builtins__': None})会消耗CPU、内存。
        • 限制 eval 的命名空间不会增加安全性。
        【解决方案8】:

        这是一个很晚的回复,但我认为对将来的参考很有用。与其编写自己的数学解析器(尽管上面的 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
        

        确实很酷! from sympy import * 带来了更多的函数支持,例如三角函数、特殊函数等,但我在这里避免了这些,以显示来自哪里。

        【讨论】:

        • sympy“安全”吗?似乎有 numerous posts 表明它是 eval() 的包装器,可以以相同的方式被利用。 evalf 也不需要 numpy ndarrays。
        • No sympy 对于不受信任的输入是不安全的。试试sympy.sympify("""[].__class__.__base__.__subclasses__()[158]('ls')""") 这调用subprocess.Popen() 我通过ls 而不是rm -rf /。其他计算机上的索引可能会有所不同。这是Ned Batchelder exploit 的变体
        • 确实,它根本不会增加安全性。
        【解决方案9】:

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

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

        请参阅:PEP 498 -- Literal String Interpolation

        例如(注意 f 前缀):

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

        【讨论】:

        • 非常有趣的链接。但我想 f-strings 是为了让编写源代码更容易,而问题似乎是关于在变量内部处理字符串(可能来自不受信任的来源)。在这种情况下不能使用 f 字符串。
        • 有什么方法可以实现 f'{2{operator}4}' 的效果,您现在可以指定运算符执行 2+4 或 2*4 或 2-4 或等
        • 这实际上相当于只做str(eval(...)),所以它肯定不比eval安全。
        【解决方案10】:

        基于Perkins' amazing approach,我更新并改进了他的简单代数表达式(无函数或变量)的“快捷方式”。现在它适用于 Python 3.6+ 并避免了一些陷阱:

        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
        

        【讨论】:

        • 这太棒了。比我的方法干净得多。请注意,将 lambda: 10 作为输入提供会导致未处理的异常,因为您的正则表达式 finds 是一个返回语句,但它与常量返回不匹配。有一些解决方案(查找MAKE_FUNCTION 操作码,检测太长的LOAD_CONST)。另外值得注意的是,这仍然容易受到 DoS 攻击(需要外部缓解)。
        • 值得注意的是,这依赖于这样一个事实,即无论使用什么 Python 实现,它都必须实现常量折叠(诚然,流行的就是这样做的)。但是,并非所有常量表达式都以这种方式优化,例如 2**12345 保留原样以在运行时进行评估。在这种情况下,您的函数会遇到 IndexError: tuple index out of range。
        • 谢谢@a_guest!我已经更新了解决这个问题的方法!
        【解决方案11】:

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

        【讨论】:

        • eval("(1).__class__.__bases__[0].__subclasses__()[81]('echo got through'.split())",{'builtins':None}) #逃离你的沙箱
        • 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...
        • 这不安全。恶意代码仍然可以执行。
        • This is not safe - 好吧,我认为它与整体使用 bash 一样安全。顺便说一句:eval('math.sqrt(2.0)')
        【解决方案12】:

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

        【讨论】:

          【解决方案13】:

          使用云雀解析器库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
          
          

          【讨论】:

            【解决方案14】:

            我也是来这里寻找数学表达式解析器的。阅读一些答案并查找库,我遇到了我现在正在使用的py-expression。它基本上可以处理很多运算符和公式结构,但是如果您遗漏了一些东西,您可以轻松地向其中添加新的运算符/函数。

            基本语法是:

            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

            【讨论】:

              猜你喜欢
              • 2018-05-19
              • 2011-01-17
              相关资源
              最近更新 更多