2013-08-20 34 views
23

Tôi có một công cụ dòng lệnh (trên thực tế, một số) rằng tôi đang viết một trình bao bọc trong Python.Python: subprocess.call, stdout thành file, stderr để tập tin, hiển thị stderr trên màn hình trong thời gian thực

Công cụ này thường được sử dụng như thế này:

$ path_to_tool -option1 -option2 > file_out 

Người dùng được đầu ra bằng văn bản cho file_out, và cũng có thể nhìn thấy thông điệp tình trạng khác nhau của công cụ này là nó đang chạy.

Tôi muốn sao chép hành vi này, đồng thời ghi nhật ký stderr (thông báo trạng thái) vào tệp.

Những gì tôi có điều này là:

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

này hoạt động tốt TRỪ stderr mà không được ghi vào màn hình. Tôi có thể thêm mã để in nội dung của log_file vào màn hình của khóa học, nhưng sau đó người dùng sẽ thấy nó sau khi mọi thứ được thực hiện thay vì trong khi nó đang xảy ra.

Để tóm tắt, hành vi mong muốn là:

  1. cuộc gọi sử dụng(), hoặc tiến trình con()
  2. stdout trực tiếp vào một tập tin
  3. stderr trực tiếp vào một tập tin, trong khi cũng viết stderr lên màn ảnh trong thời gian thực như thể công cụ đã được gọi trực tiếp từ dòng lệnh.

Tôi có cảm giác mình đang thiếu thứ gì đó thực sự đơn giản, hoặc điều này phức tạp hơn nhiều so với những gì tôi nghĩ ... cảm ơn vì sự giúp đỡ nào!

CHỈNH SỬA: điều này chỉ cần hoạt động trên Linux.

+0

Liệu mã của bạn cần phải làm việc trên Windows (hoặc các nền tảng khác không POSIXy)? Nếu không, có một câu trả lời dễ dàng hơn. – abarnert

+0

Nó không cần phải! –

+0

