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

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

86 голосов | спросил Pieter 3 MarpmWed, 03 Mar 2010 16:10:39 +03002010-03-03T16:10:39+03:0004 2010, 16:10:39

11 ответов


0

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
ответил unutbu 3 MarpmWed, 03 Mar 2010 16:52:30 +03002010-03-03T16:52:30+03:0004 2010, 16:52:30
0

eval это зло

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

Примечание: даже если вы используете для __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:
ответил jfs 4 MarpmSun, 04 Mar 2012 23:15:26 +04002012-03-04T23:15:26+04:0011 2012, 23:15:26
0

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

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

  

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

ответил Mark Mikofski 7 AM000000100000004031 2013, 10:28:40
0

Хорошо, проблема с eval в том, что он может слишком легко покинуть свою песочницу, даже если вы избавитесь от __builtins__. Все методы выхода из песочницы сводятся к использованию getattr или object.__getattribute__ (через оператор .) для получения ссылки на опасный объект через разрешенный объект (''.__class__.__bases__[0].__subclasses__ или аналогичный). getattr удаляется установкой для __builtins__ значения ---- +: = 7 =: + ----. None - сложный вопрос, поскольку его нельзя просто удалить, поскольку object.__getattribute__ является неизменным и потому что удаление его сломало бы все. Однако object доступен только через __getattribute__ оператор, так что очищения от вашего ввода достаточно, чтобы eval не смог покинуть свою песочницу.
При обработке формул единственное допустимое использование десятичного числа - это когда ему предшествует или следует код ., поэтому мы просто удаляем все другие экземпляры [0-9]

.

Обратите внимание, что в то время как python обычно обрабатывает import re inp = re.sub(r"\.(?![0-9])","", inp) val = eval(inp, {'__builtins__':None}) как 1 + 1., это удалит завершающий 1 + 1.0 и оставит вас с .. Вы можете добавить 1 + 1, ) и к списку вещей, которым разрешено следовать EOF, но зачем?

ответил Perkins 22 AM00000030000002131 2014, 03:59:21
0

Причина 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]

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

ответил Perkins 11 MaramFri, 11 Mar 2016 03:34:34 +03002016-03-11T03:34:34+03:0003 2016, 03:34:34
0

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

ответил andybuckley 4 MarpmSun, 04 Mar 2012 20:08:10 +04002012-03-04T20:08:10+04:0008 2012, 20:08:10
0

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

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

См. PEP 498 - интерполяция буквенных строк

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

f'{2**4}'
=> '16'
ответил shx2 16 FriEurope/Moscow2016-12-16T19:12:48+03:00Europe/Moscow12bEurope/MoscowFri, 16 Dec 2016 19:12:48 +0300 2016, 19:12:48
0

Если вы не хотите использовать eval, единственное решение - реализовать соответствующий синтаксический анализатор грамматики. Посмотрите на pyparsing .

ответил kgiannakakis 3 MarpmWed, 03 Mar 2010 16:19:45 +03002010-03-03T16:19:45+03:0004 2010, 16:19:45
0

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

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

ответил user1767754 10 52017vEurope/Moscow11bEurope/MoscowFri, 10 Nov 2017 21:43:18 +0300 2017, 21:43:18
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

Возможно, вы захотите предоставить доступ к математическому модулю:

>>> import math
>>> ns = vars(math).copy()
>>> ns['__builtins__'] = None
>>> eval('cos(pi/3)', ns)
0.50000000000000011
ответил krawyoti 3 MarpmWed, 03 Mar 2010 17:42:24 +03002010-03-03T17:42:24+03:0005 2010, 17:42:24
0

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

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

ответил lost 1 Maypm13 2013, 22:14:09

Похожие вопросы

Популярные теги

security × 330linux × 316macos × 2827 × 268performance × 244command-line × 241sql-server × 235joomla-3.x × 222java × 189c++ × 186windows × 180cisco × 168bash × 158c# × 142gmail × 139arduino-uno × 139javascript × 134ssh × 133seo × 132mysql × 132