การประเมินนิพจน์ทางคณิตศาสตร์ในสตริง


113
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สามารถแก้ไขปัญหานี้ได้ แต่ไม่มีวิธีที่ดีกว่าและที่สำคัญกว่า - ปลอดภัยกว่าในการประเมินนิพจน์ทางคณิตศาสตร์ที่ถูกเก็บไว้ในสตริงหรือไม่?


6
^ เป็นตัวดำเนินการ XOR ค่าที่คาดไว้คือ 6 คุณอาจต้องการพาว (2,4)
kgiannakakis

25
หรือมากกว่า pythonically 2 ** 4
fortran

1
หากคุณไม่ต้องการใช้ eval ทางออกเดียวคือใช้โปรแกรมแยกวิเคราะห์ไวยากรณ์ที่เหมาะสม มีลักษณะที่pyparsing
kgiannakakis

คำตอบ:


108

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

180

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):
    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:

29
โพสต์ที่ยอดเยี่ยมมากขอบคุณ ฉันได้ใช้แนวคิดนั้นและพยายามสร้างห้องสมุดที่ควรใช้งานง่าย: github.com/danthedeckie/simpleeval
Daniel Fairhead

สิ่งนี้สามารถขยายสำหรับฟังก์ชันของimport math?
Hotschke

2
สังเกตว่าast.parseไม่ปลอดภัย ตัวอย่างเช่นast.parse('()' * 1000000, '<string>', 'single')ล่ามล้มเหลว
Antti Haapala

1
@AnttiHaapala เป็นตัวอย่างที่ดี มันเป็นข้อผิดพลาดในล่าม Python หรือไม่? อย่างไรก็ตามอินพุตขนาดใหญ่จะได้รับการจัดการเล็กน้อยเช่นโดยใช้if len(expr) > 10000: raise ValueError.
jfs

1
@AnttiHaapala คุณสามารถให้ตัวอย่างที่ไม่สามารถแก้ไขโดยใช้len(expr)เช็คได้หรือไม่? หรือประเด็นของคุณคือมีข้อบกพร่องในการใช้งาน Python ดังนั้นจึงเป็นไปไม่ได้ที่จะเขียนรหัสปลอดภัยโดยทั่วไป?
jfs

14

ทางเลือกอื่นที่ปลอดภัยกว่าสำหรับeval()และ* :sympy.sympify().evalf()

* SymPy sympifyยังไม่ปลอดภัยตามคำเตือนต่อไปนี้จากเอกสารประกอบ

คำเตือน:โปรดทราบว่าฟังก์ชันนี้ใช้evalและไม่ควรใช้กับอินพุตที่ไม่ได้รับการฆ่าเชื้อ


10

เอาล่ะปัญหาของ eval คือมันสามารถหลบหนี sandbox ได้ง่ายเกินไปแม้ว่าคุณจะกำจัดทิ้ง__builtins__ก็ตาม วิธีการทั้งหมดในการหลบหนีแซนด์บ็อกซ์มาจากการใช้getattrหรือobject.__getattribute__(ผ่านตัว.ดำเนินการ) เพื่อขอรับการอ้างอิงถึงวัตถุอันตรายผ่านวัตถุที่อนุญาต ( ''.__class__.__bases__[0].__subclasses__หรือคล้ายกัน) getattrจะถูกกำจัดออกโดยการตั้งค่าไป __builtins__ เป็นสิ่งที่ยากเนื่องจากไม่สามารถลบออกได้ทั้งสองอย่างเนื่องจากไม่เปลี่ยนรูปและเนื่องจากการถอดออกจะทำให้ทุกอย่างพัง อย่างไรก็ตามสามารถเข้าถึงได้ผ่านทางโอเปอเรเตอร์เท่านั้นดังนั้นการล้างข้อมูลนั้นออกจากข้อมูลของคุณจึงเพียงพอที่จะทำให้แน่ใจว่า eval ไม่สามารถหลบหนีจากแซนด์บ็อกซ์ได้ ในการประมวลผลสูตรการใช้ทศนิยมที่ถูกต้องเท่านั้นคือเมื่อนำหน้าหรือตามด้วยNoneobject.__getattribute__object__getattribute__.
[0-9]ดังนั้นเราจึงลบอินสแตนซ์อื่น ๆ ทั้งหมดของ..

import re
inp = re.sub(r"\.(?![0-9])","", inp)
val = eval(inp, {'__builtins__':None})

