【翻譯】Python Subprocess:運行外部命令

翻譯
Python Subprocess: Run External Commands

儘管 PyPI 上有不少庫,但有時你須要在 Python 代碼中運行一個外部命令。內置的 Python subprocess 模塊使之相對容易。在這篇文章中,你將學習一些關於進程和子進程的基本知識。
html

咱們將使用 Python subprocess 模塊來安全地執行外部命令,獲取輸出,並有選擇地向它們提供來自標準輸入的輸入。 若是你熟悉進程和子進程的理論,你能夠跳過第一部分。python

進程和子進程

一個在計算機上執行的程序也被稱爲一個進程。但究竟什麼是進程?讓咱們更正式地定義它。
shell

進程
進程是一個計算機程序的實例,由一個或多個線程執行。

一個進程能夠有多個線程,這被稱爲多線程。反過來,一臺計算機能夠同時運行多個進程。這些進程能夠是不一樣的程序,但它們也能夠是同一個程序的多個實例。在咱們關於 Python併發性 的文章中,對此有很是詳細的解釋。下面的圖片也來自那篇文章:

數據庫

若是你想運行一個外部命令,這意味着你須要從你的 Python 進程中建立一個新的進程。這樣的進程一般被稱爲子進程或 sub-process 。從視覺上看,這就是一個進程產生兩個子進程的狀況:

segmentfault

在內部(操做系統內核內部)發生的是所謂的 fork。進程本身 fork,意味着該進程的一個新副本被建立和啓動。若是你想使你的代碼並行化,並利用你機器上的多個CPU,這多是有用的。這就是咱們所說的多進程
數組

不過,咱們能夠利用相同的技術來啓動另外一個進程。首先,進程 fork 本身,建立一個副本。該副本將自身替換爲另外一個進程:你但願執行的進程。
安全

咱們能夠採用低級別的方式,使用 Python subprocess 模塊來完成這些工做,但幸運的是,Python 還提供了一個包裝器,能夠處理全部細節,而且這樣作也很安全。多虧了包裝器,運行外部命令只須要調用一個函數。這個封裝器就是 subprocess 庫中的函數 run(),這就是咱們將在本文中使用的。
多線程

我認爲讓你知道內部發生了什麼會很好,但若是你感到困惑,請放心,你不須要這些知識就能作到你想要的:用 Python subprocess 模塊運行外部命令。併發

使用 subprocess.run 建立一個 Python subprocess

理論講得夠多了,如今是時候動手寫一些代碼來執行外部命令了。
函數

首先,您須要導入 subprocess 庫。 因爲它是 Python 3 的一部分,所以你無需單獨安裝它。 在這個庫中,咱們將使用 run 命令。 這個命令是在 Python 3.5 中添加的。 確保你至少有這個 Python 版本,但最好是運行最新版本。 若是你須要幫助,請查看咱們詳細的 Python 安裝說明

讓咱們從對 ls 的簡單調用開始,列出當前目錄和文件:

>>> import subprocess
>>> subprocess.run(['ls', '-al'])

(a list of your directories will be printed)

事實上,咱們能夠從咱們的 Python 代碼中調用 Python 二進制文件 。 接下來讓咱們獲取系統上默認安裝的 python 3 版本:

>>> import subprocess
>>> result = subprocess.run(['python3', '--version'])
Python 3.8.5
>>> result
CompletedProcess(args=['python3', '--version'], returncode=0)

逐行解釋:

  • 咱們導入 subprocess 庫
  • 運行一個 subprocess ,在這裏是 python3 二進制文件,有一個參數:--version
  • 查看 result 變量,它的類型是 CompletedProcess

該進程返回代碼 0,表示它執行成功。 任何其餘返回碼都意味着存在某種錯誤。 這取決於你調用的進程定義的不一樣返回代碼的含義。

正如你在輸出中看到的,Python 二進制文件將其版本號打印在標準輸出上,這一般是你的終端。你的結果可能不一樣,由於你的 Python 版本可能不一樣。也許,你甚至會獲得一個看起來像這樣的錯誤。FileNotFoundError: [Errno 2] No such file or directory: 'python3'。在這種狀況下,請確保 python3 的 Python 二進制文件在你的系統上,而且也在PATH中。

捕獲 Python subprocess 的輸出

若是你運行一個外部命令,你極可能想捕獲該命令的輸出。咱們能夠經過 capture_output=True 選項實現這一目的:

>>> import subprocess
>>> result = subprocess.run(['python3', '--version'], capture_output=True, encoding='UTF-8')
>>> result
CompletedProcess(args=['python3', '--version'], returncode=0, stdout='Python 3.8.5\n', stderr='')

正如你所看到的,Python 此次沒有把它的版本打印到咱們的終端。subprocess.run 命令重定向了標準輸出和標準錯誤流,因此能夠捕獲它們併爲咱們存儲在 result 中 。查看 result 變量,咱們看到 Python 的版本是從標準輸出中捕獲的。因爲沒有錯誤,stderr是空的。

