python subprocess-更优雅的创建子进程
簡介
如PEP324所言,在任何編程語言中,啟動進程都是非常常見的任務,python也是如此,而不正確的啟動進程方式會給程序帶來很大安全風險。Subprocess模塊開發(fā)之前,標準庫已有大量用于進程創(chuàng)建的接口函數(如os.system、os.spawn*),但是略顯混亂使開發(fā)者難以抉擇,因此Subprocess的目的是打造一個“統(tǒng)一”模塊來提供之前進程創(chuàng)建相關函數的功能實現(xiàn)。與之前的相關接口相比,提供了以下增強功能:
- 一個“統(tǒng)一”的模塊來提供以前進程創(chuàng)建相關函數的所有功能;
- 跨進程異常優(yōu)化:子進程中的異常會在父進程再次拋出,以便檢測子進程執(zhí)行情況;
- 提供用于在fork和exec之間執(zhí)行自定義代碼的鉤子;
- 沒有隱式調用/bin/sh,這意味著不需要對危險的shell meta characters進行轉義;
- 支持文件描述符重定向的所有組合;
- 使用subprocess模塊,可以控制在執(zhí)行新程序之前是否應關閉所有打開的文件描述符;
- 支持連接多個子進程 ;
- 支持universal newline;
- 支持communication()方法,它使發(fā)送stdin數據以及讀取stdout和stderr數據變得容易,而沒有死鎖的風險;
subprocess 基礎
subprocess.run
subprocess推薦使用run 函數來處理它所能夠處理的一切cases, 如果需要更高級靈活的定制化使用,則可以使用其底層的popen接口來實現(xiàn)。run函數signature為:
def 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)->subprocess.CompletedProcess:pass上面寫的參數只是最常見的參數,完整的函數列表在很大程度上與popen函數的參數相同,即這個函數的大多數參數都被傳遞到該接口。(timeout、input、check和capture_output除外)。該函數常用參數如下:
-
args, 必選參數,數據類型應為一個string 或則 一個sequence(list, tuple等等)。通常最好傳遞一個sequence,因為它允許模塊處理任何必需的參數轉義和引用; 如果傳遞的是字符串,則shell必須為True,否則該字符串必須簡單地為要執(zhí)行的程序的名字,而不能指定任何參數。
在復雜情況下,構建一個sequence-like的參數可以借助shlex.split()來實現(xiàn)
>>> import shlex, subprocess >>> command_line = input() /bin/vikings -input eggs.txt -output "spam spam.txt" -cmd "echo '$MONEY'" >>> args = shlex.split(command_line) >>> print(args) ['/bin/vikings', '-input', 'eggs.txt', '-output', 'spam spam.txt', '-cmd', "echo '$MONEY'"] >>> p = subprocess.Popen(args) # Success!shell模式執(zhí)行等同于:Popen(['/bin/sh', '-c', args[0], args[1], ...])
## 以下兩句代碼等價,都是通過shell模式執(zhí)行`ls -l` subprocess.run('ls -l', shell=True) subprocess.run(['/bin/sh', '-c', 'ls -l'], shell=False)## 下面代碼通過非shell模式執(zhí)行`ls -l` subprocess.run(['ls', '-l'], shell=False)# 下面代碼實際執(zhí)行的是`ls` subprocess.run(['/bin/sh', '-c', 'ls', '-l'], shell=False)當使用shell=True時,要注意可能潛在的安全問題,需要確保所有空格和元字符都被適當地引用,以避免shell注入漏洞。如下面的例子:
from shlex import quote>>> filename = 'somefile; rm -rf ~' # 有這么一個奇怪的文件名 >>> command = 'ls -l {}'.format(filename) >>> print(command) # executed by a shell: boom! ls -l somefile; rm -rf ~ >>> subprocess.run(command, shell=True) # 這時就會有極大的安全隱患>>> command = 'ls -l {}'.format(quote(filename)) # 使用shlex.quote對文件名進行正確的轉義 >>> print(command) ls -l 'somefile; rm -rf ~' >>> subprocess.run(command, shell=True) -
capture_output , 如果capture_output=True,則將捕獲stdout和stderr,調用時內部的Popen對象將自動使用stdout=PIPE和stderr = PIPE創(chuàng)建標準輸出和標準錯誤對象;傳遞stdout和stderr參數時不能同時傳遞capture_output參數。如果希望捕獲并將兩個stream合并為一個,使用stdout=PIPE和stderr = STDOUT。
-
check,如果check=True,并且進程以非零退出代碼退出,則將拋出CalledProcessError異常。
-
input,該參數傳遞給Popen.communicate(),然后傳遞給子進程的stdin。該參數數據類型應為字節(jié)序列(bytes);但如果指定了encoding , errors參數或則 text=True,參數則必須為字符串。使用該參數時,內部Popen對象,將使用stdin = PIPE自動創(chuàng)建該對象,不能同時使用stdin參數。
-
timeout,該參數傳遞給Popen.communicate(),如果指定時間之后子進程仍未結束,子進程將被kill,并拋出TimeoutExpired異常。
-
stdin,stdout和stderr分別指定執(zhí)行程序的標準輸入,標準輸出和標準錯誤文件的file handles。如subprocess.PIPE, subprocess.DEVNULL, 或者 None。此外,stderr可以設定為subprocess.STDOUT,這表示來自子進程的stderr數據應重定向到與stdout相同的file handle中。默認情況下,stdin,stdout和stderr對應的file handle都是以binary的方式打開。
-
encoding, errors , text 。當傳遞encoding, errors參數 或 text=True時,stdin,stdout和stderr對應的file handle以text的模式打開。universal_newlines 和text同義,為了保持向下兼容而保留。默認情況下,文件對象以二進制的方式打開。
-
env,通過傳遞mappings對象,給子進程提供環(huán)境變量,該參數直接傳遞給Popen函數。
-
shell, 如果shell=True,則將通過Shell執(zhí)行指定的命令。當使用shell=True時,shlex.quote() 函數可用于正確地轉義字符串中的空格和Shell元字符。
-
函數返回數據類型為subprocess.CompletedProcess, 該對象包含以下屬性或方法:
- args, 調用該進程的參數,同subprocess.run(args,***) 中的args;
- returncode,當值為0時,代表子進程執(zhí)行成功;負值 -N 指示進程被signal N所終止 (POSIX only); None代表未終止;
- stdout,stderr ,代表子進程的標準輸出和標準錯誤;
- check_returncode(), check子進程是否執(zhí)行成功,若執(zhí)行失敗將拋出異常;
old high level interfaces
run 函數在 Python 3.5 新增,之前使用該模塊的high level interface包括三個函數: call(), check_call(), check_output()。這三個函數參數和subprocess.run()的函數參數含義相同。但需要注意的是,這三個函數的參數列表略微不同,函數signature如下:
- subprocess.call(args, *, stdin=None, stdout=None, stderr=None, shell=False, cwd=None, timeout=None, **other_popen_kwargs)
? 執(zhí)行args參數所指定的程序并等待其完成。當shell=True, 無論子進程執(zhí)行成功與否,返回值為return code;當shell=False,子進程如果執(zhí)行失敗,將會拋出異常;該函數旨在對os.system()進行功能增強,同時易于使用。
-
subprocess.check_call(args, *, stdin=None, stdout=None, stderr=None, shell=False, cwd=None, timeout=None, **other_popen_kwargs)
執(zhí)行args參數所指定的程序并等待其完成,如果子進程返回0,則函數返回;若子進程失敗,則拋出異常;
-
subprocess.check_output(args, *, stdin=None, stderr=None, shell=False, cwd=None, encoding=None, errors=None, universal_newlines=None, timeout=None, text=None, **other_popen_kwargs)
執(zhí)行args參數所指定的程序并返回其輸出,如果子進程執(zhí)行失敗將拋出異常; 該函數的返回值默認為bytes
注意: 請勿在subprocess.call及``subprocess.check_call中使用stdout=PIPE或stderr=PIPE。如果子進程輸出信息過大將會耗盡OS管道緩沖區(qū)的緩沖,該子進程將阻塞; 要禁止這兩個函數的stdout或stderr,可以通過subprocess.DEVNULL`設置。
subprocess.Popen
Popen構造函數
上面四個high level interfaces 底層的進程創(chuàng)建及進程管理實際上都是基于subprocess.Popen類來實現(xiàn),當需要定制化更靈活的進程調用時,這個函數會是一個更好的選擇。首先看該類的構造函數如下:
class subprocess.Popen(args, bufsize=-1, executable=None, stdin=None, stdout=None, stderr=None, preexec_fn=None, shell=False, cwd=None, env=None, universal_newlines=None, startupinfo=None, creationflags=0,start_new_session=False,restore_signals=True,close_fds=True,pass_fds=(), *, encoding=None, errors=None, text=None):pass此時,你會發(fā)現(xiàn)很多熟悉的參數。因為high level function本身大部分參數也確實傳遞給了Popen類。該類的作用即是創(chuàng)建 (fork) 并執(zhí)行 (exec) 子進程。在POSIX,該類使用類似os.execvp()的方式執(zhí)行子進程;在windows上,則使用windows系統(tǒng)的CreateProcess()函數執(zhí)行子進程。Popen構造函數參數十分豐富,除了上面介紹的還有一大堆參數需要注意。close_fds, pass_fds與file handle相關;restore_signals與POSIX信號相關;start_new_session, startupinfo, creationflags則和子進程的創(chuàng)建相關;另外,以下參數可能需要特別關注:
- bufsize, 在創(chuàng)建stdin / stdout / stderr管道文件對象時,bufsize將作為open()函數的相應參數
- 0 代表unbuffered
- 1 代表 line buffered
- 其他正數代表buffer size
- 負數代表使用系統(tǒng)默認的buffer size (io.DEFAULT_BUFFER_SIZE)
-
excutable,這個參數不常見,當shell=True時, 在POSIX上,可使用該參數指定不同于默認的/bin/shshell來執(zhí)行子進程;而當shell=False時,這個用法似乎不太常見,我能想到的一個例子可能如下:
## 以下兩行命令等價 >>> subprocess.run(['bedtools','intersect', '--help']) >>> subprocess.run(['','intersect', '--help'], executable='bedtools')From 官方文檔: executable replaces the program to execute specified by args. However, the original args is still passed to the program. Most programs treat the program specified by args as the command name, which can then be different from the program actually executed.
-
preexec_fn, 該參數可綁定一個callable對象,該對象將在子進程執(zhí)行之前在子進程中調用。需要注意的是,在應用程序中存在線程的情況下,該參數應該避免使用,可能會引發(fā)死鎖。
- cwd, 指定該參數時,函數在執(zhí)行子進程之前將會將工作目錄設置為cwd。
Popen方法與屬性
-
Popen.poll() check子進程是否已終止,如果結束則返回return code,反之返回None
-
Popen.wait(timeout=None)等待子進程終止,如果timeout時間內子進程不結束,則會拋出TimeoutExpired異常
當使用stdout=PIPE 或則 stderr=PIPE時,避免使用該函數,使用``Popen.communicate()`以避免死鎖的發(fā)生。
-
Popen.communicate(input=None, timeout=None)
與進程交互:將input指定數據發(fā)送到stdin;從stdout和stderr讀取數據,直到到達文件末尾,等待進程終止。所以,返回值是一個tuple: (stdout_data, stderr_data)。如果timeout時間內子進程不結束,則會拋出TimeoutExpired異常。其中需要注意的是,捕獲異常之后,可以再次調用該函數,因為子進程并沒有被kill。因此,如果超時結束程序的話,需要現(xiàn)正確kill子進程:
proc = subprocess.Popen(...) try:outs, errs = proc.communicate(timeout=15) except TimeoutExpired:proc.kill()outs, errs = proc.communicate() -
Popen.send_signal(signal) 向子進程發(fā)送信號
-
Popen.terminate() 停止子進程,在POSIX上,實際上即是向子進程發(fā)送SIGTERM信號;在windows上則是調用TerminateProcess()函數
-
Popen.kill() 殺掉子進程,在POSIX上,實際上即是向子進程發(fā)送SIGKILL信號;在windows上則是調用terminate()函數
-
屬性包括.args:子進程命令;.returncode:子進程終止返回值;.pid:子進程進程號;.stdin,.stdout, .stderr分別代表標準輸入輸出,標準錯誤,默認為bytes,這幾個屬性類似于open()函數返回值,是一個可讀的stream對象
異常處理
subprocess模塊共包含三個異常處理類: 基類SubprocessError, 及其兩個子類TimeoutExpired,CalledProcessError,前者在在等待子進程超時時拋出;后者在調用check_call()或check_output()返回非零狀態(tài)值時拋出。他們共同的屬性包括:
- cmd , 該子進程的命令
- output , 子進程所capture的標準輸出 (如調用run()或則check_output()),否則為None
- stdout, output的別名
- stderr, 子進程所capture的標準錯誤 (如調用run()) ,否則為None
TimeoutExpired還包括timeout,指示所設置的timeout的值;CalledProcessError則還包括屬性returncode;
Subprocess 應用
??在官方文檔中給出很多例子指導我們如何使用subprocess替代舊的接口,具體例子如下:
shell 命令行, 比如要實現(xiàn)一個簡單的shell command line 命令 ls -lhrt,可以有以下幾種等價的方式:
## shell cmd ls -lhrt >>> output = check_output(["ls", "-lhrt"]) >>> subprocess.run(['ls', '-lhrt'], stdout=subprocess.PIPE).stdout >>> output = subprocess.Popen(["ls", "-lhrt"], stdout=subprocess.PIPE).communicate()[0]替代os.system(),前面有提到subprocess.call()是為os.system設置的增強版,應用如下:
from subprocess import * try:retcode = call("ls" + " -hrtl", shell=True)if retcode < 0:print("Child was terminated by signal", -retcode, file=sys.stderr)else:print("Child returned", retcode, file=sys.stderr) except OSError as e:print("Execution failed:", e, file=sys.stderr)替代os.spawn*(),該家族包括八個變體,os.spawnl(), os.spawnle(), os.spawnlp(), os.spawnlpe(), os.spawnv(), os.spawnve(), os.spawnvp(), os.spawnvpe(), l和v變體分別代表fixed parameters和variable parameters, p變體函數默認使用環(huán)境變量$PATH尋找program file (如ls, cp),e變體則是函數增加一個env mappings 參數來指定子進程執(zhí)行的環(huán)境變量,不使用當前進程的環(huán)境變量,具體見官方文檔 os.spawn*。官方建議這些函數都可用subprocess替代,如常見的兩個場景如下:
### 場景1 P_NOWAIT pid = os.spawnlp(os.P_NOWAIT, "ls", "ls", "-hlrt") ==> pid = Popen(["/bin/mycmd", "myarg"]).pid### 場景2 retcode = os.spawnlp(os.P_WAIT, "/bin/mycmd", "mycmd", "myarg") ==> retcode = call(["/bin/mycmd", "myarg"])替代os.popen*(),該系列一共包括4個變體,分別是os.popen(), os.popen2(), os.popen3(),os.popen()4,首先需要理解的是os.popen()是基于subprocess.Popen實現(xiàn)的一個方法,用于從一個命令打開一個管道,存在r或w兩種模式。比如:
>>> f = os.popen(cmd='ls -lhrt', mode='r', buffering=-1) # cmd必須是字符串,其以shell的方式執(zhí)行 >>> f.read() 'total 0\n-rw-r--r-- 1 liunianping qukun 8 Jan 29 20:50 test.txt\n' >>> >>> f.close()而剩下三個變體其實不是基于subprocess來實現(xiàn)的,并且功能差別僅僅在于返回值,三者返回值依次是:(child_stdin, child_stdout), (child_stdin, child_stdout, child_stderr), (child_stdin, child_stdout_and_stderr), 因此,我們自然也可以使用subprocess模塊函數來替代它:
### popen2 (child_stdin, child_stdout) = os.popen2(cmd, mode, bufsize) ## ==> p = Popen(cmd, shell=True, bufsize=bufsize,stdin=PIPE, stdout=PIPE, close_fds=True) (child_stdin, child_stdout) = (p.stdin, p.stdout)### popen3 (child_stdin,child_stdout,child_stderr) = os.popen3(cmd, mode, bufsize) ## ==> p = Popen(cmd, shell=True, bufsize=bufsize,stdin=PIPE, stdout=PIPE, stderr=PIPE, close_fds=True) (child_stdin,child_stdout,child_stderr) = (p.stdin, p.stdout, p.stderr)### popen4(child_stdin, child_stdout_and_stderr) = os.popen4(cmd, mode, bufsize) ## ==> p = Popen(cmd, shell=True, bufsize=bufsize,stdin=PIPE, stdout=PIPE, stderr=STDOUT, close_fds=True) (child_stdin, child_stdout_and_stderr) = (p.stdin, p.stdout)其他
subprocess中還提供另外兩個python2.x中 commands模塊中的舊版shell調用功能getstatusoutput和getoutput,查看python源碼可以看到它的實現(xiàn)其實也非常簡單,就是借助subprocess.check_output()函數捕獲shell 命令的輸出,最終返回return_code以及output:
def getstatusoutput(cmd):try:data = check_output(cmd, shell=True, text=True, stderr=STDOUT)exitcode = 0except CalledProcessError as ex:data = ex.outputexitcode = ex.returncodeif data[-1:] == '\n':data = data[:-1]return exitcode, datadef getoutput(cmd):return getstatusoutput(cmd)[1]寫在篇尾
subprocess是基于python2 中popen2模塊發(fā)展而來,專門為替代python中眾多繁雜的子進程創(chuàng)建方法而設計,平時使用的過程中,subprocess.run()以及subprocess.call可以滿足我們大多數的使用需求,但是更深入的了解該package的設計思想可以讓我們更加靈活的控制復雜場景下的子進程任務。
參考
python3 subprocess
PEP324
總結
以上是生活随笔為你收集整理的python subprocess-更优雅的创建子进程的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: R语言入门4---R语言流程控制
- 下一篇: python绘制dotplot