ทราบว่าในขณะหลามปกติถือว่า1 + 1.เป็น1 + 1.0นี้จะลบต่อท้ายและปล่อยให้คุณกับ. 1 + 1คุณสามารถเพิ่ม), และEOFไปยังรายการของสิ่งที่ได้รับอนุญาตให้ทำตาม.แต่ทำไมรำคาญ?


คำถามที่เกี่ยวข้องกับการอภิปรายที่น่าสนใจสามารถพบได้ที่นี่
djvg

3
ไม่ว่าอาร์กิวเมนต์เกี่ยวกับการลบ.จะถูกต้องหรือไม่ในขณะนี้สิ่งนี้จะทิ้งโอกาสในการเกิดช่องโหว่ด้านความปลอดภัยหาก Python เวอร์ชันอนาคตแนะนำไวยากรณ์ใหม่ทำให้สามารถเข้าถึงวัตถุหรือฟังก์ชันที่ไม่ปลอดภัยได้ด้วยวิธีอื่น การแก้ปัญหานี้จะไม่ปลอดภัยแล้วในหลาม 3.6 f"{eval('()' + chr(46) + '__class__')}"เพราะฉสตริงซึ่งจะช่วยให้การโจมตีต่อไปนี้: วิธีแก้ปัญหาที่ขึ้นอยู่กับการอนุญาตพิเศษมากกว่าการขึ้นบัญชีดำจะปลอดภัยกว่า แต่จริงๆแล้วจะดีกว่าถ้าไม่แก้ปัญหานี้evalเลย
kaya3

นั่นเป็นจุดที่ยอดเยี่ยมเกี่ยวกับคุณลักษณะภาษาในอนาคตที่มีปัญหาด้านความปลอดภัยใหม่ ๆ
Perkins

8

คุณสามารถใช้โมดูล 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)

เนื่องจากการทำงานผ่านรายการที่อนุญาตมากกว่าบัญชีดำจึงปลอดภัย ฟังก์ชันและตัวแปรเดียวที่สามารถเข้าถึงได้คือฟังก์ชันที่คุณให้สิทธิ์เข้าถึงอย่างชัดเจน ฉันเติมคำสั่งด้วยฟังก์ชันที่เกี่ยวข้องกับคณิตศาสตร์เพื่อให้คุณสามารถเข้าถึงสิ่งเหล่านั้นได้อย่างง่ายดายหากต้องการ แต่คุณต้องใช้มันอย่างชัดเจน

หากสตริงพยายามเรียกใช้ฟังก์ชันที่ไม่ได้จัดเตรียมไว้หรือเรียกใช้เมธอดใด ๆ จะมีการเพิ่มข้อยกเว้นและจะไม่ดำเนินการ

เนื่องจากสิ่งนี้ใช้ตัวแยกวิเคราะห์และตัวประเมินในตัวของ Python จึงสืบทอดลำดับความสำคัญและกฎการส่งเสริมการขายของ Python ด้วยเช่นกัน

>>> evaluate("7 + 9 * (2 << 2)")
79
>>> evaluate("6 // 2 + 0.0")
3.0

โค้ดด้านบนได้รับการทดสอบบน Python 3 เท่านั้น

หากต้องการคุณสามารถเพิ่มมัณฑนากรแบบหมดเวลาในฟังก์ชันนี้ได้


7

เหตุผลevalและexecอันตรายมากคือcompileฟังก์ชันเริ่มต้นจะสร้าง bytecode สำหรับนิพจน์ python ที่ถูกต้องและค่าเริ่มต้นevalหรือexecจะเรียกใช้งาน bytecode ของ python ที่ถูกต้อง คำตอบทั้งหมดในปัจจุบันมุ่งเน้นไปที่การ จำกัด bytecode ที่สามารถสร้างได้ (โดยการล้างข้อมูลที่ป้อน) หรือสร้างภาษาเฉพาะโดเมนของคุณเองโดยใช้ 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ซึ่งเป็น bytecode สำหรับLOAD_CONSTตามจำนวนคงที่จะโหลด (โดยปกติจะเป็นคนสุดท้ายในรายการ) ตามด้วยSซึ่งเป็น bytecode RETURN_VALUEสำหรับ หากทางลัดนี้ใช้ไม่ได้แสดงว่าอินพุตของผู้ใช้ไม่ใช่นิพจน์คงที่ (มีการเรียกใช้ตัวแปรหรือฟังก์ชันหรือคล้ายกัน)

