Оценка математического выражения в строке
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
может обойти это, но нет ли лучшего и, что более важно, более безопасного метода для оценки математического выражения, которое хранится в строке?
Ответы
Ответ 1
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 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
Ответ 2
eval
is evil
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:
Ответ 3
Некоторые более безопасные альтернативы eval()
и sympy.sympify().evalf()
*:
* SymPy sympify
также небезопасно в соответствии со следующим предупреждением из документации.
Предупреждение: Обратите внимание, что эта функция использует eval
и поэтому не должна использоваться для неанитированного ввода.
Ответ 4
Хорошо, поэтому проблема с eval заключается в том, что он может легко покинуть свою песочницу, даже если вы избавитесь от __builtins__
. Все методы экранирования песочницы сводятся к использованию getattr
или object.__getattribute__
(через оператор .
), чтобы получить ссылку на какой-либо опасный объект через некоторый разрешенный объект (''.__class__.__bases__[0].__subclasses__
или аналогичный). getattr
устраняется установкой __builtins__
в 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
. Вы могли бы добавить )
,
и EOF
в список вещей, которым разрешено следовать .
, но зачем беспокоиться?
Ответ 5
Причина 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
взять что-либо сколь угодно сложным и сводит его к последовательности простых инструкций).
Ответ 6
Это массово поздний ответ, но я считаю полезным для будущей ссылки. Вместо того, чтобы писать собственный математический анализатор (хотя приведенный выше пример 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
Очень здорово! A from sympy import *
обеспечивает гораздо большую функциональную поддержку, такую как функции триггера, специальные функции и т.д., Но я избегал этого, чтобы показать, что происходит отсюда.
Ответ 7
Вы можете использовать модуль ast и написать NodeVisitor, который проверяет, что тип каждого node является частью белого списка.
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.
При желании вы можете добавить декоратор тайм-аута в эту функцию.
Ответ 8
[Я знаю, что это старый вопрос, но стоит отметить новые полезные решения по мере их появления]
Начиная с python3.6, эта возможность теперь встроена в язык, придумал "f-строки" .
Смотрите: PEP 498 - Интерполяция буквенных строк
Например (обратите внимание на префикс f
):
f'{2**4}'
=> '16'
Ответ 9
Я думаю, что я использовал бы eval()
, но сначала проверил бы, чтобы строка была действительным математическим выражением, а не чем-то злым. Вы можете использовать регулярное выражение для проверки.
eval()
также принимает дополнительные аргументы, которые можно использовать для ограничения пространства имен, в котором он работает, для большей безопасности.
Ответ 10
Если вы не хотите использовать eval, то единственным решением является реализация соответствующего анализатора грамматики. Посмотрите pyparsing.
Ответ 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
Ответ 12
Python уже имеет функцию для безопасной оценки строк, содержащих литералы:
http://docs.python.org/2/library/ast.html#ast.literal_eval
Ответ 13
Если вы уже используете wolframalpha, у них есть python api, который позволяет вам оценивать выражения. Может быть немного медленнее, но, по крайней мере, очень точно.
https://pypi.python.org/pypi/wolframalpha