subprocess Python執行系統命令最優選模塊

image

簡介

subprocess 是 Python 中執行操作系統級別的命令的模塊,所謂系級級別的命令就是如ls /etc/user ifconfig 等和操作系統有關的命令。
subprocess 創建子進程來執行相關命令,並連接它們的輸入、輸出和錯誤管道,獲取它們的返回狀態。

subprocess 來源

Subprocess模塊開發之前,標準庫已有大量用於執行系統級別命令的的方法,如os.system、os.spawn等。但是略顯混亂使開發者難以抉擇,因此subprocess的目的是打造一個統一模塊來替換之前執行系統界別命令的方法。
所以 推薦使用subprocess替代了一些老的方法,比如:os.system、os.spawn*等。

模塊常用函數

函數清單總覽

image

Subprocess 模塊推薦使用run方法替換低版本方法,如果想要更加精細的控制可以使用Popen方法。所以本教程中重點介紹run和Popen方法。

subprocess.run()

函數簽名

subprocess.run(
    args, 
    *, 
    stdin=None, 
    input=None, 
    stdout=None, 
    stderr=None, 
    capture_output=False, 
    shell=False, 
    cwd=None, 
    timeout=None, 
    check=False, 
    encoding=None, 
    errors=None, 
    text=None, 
    env=None, 
    universal_newlines=None, 
    **other_popen_kwargs
)

簡單使用

執行簡單shell命令
默認情況下,子進程會繼承父進程的設置,會將輸出顯示在終端上

import subprocess

res = subprocess.run("ls -al /home/ljk/Videos", shell=True)
>>>
(ymir) ➜  subprocess_demo python3 subprocess_demo.py
總用量 96
drwxr-xr-x  3 ljk ljk  4096 4月  11 11:04  .
drwxr-x--- 62 ljk ljk  4096 7月   6 13:40  ..
-rw-r--r--  1 ljk ljk 84176 4月  11 11:04  346e30f4-9119-11eb-bb4a-4a238cf0c417.mp4
lrwxrwxrwx  1 ljk ljk    36 10月 21  2022  dde-introduction.mp4 -> /usr/share/dde-introduction/demo.mp4
drwxr-xr-x  2 ljk ljk  4096 4月  11 18:26 'Screen Recordings'

如果命令沒有輸出則不會打印輸出信息

獲取狀態碼
returncode 是subprocess的返回碼

import subprocess

res = subprocess.run("ls -al /home/ljk/Videos", shell=True)

print("returncode:", res.returncode)
>>>
總用量 96
drwxr-xr-x  3 ljk ljk  4096 7月   6 13:41  .
drwxr-x--- 62 ljk ljk  4096 7月   6 13:47  ..
-rw-r--r--  1 ljk ljk 84176 4月  11 11:04  346e30f4-9119-11eb-bb4a-4a238cf0c417.mp4
-rw-r--r--  1 ljk ljk     0 7月   6 13:41  a.txt
lrwxrwxrwx  1 ljk ljk    36 10月 21  2022  dde-introduction.mp4 -> /usr/share/dde-introduction/demo.mp4
drwxr-xr-x  2 ljk ljk  4096 4月  11 18:26 'Screen Recordings'
0

參數介紹

args:要執行的命令,可以是字符串形式或由命令及其參數組成的列表。例如,['ls', '-l'] 或 'ls -l'。
input:允許將字節或字符串傳遞給子進程的標準輸入(stdin)。
stdin:子進程的標準輸入。默認爲None,可以是以下三個參數:

  1. subprocess.PIPE 創建一個管道,允許與子進程進行通信
  2. subprocess.DEVNULL 特殊的文件對象,可以將其用於丟棄子進程的輸出
  3. 一個打開的文件對象,將內容寫入文件

