Вычисление математического выражения в строке
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
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):
match node:
case ast.Constant(value) if isinstance(value, int):
return value # integer
case ast.BinOp(left, op, right):
return operators[type(op)](eval_(left), eval_(right))
case ast.UnaryOp(op, operand): # e.g., -1
return operators[type(op)](eval_(operand))
case _:
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:
Ответ 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
и exec
настолько опасны, заключается в том, что функция по умолчанию compile
будет генерировать байт-код для любого допустимого выражения python, а функция по умолчанию eval
or 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]
Способ, которым это работает, прост: любое постоянное математическое выражение безопасно вычисляется во время компиляции и сохраняется как константа. Объект code, возвращаемый 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
возьмите что-нибудь сколь угодно сложное и сведите это к последовательности простых инструкций).