2013-08-20 27 views
23

我有一個命令行工具(實際上是幾個),我正在爲Python編寫一個包裝器。Python:subprocess.call,標準輸出到文件,stderr到文件,實時在屏幕上顯示stderr

的工具一般是用這樣的:

$ path_to_tool -option1 -option2 > file_out 

用戶得到寫入file_out輸出,並且還能夠看到工具的各種狀態信息,因爲它正在運行。

我想複製此行爲,同時還將stderr(狀態消息)記錄到文件中。

我有這樣的:

from subprocess import call 
call(['path_to_tool','-option1','option2'], stdout = file_out, stderr = log_file) 

這工作得很好除非該標準錯誤不被寫入到屏幕上。 我可以添加代碼來將log_file的內容打印到屏幕當然,但是用戶在完成所有事情之後會看到它,而不是在它發生的時候。

總括來說,所需的行爲是:

  1. 使用()調用,或子()
  2. 直接標準輸出到文件
  3. 直接錯誤輸出到一個文件,同時還寫錯誤輸出到屏幕實時彷彿 工具直接從命令行調用。

我有一種感覺,我要麼丟失了一些非常簡單的東西,要麼比我想像的要複雜得多...感謝您的幫助!

編輯:這隻需要在Linux上工作。

+0

請問你的代碼需要在Windows(或其他非POSIXy平臺)工作的?如果不是,則有一個更簡單的答案。 – abarnert

+0

它不需要! –

+0

相關:[Python子進程獲取兒童輸出到文件和終端?](http://stackoverflow.com/q/4984428/4279) – jfs

回答

52

可以做到這一點與subprocess,但它不是微不足道的。如果您查看文檔中的Frequently Used Arguments,您會發現可以通過PIPE作爲stderr參數,該參數創建一個新管道,將管道的一端傳遞給子進程,並使另一端可用作stderr屬性。*

因此,您將需要服務該管道,寫入屏幕和文件。一般情況下,爲此獲取詳細信息非常棘手。**在您的情況下,只有一個管道,並且您計劃同步進行維護,所以沒有那麼糟糕。

import subprocess 
proc = subprocess.Popen(['path_to_tool', '-option1', 'option2'], 
         stdout=file_out, stderr=subprocess.PIPE) 
for line in proc.stderr: 
    sys.stdout.write(line) 
    log_file.write(line) 
proc.wait() 

(注意有使用for line in proc.stderr:一些問題-basically,如果你正在讀證明不是要行緩衝以任何理由,你可以坐在那裏等待換行符,即使實際上有一半需要處理一行數據,如果需要,你可以一次讀取數據塊,例如read(128),甚至可以使用read(1)來獲得數據更加平滑的數據,如果你需要一到達每一個字節就可以得到數據,並且可以承擔read(1)的費用,您需要將管道置於非阻塞模式並異步讀取。)


但是,如果您使用的是Unix,使用tee命令可能會更簡單。

對於快速的&骯髒的解決方案,您可以使用shell來穿過它。這樣的事情:

subprocess.call('path_to_tool -option1 option2 2|tee log_file 1>2', shell=True, 
       stdout=file_out) 

但我不想調試殼管道;讓我們做它在Python,如圖in the docs

tool = subprocess.Popen(['path_to_tool', '-option1', 'option2'], 
         stdout=file_out, stderr=subprocess.PIPE) 
tee = subprocess.Popen(['tee', 'log_file'], stdin=tool.stderr) 
tool.stderr.close() 
tee.communicate() 

最後,有十幾個或更多的圍繞子流程和/或PyPI- shshellshell_commandshellout外殼更高層次的包裝,搜索「shell」,「subprocess」,「process」,「command line」等,找到一個你喜歡的,這使得問題變得微不足道。


如果您需要收集stderr和stdout,該怎麼辦?

最簡單的方法就是將其中一個重定向到另一個,就像Sven Marnach在評論中提出的那樣。只要改變Popen參數是這樣的:

tool = subprocess.Popen(['path_to_tool', '-option1', 'option2'], 
         stdout=subprocess.PIPE, stderr=subprocess.STDOUT) 

然後到處使用tool.stderr,使用tool.stdout代替-e.g,對於最後一個例子:

tee = subprocess.Popen(['tee', 'log_file'], stdin=tool.stdout) 
tool.stdout.close() 
tee.communicate() 

但是這有一定的權衡。最明顯的是,將兩個流混合在一起意味着您不能將stdout記錄到file_out和stderr以log_file,或將stdout複製到stdout和stderr到您的stderr。但是這也意味着排序可能是非確定性的 - 如果在將任何內容寫入stdout之前,子進程總是寫兩行到stderr,那麼一旦混合了這些流,您可能會在這兩行之間得到一堆stdout。這意味着他們必須共享stdout的緩衝模式,所以如果你依賴的是linux/glibc保證stderr被行緩衝(除非子進程明確地改變它),這可能不再是真實的。


如果您需要分開處理這兩個過程,則會變得更加困難。早些時候,我說過,只要你只有一根管道並且可以同步維修,就可以輕鬆地維修管道。如果你有兩個管道,那顯然不再是真的。想象一下,你正在等待tool.stdout.read(),新數據來自tool.stderr。如果數據太多,可能會導致管道溢出並阻塞子進程。但即使這種情況沒有發生,您顯然將無法讀取並記錄stderr數據,直到從stdout中輸入內容爲止。

如果您使用pipe-through-tee解決方案,那就避免了最初的問題......但只能通過創建一個同樣糟糕的新項目。你有兩個tee實例,當你打電話給communicate時,另一個坐在一旁等待。

因此,無論哪種方式,都需要某種異步機制。你可以做到這一點是與線程,select反應堆,如gevent,等等。

這裏有一個快速和骯髒的例子:

proc = subprocess.Popen(['path_to_tool', '-option1', 'option2'], 
         stdout=subprocess.PIPE, stderr=subprocess.PIPE) 
def tee_pipe(pipe, f1, f2): 
    for line in pipe: 
     f1.write(line) 
     f2.write(line) 
t1 = threading.Thread(target=tee_pipe, args=(proc.stdout, file_out, sys.stdout)) 
t2 = threading.Thread(target=tee_pipe, args=(proc.stderr, log_file, sys.stderr)) 
t3 = threading.Thread(proc.wait) 
t1.start(); t2.start(); t3.start() 
t1.join(); t2.join(); t3.join() 

然而,也有一些邊緣情況下,這是行不通的。 (問題在於SIGCHLD和SIGPIPE/EPIPE/EOF到達的順序,我不認爲這會影響到我們,因爲我們沒有發送任何輸入信息......但是不要相信我通過和/或測試)。從3.3+的subprocess.communicate函數獲得所有的細節。但是您可能會發現使用PyPI和ActiveState上可以找到的一個異步子進程包裝器實現,或者甚至是像Twisted這樣的完整異步框架中的子進程內容,都會更加簡單。


*的文檔並不真正說明什麼管道是,彷彿他們希望你是一個老Unix下C手......但一些例子,特別是在Replacing Older Functions with the subprocess Module部分,展示他們如何是使用,而且非常簡單。

**困難的部分是正確排序兩個或多個管道。如果你等待一個管道,另一個可能會溢出並阻塞,從而阻止你等待另一個管道完成。解決這個問題的唯一簡單方法是創建一個線程來服務每個管道。 (在大多數* nix平臺上,您可以使用selectpoll電抗器,但是使該跨平臺非常困難。)The source模塊,特別是communicate及其幫助程序顯示如何執行此操作。 (我鏈接到3.3,因爲在早期版本中,communicate本身有一些重要的錯誤...)這就是爲什麼,只要有可能,如果您需要多個管道,則要使用communicate。在你的情況下,你不能使用communicate,但幸運的是你不需要多個管道。

+1

非常有幫助,謝謝。你的意思是寫p1和p2嗎? –

+0

@ user2063292:對不起,這是'tool'和'tee'。遵循示例代碼太緊密。 :)謝謝你抓到它。 – abarnert

