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

2023-07-17 12:02:10

簡介

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

subprocess 來源

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

模組常用函數

函數清單總覽

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檔案,裡面儲存的是命令的輸出結果

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模組可能更加適合。