stdout: 同 stdin
stderr: 同 stdin
capture_output :這個參數控制是否捕獲外部命令的標準輸出(stdout)和標準錯誤(stderr)。如果將其設置爲True,run()函數將返回一個CompletedProcess對象,該對象具有stdout和stderr屬性,分別存儲了命令的標準輸出和標準錯誤輸出。如果設置爲False,標準輸出和標準錯誤將被髮送到控制檯。默認爲False。
shell:指定是否通過shell來執行命令。如果爲True,命令將在shell中執行;如果爲False,則直接調用可執行文件。默認爲False。
cwd:設置子進程的工作目錄。默認爲None,表示使用當前工作目錄。
timeout:設置子進程的超時時間(秒)。如果子進程在指定的時間內沒有運行完成,則會引發TimeoutExpired異常。
check:設置是否檢查子進程的返回碼。如果爲True,並且子進程的返回碼不爲零,則會引發CalledProcessError異常。
encoding:該參數指定輸出結果的字符編碼。默認情況下,它是None,表示使用原始的字節數據。如果提供了有效的編碼名稱(如"utf-8"、"gbk"等),run()函數將自動將輸出解碼爲字符串。
errors:該參數定義在解碼輸出時如何處理編碼錯誤。它與Python的str.decode()函數的相同參數含義相匹配。常用的值包括"strict" (默認值,拋出異常)、"ignore" (忽略錯誤字符) 和 "replace" (用替代字符代替錯誤字符)。
text:指定是否將輸出結果以文本形式返回。如果爲True,則結果以字符串形式返回,同時input或者stdin參數也需要輸入String;如果爲False,則返回字節流。默認爲False。
env:該參數允許您爲子進程指定環境變量。它可以接受一個字典類型的對象,其中鍵是環境變量的名稱,值是環境變量的值。通過設置env參數,您可以在子進程中使用特定的環境變量。
universal_newlines: 該參數影響的是輸入與輸出的數據格式,比如它的值默認爲False,此時stdout和stderr的輸出是字節序列;當該參數的值設置爲True時,stdout和stderr的輸出是字符串。

args 執行命令

args傳入的是要執行的系統命令,可以接收兩種方法:字符串或列表。

  • 使用列表形式subprocess.run(["ls", "-al"])
  • 使用字符串形式 subprocess.run("ls -al", shell=True)。使用字符串形式必須設置參數shell=True
import subprocess

subprocess.run(["ls", "-al", "/Users/ljk/Documents/code/daily_dev"])

subprocess.run("ls -al /Users/ljk/Documents/code/daily_dev", shell=True)

>>>
➜  subprocess_demo python3 subprocess_demo.py
total 0
drwxr-xr-x   5 ljk  staff  160  7 11 22:27 .
drwxr-xr-x@ 18 ljk  staff  576  7  3 22:11 ..
drwxr-xr-x   3 ljk  staff   96  6 24 18:28 docker_dev
drwxr-xr-x   3 ljk  staff   96  6 17 22:08 requests_demo
drwxr-xr-x   4 ljk  staff  128  7 11 22:30 subprocess_demo
total 0
drwxr-xr-x   5 ljk  staff  160  7 11 22:27 .
drwxr-xr-x@ 18 ljk  staff  576  7  3 22:11 ..
drwxr-xr-x   3 ljk  staff   96  6 24 18:28 docker_dev
drwxr-xr-x   3 ljk  staff   96  6 17 22:08 requests_demo
drwxr-xr-x   4 ljk  staff  128  7 11 22:30 subprocess_demo

默認情況下,命令的輸出是直接打印到控制檯上的。

stdin、stdout、sterr 設置命令輸出輸入的對象

這三個值是用來設置標準輸入,標準輸出,標準錯誤的。默認情況下,子進程會繼承父進程的設置,會將輸出顯示在控制檯上,除此之外也可以設置成如下三個值:

  1. subprocess.PIPE 創建一個管道,允許與子進程進行通信
  2. subprocess.DEVNULL 特殊的文件對象,可以將其用於丟棄子進程的輸出
  3. 一個打開的文件對象,將內容寫入文件
    以studout爲例子,驗證這三個輸出選項。

將命令輸出保存到管道

import subprocess

res = subprocess.run(["ls", "-al", "/Users/ljk/Documents/code/daily_dev"], stdout=subprocess.PIPE)

print(res.returncode)
print(res.stdout)
>>>
0
b'total 0\ndrwxr-xr-x   5 ljk  staff  160  7 11 22:27 .\ndrwxr-xr-x@ 18 ljk  staff  576  7  3 22:11 ..\ndrwxr-xr-x   3 ljk  staff   96  6 24 18:28 docker_dev\ndrwxr-xr-x   3 ljk  staff   96  6 17 22:08 requests_demo\ndrwxr-xr-x   4 ljk  staff  128  7 11 22:44 subprocess_demo\n'

