利用Python+eval函數(shù)構建數(shù)學表達式計算器
Python 中的函數(shù)eval()?是一個非常有用的工具,在前期,我們一起學習過該函數(shù)點擊查看:Python eval 函數(shù)動態(tài)地計算數(shù)學表達式?。盡管如此,我們在使用之前,還需要考慮到該函數(shù)的一些重要的安全問題。在本文中,云朵君將和大家一起學習 eval() 如何工作,以及如何在 Python 程序中安全有效地使用它。
eval() 的安全問題
本節(jié)主要學習 eval() 如何使我們的代碼不安全,以及如何規(guī)避相關的安全風險。
eval() 函數(shù)的安全問題在于它允許你(或你的用戶)動態(tài)地執(zhí)行任意的Python代碼。
通常情況下,會存在正在讀(或?qū)懀┑拇a不是我們要執(zhí)行的代碼的情況。如果我們需要使用eval()來計算來自用戶或任何其他外部來源的輸入,此時將無法確定哪些代碼將被執(zhí)行,這將是一個非常嚴重的安全漏洞,極易收到黑客的攻擊。
一般情況下,我們并不建議使用 eval()。但如果非要使用該函數(shù),需要記住根據(jù)經(jīng)驗法則:永遠不要 用 未經(jīng)信任的輸入 來使用該函數(shù)。這條規(guī)則的重點在于要弄清楚我們可以信任哪些類型的輸入。
舉個例子說明,隨意使用eval()?會使我們寫的代碼漏洞百出。假設你想建立一個在線服務來計算任意的Python數(shù)學表達式:用戶自定義表達式,然后點擊運行?按鈕。應用程序app獲得用戶的輸入并將其傳遞給eval()進行計算。
這個應用程序app將在我們的個人服務器上運行,而那些服務器內(nèi)具有重要文件,如果你在一個Linux 操作系統(tǒng)運行命令,并且該進程有合法權限,那么惡意的用戶可以輸入危險的字符串而損害服務器,比如下面這個命令。
"__import__('subprocess').getoutput('rm –rf *')"
上述代碼將刪除程序當前目錄中的所有文件。這簡直太可怕了!
注意: __import__()?是一個內(nèi)置函數(shù),它接收一個字符串形式的模塊名稱,并返回一個模塊對象的引用。__import__()? 是一個函數(shù),它與導入語句完全不同。我們不能使用 eval() 來計算一個導入語句。
當輸入不受信任時,并沒有完全有效的方法來避免eval()?函數(shù)帶來的安全風險。其實我們可以通過限制eval()的執(zhí)行環(huán)境來減少風險。在下面的內(nèi)容中,我們學習一些規(guī)避風險的技巧。
限制globals和locals
可以通過向 globals 和 locals 參數(shù)傳遞自定義字典來限制 eval()? 的執(zhí)行環(huán)境。例如,可以給這兩個參數(shù)傳遞空的字典,以防止eval()訪問調(diào)用者當前范圍或命名空間中的變量名。
# 避免訪問調(diào)用者當前范圍內(nèi)的名字 >>> x = 100 >>> eval("x * 5", {}, {}) Traceback (most recent call last): File "<stdin>", line 1, in <module> File "<string>", line 1, in <module> NameError: name 'x' is not defined
如果給 globals 和 locals 傳遞了空的字典({}?),那么eval()?在計算字符串 "x * 5 "? 時,在它的全局名字空間和局部名字空間都找不到名字x?。因此,eval()將拋出一個NameError。
然而,像這樣限制 globals 和 locals 參數(shù)并不能消除與使用 Python 的 eval() 有關的所有安全風險,因為仍然可以訪問所有 Python 的內(nèi)置變量名。
限制內(nèi)置名稱的使用
函數(shù) eval()? 會在解析 expression 之前自動將 builtins? 內(nèi)置模塊字典的引用插入到 globals 中。使用內(nèi)置函數(shù) __import__() 來訪問標準庫和在系統(tǒng)上安裝的任何第三方模塊。這還容易被惡意用戶利用。
下面的例子表明,即使在限制了 globals 和 locals 之后,我們也可以使用任何內(nèi)置函數(shù)和任何標準模塊,如 math 或 subprocess。
>>> eval("sum([5, 5, 5])", {}, {}) 15 >>> eval("__import__('math').sqrt(25)", {}, {}) 5.0 >>> eval("__import__('subprocess').getoutput('echo Hello, World')", {}, {}) 'Hello, World'
我們可以使用 __import__() 來導入任何標準或第三方模塊,如導入 math 和 subprocess 。因此 可以訪問在 math、subprocess 或任何其他模塊中定義的任何函數(shù)或類?,F(xiàn)在想象一下,一個惡意的用戶可以使用 subprocess 或標準庫中任何其他強大的模塊對系統(tǒng)做什么,那就有點恐怖了。
為了減少這種風險,可以通過覆蓋 globals 中的 "__builtins__?" 鍵來限制對 Python 內(nèi)置函數(shù)的訪問。通常建議使用一個包含鍵值對 "__builtins__:{}" 的自定義字典。
>>> eval("__import__('math').sqrt(25)", {"__builtins__": {}}, {}) Traceback (most recent call last): File "<stdin>", line 1, in <module> File "<string>", line 1, in <module> NameError: name '__import__' is not defined
如果我們將一個包含鍵值對 "__builtins__: {}?" 的字典傳遞給 globals,那么 eval()? 就不能直接訪問 Python 的內(nèi)置函數(shù),比如 __import__()。
然而這種方法仍然無法使得 eval() 完全規(guī)避風險。
限制輸入中的名稱
即使可以使用自定義的 globals? 和 locals? 字典來限制 eval()?的執(zhí)行環(huán)境,這個函數(shù)仍然會被攻擊。例如可以使用像""、"[]"、"{}"或"() "?來訪問類object以及一些特殊屬性。
>>> "".__class__.__base__ <class 'object'> >>> [].__class__.__base__ <class 'object'> >>> {}.__class__.__base__ <class 'object'> >>> ().__class__.__base__ <class 'object'>
一旦訪問了 object,可以使用特殊的方法 `.__subclasses__()`來訪問所有繼承于 object 的類。下面是它的工作原理。
>>> for sub_class in ().__class__.__base__.__subclasses__(): ... print(sub_class.__name__) ... type weakref weakcallableproxy weakproxy int ...
這段代碼將打印出一個大類列表。其中一些類的功能非常強大,因此也是一個重要的安全漏洞,而且我們無法通過簡單地限制 eval() 的避免該漏洞。
>>> input_string = """[ ... c for c in ().__class__.__base__.__subclasses__() ... if c.__name__ == "range" ... ][0](10 "0")""" >>> list(eval(input_string, {"__builtins__": {}}, {})) [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
上面代碼中的列表推導式對繼承自 object? 的類進行過濾,返回一個包含 range? 類的 list?。第一個索引([0]?)返回類的范圍。一旦獲得了對 range? 的訪問權,就調(diào)用它來生成一個 range? 對象。然后在 range? 對象上調(diào)用 list(),從而生成一個包含十個整數(shù)的列表。
在這個例子中,用 range? 來說明 eval()? 函數(shù)中的一個安全漏洞?,F(xiàn)在想象一下,如果你的系統(tǒng)暴露了像 subprocess.Popen 這樣的類,一個惡意的用戶可以做什么?
我們或許可以通過限制輸入中的名字的使用,從而解決這個漏洞。該技術涉及以下步驟。
- 創(chuàng)建一個包含你想用eval()使用的名字的字典。
- 在eval? 模式下使用compile() 將輸入字符串編譯為字節(jié)碼。
- 檢查字節(jié)碼對象上的.co_names,以確保它只包含允許的名字。
- 如果用戶試圖輸入一個不允許的名字,會引發(fā)一個`NameError`。
看看下面這個函數(shù),我們在其中實現(xiàn)了所有這些步驟。
>>> def eval_expression(input_string): ... # Step 1 ... allowed_names = {"sum": sum} ... # Step 2 ... code = compile(input_string, "<string>", "eval") ... # Step 3 ... for name in code.co_names: ... if name not in allowed_names: ... # Step 4 ... raise NameError(f"Use of {name} not allowed") ... return eval(code, {"__builtins__": {}}, allowed_names)
eval_expression()? 函數(shù)可以在 eval()? 中使用的名字限制為字典 allowed_names? 中的那些名字。而該函數(shù)使用了 .co_names,它是代碼對象的一個屬性,返回一個包含代碼對象中的名字的元組。
下面的例子顯示了eval_expression() 在實踐中是如何工作的。
>>> eval_expression("3 + 4 * 5 + 25 / 2") 35.5 >>> eval_expression("sum([1, 2, 3])") 6 >>> eval_expression("len([1, 2, 3])") Traceback (most recent call last): File "<stdin>", line 1, in <module> File "<stdin>", line 10, in eval_expression NameError: Use of len not allowed >>> eval_expression("pow(10, 2)") Traceback (most recent call last): File "<stdin>", line 1, in <module> File "<stdin>", line 10, in eval_expression NameError: Use of pow not allowed
如果調(diào)用 eval_expression()? 來計算算術運算,或者使用包含允許的變量名的表達式,那么將會正常運行并得到預期的結(jié)果,否則會拋出一個`NameError`。上面的例子中,我們僅允許輸入的唯一名字是sum()?,而不允許其他算術運算名稱如len()和pow(),所以當使用它們時,該函數(shù)會產(chǎn)生一個`NameError`。
如果完全不允許使用名字,那么可以把 eval_expression() 改寫:
>>> def eval_expression(input_string): ... code = compile(input_string, "<string>", "eval") ... if code.co_names: ... raise NameError(f"Use of names not allowed") ... return eval(code, {"__builtins__": {}}, {}) ... >>> eval_expression("3 + 4 * 5 + 25 / 2") 35.5 >>> eval_expression("sum([1, 2, 3])") Traceback (most recent call last): File "<stdin>", line 1, in <module> File "<stdin>", line 4, in eval_expression NameError: Use of names not allowed
現(xiàn)在函數(shù)不允許在輸入字符串中出現(xiàn)任何變量名。需要檢查.co_names?中的變量名,一旦發(fā)現(xiàn)就引發(fā) NameError。否則計算 input_string? 并返回計算的結(jié)果。此時也使用一個空的字典來限制locals。
我們可以使用這種技術來盡量減少eval()的安全問題,并加強安全盔甲,防止惡意攻擊。
將輸入限制為只有字數(shù)
函數(shù)eval()的一個常見用例是計算包含標準Python字面符號的字符串,并將其變成具體的對象。
標準庫提供了一個叫做 literal_eval() 的函數(shù),可以幫助實現(xiàn)這個目標。雖然這個函數(shù)不支持運算符,但它支持 list, tuples, numbers, strings等等。
>>> from ast import literal_eval >>> # 計算字面意義 >>> literal_eval("15.02") 15.02 >>> literal_eval("[1, 15]") [1, 15] >>> literal_eval("(1, 15)") (1, 15) >>> literal_eval("{'one': 1, 'two': 2}") {'one': 1, 'two': 2} >>> # 試圖計算一個表達式 >>> literal_eval("sum([1, 15]) + 5 + 8 * 2") Traceback (most recent call last): ... ValueError: malformed node or string: <_ast.BinOp object at 0x7faedecd7668>
注意,literal_eval()?只作用于標準類型的字詞。它不支持使用運算符或變量名。如果向 literal_eval()? 傳遞一個表達式,會得到一個 ValueError。這個函數(shù)還可以將與使用eval()有關的安全風險降到最低。
使用eval()與input()函數(shù)
在 Python 3.x 中,內(nèi)置函數(shù) input() 讀取命令行上的用戶輸入,去掉尾部的換行,轉(zhuǎn)換為字符串,并將結(jié)果返回給調(diào)用者。由于 input()? 的輸出結(jié)果是一個字符串,可以把它傳遞給 eval() 并作為一個 Python 表達式來計算它。
>>> eval(input("Enter a math expression: ")) Enter a math expression: 15 * 2 30 >>> eval(input("Enter a math expression: ")) Enter a math expression: 5 + 8 13
我們可以將函數(shù) eval()? 包裹在函數(shù) input()? 中,實現(xiàn)自動計算用戶的輸入的功能。一個常見用例模擬 Python 2.x 中 input()? 的行為,input() 將用戶的輸入作為一個 Python 表達式來計算,并返回結(jié)果。
因為它涉及安全問題,因此在 Python 2.x 中的 input() 的這種行為在 Python 3.x 中被改變了。
構建一個數(shù)學表達式計算器
到目前為止,我們已經(jīng)了解了函數(shù) eval()? 是如何工作的以及如何在實踐中使用它。此外還了解到 eval()? 具有重要的安全漏洞,盡量在代碼中避免使用 eval()?,然而在某些情況下,eval()? 可以為我們節(jié)省大量的時間和精力。因此,學會合理使用 eval() 函數(shù)還是蠻重要的。
在本節(jié)中,將編寫一個應用程序來動態(tài)地計算數(shù)學表達式。首先不使用eval()來解決這個問題,那么需要通過以下步驟:
- 解析輸入的表達式。
- 將表達式的組成部分變?yōu)镻ython對象(數(shù)字、運算符、函數(shù)等等)。
- 將所有的東西合并成一個表達式。
- 確認該表達式在Python中是有效的。
- 計算最終表達式并返回結(jié)果。
考慮到 Python 可以處理和計算的各種表達式非常耗時。其實我們可以使用 eval() 來解決這個問題,而且通過上文我們已經(jīng)學會了幾種技術來規(guī)避相關的安全風險。
首先創(chuàng)建一個新的Python腳本,名為mathrepl.py,然后添加以下代碼。
import math __version__ = "1.0" ALLOWED_NAMES = { k: v for k, v in math.__dict__.items() if not k.startswith("__") } PS1 = "mr>>" WELCOME = f""" MathREPL {__version__}, your Python math expressions evaluator! Enter a valid math expression after the prompt "{PS1}". Type "help" for more information. Type "quit" or "exit" to exit. """ USAGE = f""" Usage: Build math expressions using numeric values and operators. Use any of the following functions and constants: {', '.join(ALLOWED_NAMES.keys())} """
在這段代碼中,我們首先導入 math 模塊。這個模塊使用預定義的函數(shù)和常數(shù)進行數(shù)學運算。常量 ALLOWED_NAMES? 保存了一個包含數(shù)學中非特變量名的字典。這樣就可以用 eval() 來使用它們。
我們還定義了另外三個字符串常量。將使用它們作為腳本的用戶界面,并根據(jù)需要打印到屏幕上。
現(xiàn)在準備編寫核心功能,首先編寫一個函數(shù),接收數(shù)學表達式作為輸入,并返回其結(jié)果。此外還需要寫一個叫做 evaluate() 的函數(shù),如下所示。
def evaluate(expression): """Evaluate a math expression.""" # 編譯表達式 code = compile(expression, "<string>", "eval") # 驗證允許名稱 for name in code.co_names: if name not in ALLOWED_NAMES: raise NameError(f"The use of '{name}' is not allowed") return eval(code, {"__builtins__": {}}, ALLOWED_NAMES)
以下是該功能的工作原理。
- 定義了evaluate(),該函數(shù)將字符串表達式作為參數(shù),并返回一個浮點數(shù),代表將字符串作為數(shù)學表達式進行計算的結(jié)果。
- 使用compile()將輸入的字符串表達式變成編譯的Python代碼。如果用戶輸入了一個無效的表達式,編譯操作將引發(fā)一個 SyntaxError。
- 使用一個for循環(huán),檢查表達式中包含的名字,并確認它們可以在最終表達式中使用。如果用戶提供的名字不在允許的名字列表中,那么會引發(fā)一個NameError。
- 執(zhí)行數(shù)學表達式的實際計算。注意將自定義的字典傳遞給了globals和locals。ALLOWED_NAMES保存了數(shù)學中定義的函數(shù)和常量。
注意: 由于這個應用程序使用了 math 中定義的函數(shù),需要注意,當我們用一個無效的輸入值調(diào)用這些函數(shù)時,其中一些函數(shù)將拋出 ValueError 異常。
例如,math.sqrt(-10)? 會引發(fā)一個異常,因為-10的平方根是未定義的。我們會在稍后的代碼中看到如何捕捉該異常。
為 globals 和 locals 參數(shù)使用自定義值,加上名稱檢查,可以將與使用eval()有關的安全風險降到最低。
當在 main() 中編寫其代碼時,數(shù)學表達式計算器就完成了。在這個函數(shù)中,定義程序的主循環(huán),結(jié)束讀取和計算用戶在命令行中輸入的表達式的循環(huán)。
在這個例子中,應用程序?qū)ⅲ?/p>
- 向用戶打印一條歡迎信息
- 顯示一個提示,準備讀取用戶的輸入
- 提供獲取使用說明和終止應用程序的選項
- 讀取用戶的數(shù)學表達式
- 計算用戶的數(shù)學表達式
- 將計算的結(jié)果打印到屏幕上
def main(): """Main loop: Read and evaluate user's input.""" print(WELCOME) while True: # 讀取用戶的輸入 try: expression = input(f"{PS1} ") except (KeyboardInterrupt, EOFError): raise SystemExit() # 處理特殊命令 if expression.lower() == "help": print(USAGE) continue if expression.lower() in {"quit", "exit"}: raise SystemExit() # 對表達式進行計算并處理錯誤 try: result = evaluate(expression) except SyntaxError: # 如果用戶輸入了一個無效的表達式 print("Invalid input expression syntax") continue except (NameError, ValueError) as err: # 如果用戶試圖使用一個不允許的名字 # 對于一個給定的數(shù)學函數(shù)來說是一個無效的值 print(err) continue # 如果沒有發(fā)生錯誤,則打印結(jié)果 print(f"The result is: {result}") if __name__ == "__main__": main()
在main()?中,首先打印WELCOME消息。然后在一個try語句中讀取用戶的輸入,以捕獲鍵盤中斷和 EOFError。如果這些異常發(fā)生,就終止應用程序。
如果用戶輸入幫助選項,那么應用程序就會顯示使用指南。同樣地,如果用戶輸入quit或exit,那么應用程序就會終止。
最后,使用evaluate()?來計算用戶的數(shù)學表達式,然后將結(jié)果打印到屏幕上。值得注意的是,對 evaluate() 的調(diào)用會引發(fā)以下異常。
- SyntaxError:語法錯誤,當用戶輸入一個不符合Python語法的表達式時,就會發(fā)生這種情況。
- NameError:當用戶試圖使用一個不允許的名稱(函數(shù)、類或?qū)傩裕r,就會發(fā)生這種情況。
- ValueError:當用戶試圖使用一個不允許的值作為數(shù)學中某個函數(shù)的輸入時,就會發(fā)生這種情況。
注意,在main()中,捕捉了所有已知異常,并相應地打印信息給用戶。這將使用戶能夠?qū)彶楸磉_式,修復問題,并再次運行程序。
現(xiàn)在已經(jīng)使用函數(shù) eval() 在大約七十行的代碼中建立了一個數(shù)學表達式計算器。要運行這個程序,打開我們的系統(tǒng)命令行,輸入以下命令。
$ python3 mathrepl.py
這個命令將啟動數(shù)學表達式計算器的命令行界面(CLI),會在屏幕上看到類似這樣的東西。
MathREPL 1.0, your Python math expressions evaluator! Enter a valid math expression after the prompt "mr>>". Type "help" for more information. Type "quit" or "exit" to exit. mr>>
現(xiàn)在我們可以輸入并計算任何數(shù)學表達式。例如,輸入以下表達式。
mr>> 25 * 2 The result is: 50 mr>> sqrt(25) The result is: 5.0 mr>> pi The result is: 3.141592653589793
如果輸入了一個有效的數(shù)學表達式,那么應用程序就會對其進行計算,并將結(jié)果打印到屏幕上。如果表達式有任何問題,那么應用程序會告訴我們。
r>> 5 * (25 + 4 Invalid input expression syntax mr>> sum([1, 2, 3, 4, 5]) The use of 'sum' is not allowed mr>> sqrt(-15) math domain error mr>> factorial(-15) factorial() not defined for negative values
在第一個示例中,漏掉了右括號,因此收到一條消息,告訴我們語法不正確。然后調(diào)用 sum()? ,這會得到一個解釋性的異常消息。最后,使用無效的輸入值調(diào)用“math”函數(shù),應用程序?qū)⑸梢粭l消息來識別輸入中的問題。
總結(jié)
你可以使用Python的 eval() 從基于字符串或基于代碼的輸入中計算Python 表達式。當我們動態(tài)地計算Python表達式,并希望避免從頭創(chuàng)建自己的表達式求值器的麻煩時,這個內(nèi)置函數(shù)可能很有用。
在本文中,我們已經(jīng)學習了 eval() 是如何工作的,以及如何安全有效地使用它來計算任意Python表達式。
使用Python的eval()來動態(tài)計算基本的Python表達式。
使用eval()運行更復雜的語句,如函數(shù)調(diào)用、對象創(chuàng)建和屬性訪問。
最大限度地減少與使用Python的eval()有關的安全風險。
到此這篇關于利用Python+eval函數(shù)構建數(shù)學表達式計算器的文章就介紹到這了,更多相關Python eval數(shù)學表達式內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關文章希望大家以后多多支持腳本之家!