นอกจากนี้ยังเปิดประตูสู่รูปแบบการป้อนข้อมูลที่ซับซ้อนมากขึ้น ตัวอย่างเช่น:

stringExp = "1 + cos(2)"

สิ่งนี้ต้องการการประเมิน bytecode จริงซึ่งยังค่อนข้างง่าย Python bytecode เป็นภาษาที่เน้นสแต็กดังนั้นทุกอย่างจึงเป็นเรื่องธรรมดาTOS=stack.pop(); op(TOS); stack.put(TOS)หรือคล้ายกัน กุญแจสำคัญคือใช้เฉพาะ opcodes ที่ปลอดภัย (การโหลด / การจัดเก็บค่าการดำเนินการทางคณิตศาสตร์ค่าที่ส่งคืน) และไม่ใช่สิ่งที่ไม่ปลอดภัย (การค้นหาแอตทริบิวต์) หากคุณต้องการให้ผู้ใช้สามารถเรียกใช้ฟังก์ชันได้ (เหตุผลทั้งหมดที่จะไม่ใช้ทางลัดด้านบน) ให้ดำเนินการอย่างง่าย ๆ โดย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 opcodes ซึ่ง 24 รายการเกี่ยวข้องกับคณิตศาสตร์) การเพิ่มSTORE_FASTและอีกสองสามอย่างจะช่วยให้สามารถป้อนข้อมูลที่เหมือน'x=5;return x+xหรือคล้ายกันได้อย่างง่ายดาย มันสามารถใช้เพื่อเรียกใช้ฟังก์ชันที่ผู้ใช้สร้างขึ้นได้ตราบใดที่ฟังก์ชันที่ผู้ใช้สร้างขึ้นนั้นถูกเรียกใช้ด้วยตัวเองผ่าน VMeval (อย่าทำให้สามารถเรียกได้ !!! หรืออาจใช้เป็นการโทรกลับที่ใดก็ได้) การจัดการลูปต้องการการสนับสนุนสำหรับgotobytecodes ซึ่งหมายถึงการเปลี่ยนจากตัวforวนซ้ำไปเป็นwhileและรักษาตัวชี้เป็นคำสั่งปัจจุบัน แต่ก็ไม่ยากเกินไป เพื่อความต้านทานต่อ DOS ลูปหลักควรตรวจสอบระยะเวลาที่ผ่านไปนับตั้งแต่เริ่มการคำนวณและตัวดำเนินการบางรายควรปฏิเสธการป้อนข้อมูลเกินขีด จำกัด ที่สมเหตุสมผล (BINARY_POWER ชัดเจนที่สุด)

แม้ว่าวิธีนี้จะค่อนข้างยาวกว่าตัวแยกวิเคราะห์ไวยากรณ์แบบง่ายสำหรับนิพจน์ทั่วไป (ดูด้านบนเกี่ยวกับการจับค่าคงที่ที่คอมไพล์แล้ว) แต่ก็สามารถขยายไปยังอินพุตที่ซับซ้อนมากขึ้นได้อย่างง่ายดายและไม่จำเป็นต้องจัดการกับไวยากรณ์ ( compileใช้อะไรที่ซับซ้อนโดยพลการและลดขนาดเป็น ลำดับคำแนะนำง่ายๆ)


6

ฉันคิดว่าจะใช้eval()แต่ก่อนอื่นให้ตรวจสอบให้แน่ใจว่าสตริงเป็นนิพจน์ทางคณิตศาสตร์ที่ถูกต้องซึ่งตรงข้ามกับสิ่งที่เป็นอันตราย คุณสามารถใช้นิพจน์ทั่วไปสำหรับการตรวจสอบความถูกต้อง

eval() นอกจากนี้ยังใช้อาร์กิวเมนต์เพิ่มเติมซึ่งคุณสามารถใช้เพื่อ จำกัด เนมสเปซที่ดำเนินการเพื่อความปลอดภัยยิ่งขึ้น


3
แต่อย่าใช้นิพจน์ทั่วไปเพื่อตรวจสอบนิพจน์ทางคณิตศาสตร์โดยพลการ
High Performance Mark

