Вычисление математического выражения в строке



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 можно обойти это, но нет ли лучшего и, что более важно, более безопасного метода для оценки математического выражения, которое хранится в строке?

604   13  

13 ответов:

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

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:

некоторые более безопасные альтернативы eval() и sympy.sympify().evalf()*:

*SymPy sympify также небезопасно в соответствии со следующим предупреждением из документации.

предупреждение: обратите внимание, что эта функция использует eval, и, следовательно, не должны использоваться непроверенные вход.

хорошо, поэтому проблема с eval заключается в том, что он может слишком легко избежать своей песочницы, даже если вы избавитесь от __builtins__. Все методы для выхода из песочницы сводятся к использованию getattr или object.__getattribute__ (через . оператор) для получения ссылки на некоторый опасный объект через некоторый разрешенный объект (''.__class__.__bases__[0].__subclasses__ или подобные). getattr устраняется путем установки __builtins__ to None. 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. Вы можете добавить ),, and EOF к списку вещей разрешенных следовать ., но зачем?

причина eval и exec настолько опасны, что по умолчанию compile функция будет генерировать байт-код для любого допустимого выражения python, а по умолчанию eval или exec выполнит любой допустимый байт-код python. Все ответы на сегодняшний день сосредоточены на ограничении байт-кода, который может быть сгенерирован (путем очистки ввода) или создания собственного доменного языка с использованием AST.

вместо этого вы можете легко создать простой это неспособный делать что-либо гнусное и может легко иметь проверки времени выполнения на памяти или времени, используемого. Конечно, если это простая математика, то есть ярлык.

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]

способ, которым это работает, прост, любое постоянное математическое выражение безопасно оценивается во время компиляции и хранится как константа. Объект кода, возвращаемый компиляцией, состоит из 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 возьмите что-нибудь произвольно сложное и сведите его к последовательности простых инструкций).

вы можете использовать модуль 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.

при желании, вы можете добавить тайм-аут декоратор на этой функции.

это массово поздний ответ, но я думаю, что полезно для дальнейшего использования. Вместо того, чтобы писать свой собственный математический парсер (хотя пример 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 * приносит в гораздо больше поддержки функции, как триггер функции, специальные функции, etc. но я избегал этого здесь, чтобы показать, что происходит откуда.

Я думаю, что я бы использовал eval(), но сначала проверьте, чтобы убедиться, что строка является допустимым математическим выражением, а не чем-то вредоносным. Вы можете использовать регулярное выражение для проверки.

eval() также принимает дополнительные аргументы, которые можно использовать для ограничения пространства имен, в котором он работает для большей безопасности.

[Я знаю, это старый вопрос, но стоит отметить новые полезные решения, как они выскакивают]

начиная с python3. 6, эта возможность теперь встроенный в язык, придуман "f-strings".

посмотреть: PEP 498 -- Литеральная строковая интерполяция

например (обратите внимание на f префикс):

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

Если вы не хотите использовать eval, то единственным решением является реализация соответствующего синтаксического анализатора грамматики. Взгляните на 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

вы можете дать доступ к математическому модулю:

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

Если вы уже используете wolframalpha, у них есть api python, который позволяет вам оценивать выражения. Может быть немного медленно, но по крайней мере очень точно.

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

Python уже имеет функцию для безопасного вычисления строк, содержащих литеральные выражения:

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

Comments

    Ничего не найдено.