我還添加了 encoding='UTF-8' 選項。若是你不這樣作,subprocess.run 會認爲輸出是一個字節流,由於它沒有這個信息。你能夠試試。結果是,stdout 和 stderr 將是字節數組。所以,若是你知道輸出將是 ASCII文本或 UTF-8 文本,你最好指定它,以便運行函數對捕獲的輸出也進行相應編碼。

另外,你也可使用選項 text=True 而不指定編碼。Python 將把輸出做爲文本捕獲。若是你知道編碼,我建議明確指定它。

從標準輸入輸入數據

若是外部命令指望在標準輸入上得到數據,咱們也能夠經過 Python 的 subprocess.run 函數的 input 選項來輕鬆實現。請注意,我不會在這裏討論流數據。在這裏咱們將創建在前面的例子上:

>>> import subprocess
>>> code = """
... for i in range(1, 3):
...   print(f"Hello world {i}")
... """

>>> result = subprocess.run(['python3'], input=code, capture_output=True, encoding='UTF-8')
>>> print(result.stdout)
>>> print(result.stdout)
Hello world 1
Hello world 2

咱們只是用 Python3 二進制文件來執行一些 Python 代碼。徹底無用,但 (但願) 很是有指導意義!

code 變量是一個多行的 Python 字符串,咱們用 input 選項將其做爲輸入分配給 subprocess.run 命令。

運行 shell 命令

若是你想在類 Unix 系統上執行 shell 命令,我指的是你一般會在相似 Bash 的 shell 中輸入的任何命令,你須要意識到,這些命令一般不是執行的外部二進制文件。例如,像 for 和 while 循環這樣的表達式,或者管道和其它操做符,是由 shell 自己解釋的。

Python 經常之內置庫的形式提供替代方案,你應該更喜歡這些方案。可是若是你須要執行一個 shell 命令,不論是什麼緣由,當你使用 shell=True 選項時,subprocess.run 會很樂意這樣作。它容許你輸入命令,就像你在一個與 Bash 兼容的 shell 中輸入同樣:

>>> import subprocess
>>> result = subprocess.run(['ls -al | head -n 1'], shell=True)
total 396
>>> result
CompletedProcess(args=['ls -al | head -n 1'], returncode=0)

但有一個警告:使用這種方法容易受到命令注入攻擊(見:注意事項)。

須要注意的事項

運行外部命令並不是沒有風險。請很是仔細地閱讀本節。

os.system vs subprocess.run

你可能會看到 os.system() 用於執行命令的代碼示例。 不過,subprocess 模塊更增強大,官方 Python 文檔推薦使用它而不是 os.system()。os.system 的另外一個問題是,它更容易被注入命令。

命令注入

一種常見的攻擊或漏洞,是注入額外的命令來得到對計算機系統的控制。 例如,若是你要求你的用戶輸入並在調用 os.system() 或調用 subprocess.run(...., shell=True) 時使用這些輸入,你就有可能受到命令注入攻擊。

爲了演示,下面的代碼容許咱們運行任何 shell 命令。

import subprocess
thedir = input()
result = subprocess.run([f'ls -al {thedir}'], shell=True)

由於咱們直接使用了用戶的輸入,用戶只需在其後面加上分號,就能夠運行任何命令。例如,下面的輸入將列出/目錄並回顯一個文本。本身試試吧。

/; echo "command injection worked!";

解決方案不是嘗試清理用戶的輸入。你可能很想開始尋找分號,並在發現分號時拒絕輸入。不要這樣作;黑客們在這種狀況下至少能想到5種其餘的追加命令的方法。這是一場艱苦的戰鬥。

更好的解決辦法是不使用shell=True,而是像咱們在前面的例子中那樣在一個列表中輸入命令。像這樣的輸入在這種狀況下會失敗,由於 subprocess 模塊會肯定輸入是你正在執行的程序的參數,而不是一個新的命令。

使用一樣的輸入,但 shell=False,你會獲得下面的結果。

import subprocess
thedir = input()
>>> result = subprocess.run([f'ls -al {thedir}'], shell=False)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/usr/lib/python3.8/subprocess.py", line 489, in run
    with Popen(*popenargs, **kwargs) as process:
  File "/usr/lib/python3.8/subprocess.py", line 854, in __init__
    self._execute_child(args, executable, preexec_fn, close_fds,
  File "/usr/lib/python3.8/subprocess.py", line 1702, in _execute_child
    raise child_exception_type(errno_num, err_msg, err_filename)
FileNotFoundError: [Errno 2] No such file or directory: 'ls -al /; echo "command injection worked!";'

該命令被看成 ls 的一個參數,而 ls 則告訴咱們,它找不到那個文件或目錄。

用戶輸入老是危險的

事實上,使用用戶輸入老是危險的,不只僅是由於命令注入。例如,假設你容許用戶輸入一個文件名。以後,咱們讀取該文件並將其顯示給用戶。雖然這看起來無害,但用戶能夠輸入這樣的內容:.../.../.../configuration/settings.yaml。

其中 settings.yaml 可能包含你的數據庫密碼......哎呀! 你老是須要對用戶輸入進行適當的清理和檢查。不過,如何正確地作到這一點,已經超出了本文的範圍。

繼續學習

如下相關資源將幫助你更深刻地研究這個主題:

相關文章
相關標籤/搜索