命令輸出不再打印到控制檯上,而是保存到對象裏,通過對象的stdout獲取到。此時命令輸出結果是字節串格式的。可以通過設置text=True,將命令輸出以文本形式保存。

import subprocess

res = subprocess.run(["ls", "-al", "/Users/ljk/Documents/code/daily_dev"], stdout=subprocess.PIPE, text=True)

print(res.returncode)
print(res.stdout)
>>>
0
total 0
drwxr-xr-x   5 ljk  staff  160  7 11 22:27 .
drwxr-xr-x@ 18 ljk  staff  576  7  3 22:11 ..
drwxr-xr-x   3 ljk  staff   96  6 24 18:28 docker_dev
drwxr-xr-x   3 ljk  staff   96  6 17 22:08 requests_demo
drwxr-xr-x   4 ljk  staff  128  7 11 22:48 subprocess_demo

命令輸出保存到文件中
可以將命令的輸出保存到一個文件中,stdout傳入一個打開的文件對象即可。

import subprocess

with open("a.txt", "a+") as f:
    res = subprocess.run(["ls", "-al", "/Users/ljk/Documents/code/daily_dev"], stdout=f, text=True)
    print(res.returncode)
    print(res.stdout)
    
>>>
0
None

此時在目錄下生成了a.txt文件,裏面保存的是命令的輸出結果
image

capture_output 捕獲控制檯輸出

捕獲命令輸出。默認爲false,所有的命令輸出都打印到控制檯。設置爲true,所有命令的輸出都被捕獲保存到返回對象中。

import subprocess

res = subprocess.run("ls -al /Users/ljk/Documents/code/daily_dev", shell=True, capture_output=True)
print(res.returncode)
print(res.stdout)
>>>
0
b'total 0\ndrwxr-xr-x   5 ljk  staff  160  7 11 22:27 .\ndrwxr-xr-x@ 18 ljk  staff  576  7  3 22:11 ..\ndrwxr-xr-x   3 ljk  staff   96  6 24 18:28 docker_dev\ndrwxr-xr-x   3 ljk  staff   96  6 17 22:08 requests_demo\ndrwxr-xr-x   5 ljk  staff  160  7 13 20:38 subprocess_demo\n'

cwd 設置命令執行的目錄

設置子進程的工作目錄。默認爲None,表示使用當前工作目錄

import subprocess

res = subprocess.run("pwd", shell=True, cwd="/Users/ljk/Documents/code/")
print(res.returncode)

/Users/ljk/Documents/code
0

如果腳本需要在特定的目錄中執行,可以設置該參數

timeout 設置命令超執行時時間

當一些命令有時間上的要求,可以設置命令執行的超時時間。如果命令在指定的時間內沒有運行完成,則會引發TimeoutExpired異常。

import subprocess

res = subprocess.run("sleep 10 && ls", shell=True, timeout=5)
print(res.returncode)
>>>
Traceback (most recent call last):
  File "/Users/ljk/Documents/code/daily_dev/subprocess_demo/subprocess_demo.py", line 22, in <module>
    res = subprocess.run("sleep 10 && ls", shell=True, timeout=5)
  File "/Library/Developer/CommandLineTools/Library/Frameworks/Python3.framework/Versions/3.9/lib/python3.9/subprocess.py", line 507, in run
    stdout, stderr = process.communicate(input, timeout=timeout)
  File "/Library/Developer/CommandLineTools/Library/Frameworks/Python3.framework/Versions/3.9/lib/python3.9/subprocess.py", line 1134, in communicate
    stdout, stderr = self._communicate(input, endtime, timeout)
  File "/Library/Developer/CommandLineTools/Library/Frameworks/Python3.framework/Versions/3.9/lib/python3.9/subprocess.py", line 2005, in _communicate
    self.wait(timeout=self._remaining_time(endtime))
  File "/Library/Developer/CommandLineTools/Library/Frameworks/Python3.framework/Versions/3.9/lib/python3.9/subprocess.py", line 1189, in wait
    return self._wait(timeout=timeout)
  File "/Library/Developer/CommandLineTools/Library/Frameworks/Python3.framework/Versions/3.9/lib/python3.9/subprocess.py", line 1909, in _wait
    raise TimeoutExpired(self.args, timeout)
