[python] 문자열에서 수학 표현식 평가

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해결할 수 있다는 것을 알고 있지만 문자열에 저장되는 수학적 표현을 평가하는 더 좋고 안전한 방법이 있습니까?



답변

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__를 사용하더라도 Noneintrospection을 사용하여 중단 할 수 있습니다.

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의 문제는 .NET을 제거하더라도 너무 쉽게 샌드 박스를 벗어날 수 있다는 것입니다 __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})

참고 파이썬 일반적으로 치료가 동안이 1 + 1.아니라 1 + 1.0,이 후행를 제거 .하고 당신을 떠나 1 + 1. 당신은 추가 할 수 있습니다 ), 그리고 EOF수행 할 수 것들의 목록 ., 그런데 왜 귀찮게?


답변

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에서만 테스트되었습니다.

원하는 경우이 함수에 타임 아웃 데코레이터를 추가 할 수 있습니다.


답변

그 이유 evalexec그렇게 위험은 기본이다 compile기능은 유효한 파이썬 표현을위한 바이트 코드, 기본을 생성 eval하거나 exec유효한 파이썬 바이트 코드를 실행합니다. 현재까지의 모든 답변은 생성 할 수있는 바이트 코드를 제한하거나 (입력을 삭제하여) 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]

이것이 작동하는 방식은 간단합니다. 상수 수학 식은 컴파일 중에 안전하게 평가되고 상수로 저장됩니다. compile에 의해 반환되는 코드 객체 d는에 대한 바이트 코드 인 LOAD_CONST,로드 할 상수 (일반적으로 목록의 마지막 항목) 번호, S에 대한 바이트 코드 인으로 구성됩니다 RETURN_VALUE. 이 단축키가 작동하지 않으면 사용자 입력이 상수 표현식이 아님을 의미합니다 (변수 또는 함수 호출 등을 포함 함).

이것은 또한 좀 더 정교한 입력 형식에 대한 문을 열어줍니다. 예를 들면 :

stringExp = "1 + cos(2)"

이를 위해서는 실제로 바이트 코드를 평가해야하며 이는 여전히 매우 간단합니다. Python 바이트 코드는 스택 지향 언어이므로 모든 것이 단순 TOS=stack.pop(); op(TOS); stack.put(TOS)하거나 유사합니다. 핵심은 안전하고 (값로드 / 저장, 수학 연산, 값 반환) 안전하지 않은 opcode (속성 조회) 만 구현하는 것입니다. 사용자가 함수를 호출 할 수 있도록하려면 (위의 바로 가기를 사용하지 않는 이유) 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 개의 opcode가 있고 그 중 24 개는 수학과 관련이 있습니다). 추가 STORE_FAST및 몇 가지 다른 것은 'x=5;return x+x사소하게 쉽게 유사하거나 유사한 입력을 허용합니다 . 사용자가 만든 함수가 VMeval을 통해 자체적으로 실행되는 한 사용자가 만든 함수를 실행하는 데 사용할 수도 있습니다 (호출 가능하게 만들지 마세요 !!! 또는 어딘가에서 콜백으로 사용될 수 있음). 루프를 처리하려면 goto바이트 코드에 대한 지원이 필요합니다. 즉, for반복자 while에서 현재 명령어로의 포인터를 변경 하고 유지하는 것을 의미 하지만 너무 어렵지는 않습니다. DOS에 대한 저항을 위해 메인 루프는 계산 시작 이후 얼마나 많은 시간이 경과했는지 확인해야하며 특정 연산자는 합리적인 제한 (BINARY_POWER 가장 명백한 것).

이 접근 방식은 간단한 표현을위한 단순한 문법 파서보다 다소 길지만 (위의 컴파일 된 상수를 잡는 것에 대한 위 참조) 더 복잡한 입력으로 쉽게 확장되고 문법을 다룰 필요가 없습니다 ( compile임의로 복잡한 것을 취하고 일련의 간단한 지침).


답변

나는을 사용할 것이라고 생각 eval()하지만 먼저 문자열이 악의적 인 것이 아닌 유효한 수학적 표현인지 확인합니다. 유효성 검사에 정규식을 사용할 수 있습니다.

eval() 또한 보안 강화를 위해 작동하는 네임 스페이스를 제한하는 데 사용할 수있는 추가 인수를받습니다.