liên quan: [Python subprocess có đầu ra của trẻ em để tập tin và thiết bị đầu cuối?] (Http://stackoverflow.com/q/4984428/4279) – jfs

Trả lời

52

Bạn có thể thực hiện việc này với subprocess, nhưng không hề nhỏ. Nếu bạn nhìn vào các tài liệu Frequently Used Arguments trong tài liệu, bạn sẽ thấy rằng bạn có thể vượt qua PIPE làm đối số stderr, tạo một đường ống mới, chuyển một bên của đường ống đến quy trình con và làm cho mặt kia có sẵn để sử dụng thuộc tính stderr. *

Vì vậy, bạn sẽ cần phải phục vụ ống đó, ghi vào màn hình và vào tệp. Nói chung, nhận được các chi tiết phù hợp cho điều này là rất phức tạp. ** Trong trường hợp của bạn, chỉ có một ống, và bạn đang lập kế hoạch phục vụ nó đồng bộ, vì vậy nó không phải là xấu.

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() 

(Lưu ý rằng có một số vấn đề sử dụng for line in proc.stderr: -basically, nếu những gì bạn đang đọc hóa ra không phải là dòng đệm vì lý do bất kỳ, bạn có thể ngồi xung quanh chờ đợi một dòng mới mặc dù có thực sự là một nửa Bạn có thể đọc các khối dữ liệu để xử lý, ví dụ: read(128) hoặc thậm chí read(1), để có được dữ liệu trơn tru hơn nếu cần. Nếu bạn thực sự nhận được mọi byte ngay khi nó đến và có thể không đủ khả năng chi phí của read(1), bạn sẽ cần đặt ống ở chế độ không chặn và đọc không đồng bộ.)


Nhưng nếu bạn đang sử dụng Unix, có thể đơn giản hơn khi sử dụng lệnh tee để thực hiện điều đó cho bạn.

Để có giải pháp dơ bẩn & nhanh chóng, bạn có thể sử dụng vỏ để đi qua nó. Một cái gì đó như thế này:

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

Nhưng tôi không muốn gỡ lỗi đường ống vỏ; chúng ta hãy làm điều đó bằng Python, như 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() 

Cuối cùng, có một tá hoặc nhiều giấy gói cao cấp xung quanh subprocesses và/hoặc vỏ trên PyPI- sh, shell, shell_command, shellout, iterpipes, sarge, cmd_utils, commandwrapper, v.v. Tìm kiếm "shell", "subprocess", "process", "command line", v.v ... và tìm một thứ bạn thích làm cho vấn đề tầm thường.


Nếu bạn cần thu thập cả stderr và stdout thì sao?

Cách dễ dàng để thực hiện việc này là chuyển hướng người này sang người khác, như Sven Marnach gợi ý trong một nhận xét. Chỉ cần thay đổi các thông số Popen như thế này:.

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

Và sau đó ở khắp mọi nơi bạn sử dụng tool.stderr, sử dụng tool.stdout thay-ví dụ, cho ví dụ cuối cùng:

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

Nhưng điều này có một số cân bằng. Rõ ràng nhất, trộn hai dòng với nhau có nghĩa là bạn không thể đăng nhập stdout để file_out và stderr để log_file, hoặc sao chép stdout để stdout của bạn và stderr để stderr của bạn. Nhưng nó cũng có nghĩa là thứ tự có thể không xác định - nếu tiến trình con luôn ghi hai dòng vào stderr trước khi viết bất cứ thứ gì để stdout, bạn có thể sẽ nhận được một loạt các stdout giữa hai dòng đó khi bạn trộn các dòng. Và nó có nghĩa là họ phải chia sẻ chế độ đệm của stdout, vì vậy nếu bạn dựa vào thực tế là linux/glibc đảm bảo stderr được line-buffered (trừ khi subprocess thay đổi rõ ràng nó), điều đó có thể không còn đúng nữa.


Nếu bạn cần xử lý riêng hai quy trình, việc này sẽ khó khăn hơn. Trước đó, tôi nói rằng phục vụ các đường ống trên bay là dễ dàng miễn là bạn chỉ có một đường ống và có thể phục vụ nó đồng bộ. Nếu bạn có hai đường ống, điều đó rõ ràng là không còn đúng nữa. Hãy tưởng tượng bạn đang chờ đợi trên tool.stdout.read() và dữ liệu mới đến từ tool.stderr. Nếu có quá nhiều dữ liệu, nó có thể làm cho đường ống tràn và quá trình con để chặn. Nhưng ngay cả khi điều đó không xảy ra, bạn rõ ràng sẽ không thể đọc và đăng nhập dữ liệu stderr cho đến khi một cái gì đó đến từ stdout.

Nếu bạn sử dụng giải pháp đường ống thông qua tee, tránh được vấn đề ban đầu… nhưng chỉ bằng cách tạo dự án mới tồi tệ. Bạn có hai trường hợp tee và trong khi bạn đang gọi communicate trên một bản sao, một trường hợp khác đang ngồi chờ đợi mãi mãi.

Vì vậy, dù bằng cách nào, bạn cũng cần một số loại cơ chế không đồng bộ. Bạn có thể làm điều này với chủ đề, một lò phản ứng select, giống như gevent, v.v.

Dưới đây là một ví dụ nhanh chóng và bẩn:

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() 

Tuy nhiên, có một số trường hợp cạnh nơi mà sẽ không hoạt động. (Vấn đề là thứ tự mà SIGCHLD và SIGPIPE/EPIPE/EOF đến. Tôi không nghĩ bất kỳ thứ gì sẽ ảnh hưởng đến chúng tôi ở đây, vì chúng tôi không gửi bất kỳ thông tin nào ... nhưng đừng tin tôi thông qua và/hoặc kiểm tra.) Hàm subprocess.communicate từ 3.3+ nhận được tất cả các chi tiết khó sử dụng. Nhưng bạn có thể thấy nó đơn giản hơn rất nhiều khi sử dụng một trong các trình cài đặt wrapper async-subprocess mà bạn có thể tìm thấy trên PyPI và ActiveState, hoặc thậm chí các công cụ con của quy trình con từ một khung công tác không chính thức như Twisted.


* Các tài liệu không thực sự giải thích những gì ống là, như thể họ mong đợi bạn là một tay Unix C cũ ... Nhưng một số ví dụ, đặc biệt là trong phần Replacing Older Functions with the subprocess Module, cho thấy làm thế nào họ đang được sử dụng và nó khá đơn giản.

** Phần cứng là giải trình tự hai hoặc nhiều ống đúng cách. Nếu bạn chờ đợi trên một đường ống, người kia có thể tràn và chặn, ngăn cản sự chờ đợi của bạn trên một trong những khác từ bao giờ kết thúc. Cách dễ dàng nhất để giải quyết vấn đề này là tạo một luồng để phục vụ từng đường ống. (Trên hầu hết các nền tảng * nix, bạn có thể sử dụng lò phản ứng select hoặc poll để thay thế, nhưng làm cho nền tảng chéo đó trở nên khó khăn một cách đáng kinh ngạc.) The source cho mô-đun, đặc biệt là communicate và người trợ giúp của nó. (Tôi liên kết với 3,3, bởi vì trong các phiên bản trước đó, communicate chính nó nhận được một số điều quan trọng sai ...) Đây là lý do tại sao, bất cứ khi nào có thể, bạn muốn sử dụng communicate nếu bạn cần nhiều hơn một đường ống. Trong trường hợp của bạn, bạn không thể sử dụng communicate, nhưng may mắn thay bạn không cần nhiều hơn một đường ống.

+1

Rất hữu ích, cảm ơn. Ý của bạn là viết p1 và p2? –

+0

@ user2063292: Xin lỗi, đó là 'công cụ' và' tee'. Theo mã mẫu một chút quá chặt chẽ. :) Cảm ơn bạn đã bắt nó. – abarnert

+0

Có phải '2 |' được gọi là stderr ống? Nó không có trong shell POSIX. –

0

Tôi nghĩ rằng những gì bạn đang tìm kiếm là một cái gì đó như:

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

Để có đầu ra/log ghi vào một tập tin tôi sẽ sửa đổi cmdline tôi để bao gồm chuyển hướng thông thường, vì nó sẽ được thực hiện trên một đồng bằng linux bash/shell. Ví dụ: tôi sẽ thêm tee vào dòng lệnh: cmdline += ' | tee -a logfile.txt'

Hy vọng điều đó sẽ hữu ích.

0

tôi đã thực hiện một vài thay đổi câu trả lời @ abarnert cho Python 3. Điều này dường như làm việc:

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() 
Các vấn đề liên quan