@ High-Performance Mark: ใช่ฉันเดาว่ามันขึ้นอยู่กับว่าเขามีนิพจน์ทางคณิตศาสตร์ประเภทใดอยู่ในใจ . . เช่นเพียงคณิตศาสตร์ที่เรียบง่ายกับตัวเลขและ+, -, *, /, **, (, )หรือสิ่งที่มีความซับซ้อนมากขึ้น
ทิมสามี

@ ทิม - มันคือ () ฉันกังวลเกี่ยวกับหรือมากกว่า ((((((())))))) ความจริงฉันคิดว่า OP น่าจะกังวลเกี่ยวกับพวกเขาคิ้วของฉันถูกคลายจากปัญหาของ OP
High Performance Mark

2
อย่าใช้eval()ถ้าคุณไม่ได้ควบคุมอินพุตแม้ว่าคุณจะ จำกัด เนมสเปซเช่นeval("9**9**9**9**9**9**9**9", {'__builtins__': None})ใช้ CPU หน่วยความจำ
jfs

3
จำกัด namespace ของ EVAL ไม่ได้เพิ่มการรักษาความปลอดภัย
Antti Haapala

5

นี่เป็นคำตอบที่ล่าช้าอย่างมาก แต่ฉันคิดว่ามีประโยชน์สำหรับการอ้างอิงในอนาคต แทนที่จะเขียนตัวแยกวิเคราะห์ทางคณิตศาสตร์ของคุณเอง (แม้ว่าตัวอย่างการแยกวิเคราะห์ด้านบนจะดีมาก) คุณสามารถใช้ 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 *นำมาซึ่งการสนับสนุนฟังก์ชันอื่น ๆ อีกมากมายเช่นฟังก์ชันตรีโกณมิติฟังก์ชันพิเศษ ฯลฯ แต่ฉันได้หลีกเลี่ยงที่นี่เพื่อแสดงว่าอะไรมาจากไหน


3
ความเห็นอกเห็นใจ "ปลอดภัย" หรือไม่? ดูเหมือนจะมีโพสต์มากมาย ที่แนะนำว่าเป็น wrapper รอบ eval () ที่สามารถใช้ประโยชน์ได้ในลักษณะเดียวกัน ยังไม่ใช้ numpy ndarrays evalf
Mark Mikofski

14
ไม่มีความเห็นอกเห็นใจไม่ปลอดภัยสำหรับข้อมูลที่ไม่น่าเชื่อถือ ลองsympy.sympify("""[].__class__.__base__.__subclasses__()[158]('ls')""")โทรนี้subprocess.Popen()ซึ่งผมผ่านไปแทนls rm -rf /ดัชนีอาจแตกต่างกันในคอมพิวเตอร์เครื่องอื่น นี่คือรูปแบบหนึ่งของการใช้ประโยชน์จาก Ned Batchelder
Mark Mikofski

1
อันที่จริงมันไม่ได้เพิ่มความปลอดภัยเลย
Antti Haapala

4

[ฉันรู้ว่านี่เป็นคำถามเก่า แต่ก็ควรชี้ให้เห็นวิธีแก้ปัญหาใหม่ ๆ ที่มีประโยชน์เมื่อพวกเขาปรากฏขึ้น]

ตั้งแต่ python3.6 ความสามารถนี้ถูกสร้างขึ้นในภาษาแล้ว "F-สตริง"

ดู: PEP 498 - การแก้ไขสตริงตามตัวอักษร

ตัวอย่างเช่น (สังเกตfคำนำหน้า):

f'{2**4}'
=> '16'

7
ลิงค์ที่น่าสนใจมาก แต่ฉันเดาว่า f-strings อยู่ที่นี่เพื่อให้การเขียนซอร์สโค้ดง่ายขึ้นในขณะที่คำถามดูเหมือนจะเกี่ยวกับการทำงานกับสตริงภายในตัวแปร (อาจมาจากแหล่งที่มาที่ไม่น่าเชื่อถือ) ไม่สามารถใช้ f-strings ในกรณีนั้น
Bernhard

มีวิธีใดบ้างที่จะทำบางสิ่งบางอย่างเพื่อให้เอฟเฟกต์ของ f '{2 {operator} 4}' ซึ่งตอนนี้คุณสามารถกำหนดให้โอเปอเรเตอร์ทำ 2 + 4 หรือ 2 * 4 หรือ 2-4 หรืออื่น ๆ
Skyler