subprocess.TimeoutExpired: Command 'sleep 10 && ls' timed out after 4.999915208999999 seconds

執行的命令是先睡眠10s,然後執行ls。設置的超時時間是5秒,所以執行的第5s就拋出timeout錯誤。

check 返回碼非1拋出錯誤

檢查子進程的返回碼。如果爲True,並且子進程的返回碼不爲零,則會引發CalledProcessError異常。以下代碼做了一組對比,ls一個不存在目錄,設置check=True的會拋出異常。

res = subprocess.run("ls no_exsit.txt", shell=True)
print(res.returncode)
>>>
ls: no_exsit.txt: No such file or directory
1

res = subprocess.run("ls no_exsit.txt", shell=True, check=True)
print(res.returncode)
>>>
ls: no_exsit.txt: No such file or directory
Traceback (most recent call last):
  File "/Users/ljk/Documents/code/daily_dev/subprocess_demo/subprocess_demo.py", line 25, in <module>
    res = subprocess.run("ls no_exsit.txt", shell=True, check=True)
  File "/Library/Developer/CommandLineTools/Library/Frameworks/Python3.framework/Versions/3.9/lib/python3.9/subprocess.py", line 528, in run
    raise CalledProcessError(retcode, process.args,
subprocess.CalledProcessError: Command 'ls no_exsit.txt' returned non-zero exit status 1.

encoding、text 設置輸出結果的格式

encoding 用於設置命令輸出的編碼格式。 默認情況下,它是None,表示使用原始的字節數據。如果提供了有效的編碼名稱,如"utf-8"、"gbk",將自動將輸出解碼爲字符串。示例演示encoding=True

# 原始輸出
res = subprocess.run("ls -al /Users/ljk/Documents/code/daily_dev", shell=True, capture_output=True)
print(res.returncode)
print(res.stdout)
>>>
0
b'total 0\ndrwxr-xr-x   5 ljk  staff  160  7 11 22:27 .\ndrwxr-xr-x@ 18 ljk  staff  576  7  3 22:11 ..\ndrwxr-xr-x   3 ljk  staff   96  6 24 18:28 docker_dev\ndrwxr-xr-x   3 ljk  staff   96  6 17 22:08 requests_demo\ndrwxr-xr-x   5 ljk  staff  160  7 13 21:07 subprocess_demo\n'

# 設置encoding="utf-8"
res = subprocess.run("ls -al /Users/ljk/Documents/code/daily_dev", shell=True, capture_output=True, encoding="utf-8")
print(res.returncode)
print(res.stdout)

>>>
0
total 0
drwxr-xr-x   5 ljk  staff  160  7 11 22:27 .
drwxr-xr-x@ 18 ljk  staff  576  7  3 22:11 ..
drwxr-xr-x   3 ljk  staff   96  6 24 18:28 docker_dev
drwxr-xr-x   3 ljk  staff   96  6 17 22:08 requests_demo
drwxr-xr-x   5 ljk  staff  160  7 13 21:07 subprocess_demo

text 參數是用於設置命令輸出的格式。命令輸出默認是字節串,text=True表示輸出格式爲字符串。和encoding=True 基本等價。

# 原始輸出
res = subprocess.run("ls -al /Users/ljk/Documents/code/daily_dev", shell=True, capture_output=True)
print(res.returncode)
print(res.stdout)
>>>
0
b'total 0\ndrwxr-xr-x   5 ljk  staff  160  7 11 22:27 .\ndrwxr-xr-x@ 18 ljk  staff  576  7  3 22:11 ..\ndrwxr-xr-x   3 ljk  staff   96  6 24 18:28 docker_dev\ndrwxr-xr-x   3 ljk  staff   96  6 17 22:08 requests_demo\ndrwxr-xr-x   5 ljk  staff  160  7 13 21:10 subprocess_demo\n'

# 設置text=True
res = subprocess.run("ls -al /Users/ljk/Documents/code/daily_dev", shell=True, capture_output=True, text=True)
print(res.returncode)
print(res.stdout)
>>>
0
total 0
drwxr-xr-x   5 ljk  staff  160  7 11 22:27 .
drwxr-xr-x@ 18 ljk  staff  576  7  3 22:11 ..
drwxr-xr-x   3 ljk  staff   96  6 24 18:28 docker_dev
drwxr-xr-x   3 ljk  staff   96  6 17 22:08 requests_demo
drwxr-xr-x   5 ljk  staff  160  7 13 21:10 subprocess_demo

返回對象

subprocess.run()函數返回值是一個CompletedProcess類的實例,subprocess.completedPorcess類是Python 3.5以上才存在的。它表示的是一個已結束進程的狀態信息,它所包含的屬性和方法如下:
args: 用於加載該進程的參數,這可能是一個列表或一個字符串。
returncode: 子進程的退出狀態碼。通常情況下,退出狀態碼爲0則表示進程成功運行了;一個負值-N表示這個子進程被信號N終止了。
stdout: 從子進程捕獲的stdout。這通常是一個字節串。如果設置了encoding或text參數,返回就是字符串。
stderr: 從子進程捕獲的stderr。它的值與stdout一樣,是一個字節序列或一個字符串。如果stderr沒有被捕獲的話,它的值就爲None。
check_returncode(): 如果returncode是一個非0值,則該方法會拋出一個CalledProcessError異常。

示例:

import subprocess

res = subprocess.run("ls -al /home/ljk/Videos", shell=True)


print("args:", res.args)
print("returncode:", res.returncode)
print("stdout:", res.stdout)
print("stderr:", res.stderr)
print("returncode():", res.check_returncode())

>>>
➜ subprocess_demo python3 subprocess_demo.py
總用量 96
drwxr-xr-x  3 ljk ljk  4096 7月   6 13:41  .
drwxr-x--- 62 ljk ljk  4096 7月   6 13:47  ..
-rw-r--r--  1 ljk ljk 84176 4月  11 11:04  346e30f4-9119-11eb-bb4a-4a238cf0c417.mp4
-rw-r--r--  1 ljk ljk     0 7月   6 13:41  a.txt
lrwxrwxrwx  1 ljk ljk    36 10月 21  2022  dde-introduction.mp4 -> /usr/share/dde-introduction/demo.mp4
drwxr-xr-x  2 ljk ljk  4096 4月  11 18:26 'Screen Recordings'
args: ls -al /home/ljk/Videos
returncode: 0
stdout: None
stderr: None
returncode(): None

subprocess.Popen()

popen是一個功能更強大的方法,而run是它的一個簡化版。如果run函數不能滿足功能的要求,可以嘗試功能更多的popen方法。
除了方法的多少之外,run和popen最大的區別在於:run方法是阻塞調用,會一直等待命令執行完成或失敗;popen是非阻塞調用,執行之後立刻返回,結果通過返回對象獲取。

popen函數簽名:

subprocess.Popen(
    args, 
    bufsize=- 1, 
    executable=None, 
    stdin=None, 
    stdout=None, 
    stderr=None, 
    preexec_fn=None, 
    close_fds=True, 
    shell=False, 
    cwd=None, 
    env=None, 
    universal_newlines=None, 
    startupinfo=None, 
    creationflags=0, 
    restore_signals=True, 
    start_new_session=False, 
    pass_fds=(), 
    *, 
    group=None, 
    extra_groups=None, 
    user=None, 
    umask=- 1, 
    encoding=None, 
    errors=None, 
    text=None, 
    pipesize=- 1, 
    process_group=None
)

簡單使用

和run一樣執行命令

import subprocess

subprocess.Popen("ls -al /Users/ljk/Documents/code/daily_dev", shell=True)

>>>>
None
total 0
drwxr-xr-x   5 ljk  staff  160  7 11 22:27 .
drwxr-xr-x@ 18 ljk  staff  576  7  3 22:11 ..
drwxr-xr-x   3 ljk  staff   96  6 24 18:28 docker_dev
drwxr-xr-x   3 ljk  staff   96  6 17 22:08 requests_demo
drwxr-xr-x   5 ljk  staff  160  7 13 21:32 subprocess_demo

執行阻塞命令

import subprocess

res = subprocess.Popen("sleep 10 && ls -al", shell=True)
print(res)
>>>
<Popen: returncode: None args: 'sleep 10 && ls -al'>

遇到阻塞命令也會直接返回,返回是一個對象。可以通過對象獲取命令執行的結果。

參數介紹

注意:因爲run是popen的一個簡化版本,所以run擁有的函數popen也擁有。這裏就不再重複說明了。

bufsize:定義了子進程的緩衝大小。可選參數,默認爲-1,表示使用系統默認的緩衝大小。
executable:指定要執行的程序路徑。如果未提供該值,則通過PATH環境變量來確定可執行文件的位置。
preexec_fn:指定在子進程啓動之前將要執行的函數。該函數將在fork()調用成功,但exec()調用之前被調用。
close_fds:指定是否關閉所有文件描述符。默認爲False。
start_new_session(僅 POSIX):如果該參數設置爲True,則在啓動子進程時創建一個新的進程會話。默認爲False。
pass_fds(僅 POSIX):通過這個參數傳遞一個文件描述符集合,這些文件描述符將保持打開狀態並傳遞給子進程。默認爲None。
startupinfo:一個可選的subprocess.STARTUPINFO對象,用於指定子進程的啓動信息,如窗口大小、窗口標題等。默認爲None。
creationflags:用於指定子進程的創建標誌,控制子進程的各種行爲。可以使用subprocess.CREATE_NEW_CONSOLE、subprocess.CREATE_NEW_PROCESS_GROUP等常量進行設置。默認爲0。
restore_signals(僅 POSIX):用於確定是否在子進程中恢復信號處理程序的默認行爲。默認爲True。
group(僅 POSIX): 如果 group 不爲 None,則 setregid() 系統調用將於子進程執行之前在下級進程中進行。 如果所提供的值爲一個字符串,將通過 grp.getgrnam() 來查找它,並將使用 gr_gid 中的值。 如果該值爲一個整數,它將被原樣傳遞。 (POSIX 專屬)
extra_groups(僅 POSIX): 如果 extra_groups 不爲 None,則 setgroups() 系統調用將於子進程之前在下級進程中進行。 在 extra_groups 中提供的字符串將通過 grp.getgrnam() 來查找,並將使用 gr_gid 中的值。 整數值將被原樣傳遞。
user(僅 POSIX): 如果 user 不爲 None,則 setreuid() 系統調用將於子進程執行之前在下級進程中進行。 如果所提供的值爲一個字符串,將通過 pwd.getpwnam() 來查找它,並將使用 pw_uid 中的值。 如果該值爲一個整數,它將被原樣傳遞。 (POSIX 專屬)

Popen類的方法與參數介紹

communicate(input=None, timeout=None): 與子進程進行交互,發送輸入並獲取輸出結果。可以在參數input中指定要發送給子進程的輸入內容。該方法會阻塞當前進程,直到子進程完成並返回輸出結果。可選的timeout參數用於設置超時時間。
poll(): 檢查子進程是否已經退出,如果已退出則返回退出狀態碼,否則返回None。
wait(timeout=None): 等待子進程完成並返回退出狀態碼。可選的timeout參數用於設置超時時間。
terminate(): 向子進程發送終止信號。這通常是優雅地終止子進程。
kill(): 強制終止子進程。
send_signal(signal): 向子進程發送信號,其中signal參數表示要發送的信號類型,如SIGINT、SIGTERM等。

communicate 獲取命令輸出

發送輸入並獲取輸出結果。可以在參數input中指定要發送給子進程的輸入內容。該方法會阻塞當前進程,直到子進程完成並返回輸出結果。函數返回一個元組: (stdoutdata , stderrdata )

import subprocess

res = subprocess.Popen("sleep 3 && ls -al", shell=True)
print(res.communicate())
>>>
total 24
drwxr-xr-x  5 ljk  staff   160  7 13 21:40 .
drwxr-xr-x  5 ljk  staff   160  7 11 22:27 ..
-rw-r--r--  1 ljk  staff   269  7 11 22:50 a.txt
-rwxrwxrwx  1 ljk  staff    42  7 11 22:29 ls_demo.sh
-rw-r--r--  1 ljk  staff  1069  7 13 21:40 subprocess_demo.py
(None, None)

該方法和run函數行爲一致,將非阻塞調用變成阻塞調用。
subprocess.Popen().communicate() 等價於 subprocess.run()

poll 檢查子進程

Poll 檢查子進程是否已經退出,如果已退出則返回退出狀態碼,否則返回None。

import time

res = subprocess.Popen("sleep 3 && ls -al", shell=True)
print(res.poll())
time.sleep(4)
print(res.poll())
>>>>

None
total 24
drwxr-xr-x  5 ljk  staff   160  7 13 21:39 .
drwxr-xr-x  5 ljk  staff   160  7 11 22:27 ..
-rw-r--r--  1 ljk  staff   269  7 11 22:50 a.txt
-rwxrwxrwx  1 ljk  staff    42  7 11 22:29 ls_demo.sh
-rw-r--r--  1 ljk  staff  1097  7 13 21:39 subprocess_demo.py
0

在執行命令之後立刻用poll檢查發現返回None,此時因爲子進程還沒有退出。程序睡眠4s之後命令已經退出,再次執行poll返回了狀態碼。在兩次打印中間是命令的標準輸出。

檢查命令執行狀態並獲取返回值

res = subprocess.Popen("sleep 3 && ls -al", shell=True, stdout=subprocess.PIPE, encoding="utf-8")
while res.poll() is None:
    time.sleep(0.5)
    print("命令還在執行中...")
print("命令執行完成,獲取結果:")
print(res.communicate())

wait 用於等待命令執行完成

等待子進程完成並返回退出狀態碼。可選的timeout參數用於設置超時時間。

# 等待,不設置超時
import subprocess

res = subprocess.Popen("sleep 3 && ls -al", shell=True)
print(res.wait())

>>>
total 24
drwxr-xr-x  5 ljk  staff   160  7 13 22:04 .
drwxr-xr-x  5 ljk  staff   160  7 11 22:27 ..
-rw-r--r--  1 ljk  staff   269  7 11 22:50 a.txt
-rwxrwxrwx  1 ljk  staff    42  7 11 22:29 ls_demo.sh
-rw-r--r--  1 ljk  staff  1071  7 13 22:04 subprocess_demo.py
0

# 等待,設置超時,報錯
res = subprocess.Popen("sleep 6 && ls -al", shell=True)
print(res.wait(timeout=5))

>>>
Traceback (most recent call last):
  File "/Users/ljk/Documents/code/daily_dev/subprocess_demo/subprocess_demo.py", line 41, in <module>
    print(res.wait(timeout=5))
  File "/Library/Developer/CommandLineTools/Library/Frameworks/Python3.framework/Versions/3.9/lib/python3.9/subprocess.py", line 1189, in wait
    return self._wait(timeout=timeout)
  File "/Library/Developer/CommandLineTools/Library/Frameworks/Python3.framework/Versions/3.9/lib/python3.9/subprocess.py", line 1909, in _wait
    raise TimeoutExpired(self.args, timeout)
subprocess.TimeoutExpired: Command 'sleep 6 && ls -al' timed out after 5 seconds

drwxr-xr-x  5 ljk  staff   160  7 13 22:04 .
drwxr-xr-x  5 ljk  staff   160  7 11 22:27 ..
-rw-r--r--  1 ljk  staff   269  7 11 22:50 a.txt
-rwxrwxrwx  1 ljk  staff    42  7 11 22:29 ls_demo.sh
-rw-r--r--  1 ljk  staff  1071  7 13 22:04 subprocess_demo.py

wait 設置了超時,在指定時間之內沒有執行完成會拋出異常,但是命令還是會在後端繼續執行,沒有停止

terminate 優雅的終止執行的命令

Terminate 可以終止一個還沒有執行完成的命令。wait設置超時之後雖然會拋出異常,但是並不會終止命令。而terminate就可以優雅的終止命令。

import subprocess

res = subprocess.Popen("sleep 3 && ls -al", shell=True)
print(res.poll())
res.terminate()
>>>
None

如果沒有終止會打印輸出信息,而終止之後就不會再打印出來了。所謂優雅可能是停止命令之前會關閉打開的文件,管道,套接字等。

kill 強制終止執行的命令

使用kill可以強制將執行的命令殺死,類似於linux系統中的kill命令。kill不會關閉已經打開的文件句柄等。

import subprocess

res = subprocess.Popen("sleep 3 && ls -al", shell=True)
print(res.poll())
res.kill()

異常捕獲

subprocess 會拋出一些異常,自帶的異常捕獲模塊可以完成相關異常的捕獲

TimeoutExpired

異常類型:class subprocess.TimeoutExpired(cmd, timeout, output=None)
當子進程執行時間超過指定的超時時間時引發。
屬性:

  • cmd:執行的命令。
  • timeout:設定的超時時間。
  • output:子進程輸出的內容。

CalledProcessError

異常類型:class subprocess.CalledProcessError(returncode, cmd, output=None, stderr=None)
在使用 check_output() 或 check_call() 函數執行外部命令並返回非零退出碼時引發。
屬性:

  • returncode:子進程的返回碼。
  • cmd:已執行的命令。
  • output:標準輸出的內容(如果沒有重定向則爲 None)
  • stderr:標準錯誤的內容(如果沒有重定向則爲 None)

使用示例:

import subprocess

try:
    res = subprocess.run("ls no_exsit.txt", shell=True, check=True)
except subprocess.CalledProcessError as e:
    print("returncode:", e.returncode)
    print("cmd:", e.cmd)
    print("output:", e.output)
    print("stderr:", e.stderr
    
>>>
ls: 無法訪問'no_exsit.txt': 沒有那個文件或目錄
returncode: 2
cmd: ls no_exsit.txt
output: None
stderr: None

subprocess.SubprocessError

這是其他subprocess常類的基類,可以用於捕獲所有與子進程相關的異常。

舊函數簡介

  1. subprocess.call()
    函數執行給定的命令,並等待其完成。它返回命令的退出碼。
    示例代碼:
import subprocess

return_code = subprocess.call(["ls", "-l"])
print(f"Command returned with exit code: {return_code}")
  1. subprocess.check_call()
    check_call() 函數也執行給定的命令,但與 call() 不同的是,如果命令返回非零的退出碼,則會引發 CalledProcessError 異常。

示例代碼:

import subprocess

subprocess.check_call(["ls", "-l"])
print("Command executed successfully")
  1. subprocess.getoutput()
    getoutput() 函數執行給定的命令,並返回其輸出作爲字符串。
    示例代碼:
import subprocess

output = subprocess.getoutput("echo Hello, subprocess!")
print(output)
  1. subprocess.getstatusoutput()
    getstatusoutput() 函數執行給定的命令,並返回一個元組,包含命令的退出狀態碼和輸出結果的字符串。
    示例代碼:
import subprocess

status, output = subprocess.getstatusoutput("ls -l")
print(f"Exit status: {status}")
print(f"Output: {output}")
  1. subprocess.check_output()
    check_output() 函數執行給定的命令,並返回其輸出結果作爲字節字符串。
    示例代碼:
import subprocess

output = subprocess.check_output(["ls", "-l"])
print(output.decode("utf-8"))

subprocess 和 os 模塊比較

與os模塊對比而言,subprocess模塊具有以下這些優勢:

  1. 更豐富的功能
    subprocess模塊提供了更多的方法和選項來執行子進程,並與其進行交互。例如,可以捕獲子進程的輸出、發送輸入數據、設置超時時間等。
  2. 更強的靈活性
    subprocess模塊允許您以多種不同的方式執行子進程,包括使用管道、重定向輸入輸出、執行shell命令等。這使得您能夠更靈活地控制和處理子進程的輸入輸出。
  3. 更好的安全性
    subprocess模塊提供了更嚴格的參數處理機制,可以幫助避免常見的安全問題,如命令注入攻擊。它支持傳遞參數列表而不是字符串,從而減少了潛在的安全漏洞。

因此,當執行復雜的子進程操作、需要更多控制權和靈活性、以及考慮到安全性時,優先選擇subprocess模塊是更好的選擇。而對於簡單的命令執行需求或與操作系統相關的功能,os模塊可能更加適合。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章