Вычисление математического выражения в строке
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 можно обойти это, но нет ли лучшего и, что более важно, более безопасного метода для оценки математического выражения, которое хранится в строке?
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})вычислить арифметическое выражение с помощью
astimport 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__toNone.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. Вы можете добавить),, andEOFк списку вещей разрешенных следовать., но зачем?
причина
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, который позволяет вам оценивать выражения. Может быть немного медленно, но по крайней мере очень точно.
Comments