นี้เป็นจริงเทียบเท่ากับเพียงแค่การทำจึงเป็นสิ่งที่ไม่แน่นอนปลอดภัยกว่าstr(eval(...)) eval
kaya3

ดูเหมือนว่าจะเหมือนกันกับ exec / eval ...
Victor VosMottor ขอบคุณ Monica

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

35
eval ("(1) .__ class __.__ bases __ [0] .__ subclasses __ () [81] ('echo got through'.split ())", {' builtins ': None}) #escapes your sandbox
Perkins

6
Python 3.4: eval("""[i for i in (1).__class__.__bases__[0].__subclasses__() if i.__name__.endswith('BuiltinImporter')][0]().load_module('sys').modules['sys'].modules['os'].system('/bin/sh')""", {'__builtins__': None})เรียกใช้ bourne shell ...
Antti Haapala

8
นี้ไม่ปลอดภัย ยังคงสามารถเรียกใช้โค้ดที่เป็นอันตรายได้
Fermi paradox

This is not safe- ฉันคิดว่ามันปลอดภัยพอ ๆ กับการใช้ bash โดยรวม BTW: eval('math.sqrt(2.0)')<- "คณิตศาสตร์" จำเป็นต้องระบุตามที่เขียนไว้ด้านบน
Hannu

0

นี่คือวิธีแก้ปัญหาของฉันโดยไม่ต้องใช้ eval ทำงานร่วมกับ Python2 และ Python3 ใช้ไม่ได้กับจำนวนลบ

$ python -m pytest test.py

test.py

from solution import Solutions

class SolutionsTestCase(unittest.TestCase):
    def setUp(self):
        self.solutions = Solutions()

    def test_evaluate(self):
        expressions = [
            '2+3=5',
            '6+4/2*2=10',
            '3+2.45/8=3.30625',
            '3**3*3/3+3=30',
            '2^4=6'
        ]
        results = [x.split('=')[1] for x in expressions]
        for e in range(len(expressions)):
            if '.' in results[e]:
                results[e] = float(results[e])
            else:
                results[e] = int(results[e])
            self.assertEqual(
                results[e],
                self.solutions.evaluate(expressions[e])
            )

solution.py

class Solutions(object):
    def evaluate(self, exp):
        def format(res):
            if '.' in res:
                try:
                    res = float(res)
                except ValueError:
                    pass
            else:
                try:
                    res = int(res)
                except ValueError:
                    pass
            return res
        def splitter(item, op):
            mul = item.split(op)
            if len(mul) == 2:
                for x in ['^', '*', '/', '+', '-']:
                    if x in mul[0]:
                        mul = [mul[0].split(x)[1], mul[1]]
                    if x in mul[1]:
                        mul = [mul[0], mul[1].split(x)[0]]
            elif len(mul) > 2:
                pass
            else:
                pass
            for x in range(len(mul)):
                mul[x] = format(mul[x])
            return mul
        exp = exp.replace(' ', '')
        if '=' in exp:
            res = exp.split('=')[1]
            res = format(res)
            exp = exp.replace('=%s' % res, '')
        while '^' in exp:
            if '^' in exp:
                itm = splitter(exp, '^')
                res = itm[0] ^ itm[1]
                exp = exp.replace('%s^%s' % (str(itm[0]), str(itm[1])), str(res))
        while '**' in exp:
            if '**' in exp:
                itm = splitter(exp, '**')
                res = itm[0] ** itm[1]
                exp = exp.replace('%s**%s' % (str(itm[0]), str(itm[1])), str(res))
        while '/' in exp:
            if '/' in exp:
                itm = splitter(exp, '/')
                res = itm[0] / itm[1]
                exp = exp.replace('%s/%s' % (str(itm[0]), str(itm[1])), str(res))
        while '*' in exp:
            if '*' in exp:
                itm = splitter(exp, '*')
                res = itm[0] * itm[1]
                exp = exp.replace('%s*%s' % (str(itm[0]), str(itm[1])), str(res))
        while '+' in exp:
            if '+' in exp:
                itm = splitter(exp, '+')
                res = itm[0] + itm[1]
                exp = exp.replace('%s+%s' % (str(itm[0]), str(itm[1])), str(res))
        while '-' in exp:
            if '-' in exp:
                itm = splitter(exp, '-')
                res = itm[0] - itm[1]
                exp = exp.replace('%s-%s' % (str(itm[0]), str(itm[1])), str(res))

        return format(exp)
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.