+0

'2 |'是否應該標記爲stderr?它不在POSIX shell中。 –

0

我想你正在尋找的是這樣的:

import sys, subprocess 
p = subprocess.Popen(cmdline, 
        stdout=sys.stdout, 
        stderr=sys.stderr) 

要使輸出/日誌寫入一個文件我想修改我的cmdline包括通常的重定向,因爲它會在一個普通的做linux bash/shell。例如,我會在命令行中追加teecmdline += ' | tee -a logfile.txt'

希望有所幫助。

0

我不得不做出的Python 3. @ abarnert的回答一些變化這似乎是工作:

def tee_pipe(pipe, f1, f2): 
    for line in pipe: 
     f1.write(line) 
     f2.write(line) 

proc = subprocess.Popen(["/bin/echo", "hello"], 
         stdout=subprocess.PIPE, 
         stderr=subprocess.PIPE) 

# Open the output files for stdout/err in unbuffered mode. 
out_file = open("stderr.log", "wb", 0) 
err_file = open("stdout.log", "wb", 0) 

stdout = sys.stdout 
stderr = sys.stderr 

# On Python3 these are wrapped with BufferedTextIO objects that we don't 
# want. 
if sys.version_info[0] >= 3: 
    stdout = stdout.buffer 
    stderr = stderr.buffer 

# Start threads to duplicate the pipes. 
out_thread = threading.Thread(target=tee_pipe, 
           args=(proc.stdout, out_file, stdout)) 
err_thread = threading.Thread(target=tee_pipe, 
           args=(proc.stderr, err_file, stderr)) 

out_thread.start() 
err_thread.start() 

# Wait for the command to finish. 
proc.wait() 

# Join the pipe threads. 
out_thread.join() 
err_thread.join() 
相關問題