bắt stdout trong thời gian thực từ quy trình con


87

Tôi muốn subprocess.Popen()rsync.exe trong Windows và in stdout bằng Python.

Mã của tôi hoạt động, nhưng nó không bắt được tiến trình cho đến khi quá trình chuyển tệp hoàn tất! Tôi muốn in tiến trình cho từng tệp trong thời gian thực.

Sử dụng Python 3.1 ngay bây giờ vì tôi nghe nói rằng nó sẽ xử lý IO tốt hơn.

import subprocess, time, os, sys

cmd = "rsync.exe -vaz -P source/ dest/"
p, line = True, 'start'


p = subprocess.Popen(cmd,
                     shell=True,
                     bufsize=64,
                     stdin=subprocess.PIPE,
                     stderr=subprocess.PIPE,
                     stdout=subprocess.PIPE)

for line in p.stdout:
    print(">>> " + str(line.rstrip()))
    p.stdout.flush()


1
(Đến từ google?) Tất cả các PIPE sẽ bế tắc khi một trong các bộ đệm của PIPE bị lấp đầy và không được đọc. ví dụ: stdout deadlock khi stderr được lấp đầy. Đừng bao giờ vượt qua một PIPE mà bạn không định đọc.
Nasser Al-Wohaibi

Ai đó có thể giải thích tại sao bạn không thể chỉ đặt stdout thành sys.stdout thay vì subprocess.PIPE?
Mike

Câu trả lời:


96

Một số quy tắc ngón tay cái cho subprocess.

  • Không bao giờ sử dụng shell=True. Nó không cần thiết phải gọi một quá trình shell bổ sung để gọi chương trình của bạn.
  • Khi gọi các tiến trình, các đối số được chuyển xung quanh dưới dạng danh sách. sys.argvtrong python là một danh sách, và như vậy là argvtrong C. Vì vậy, bạn vượt qua một danh sách để Popengọi trình con, không phải là một chuỗi.
  • Đừng chuyển hướng stderrđến a PIPEkhi bạn không đọc nó.
  • Đừng chuyển hướng stdinkhi bạn không viết thư cho nó.

Thí dụ:

import subprocess, time, os, sys
cmd = ["rsync.exe", "-vaz", "-P", "source/" ,"dest/"]

p = subprocess.Popen(cmd,
                     stdout=subprocess.PIPE,
                     stderr=subprocess.STDOUT)

for line in iter(p.stdout.readline, b''):
    print(">>> " + line.rstrip())

Điều đó nói rằng, có khả năng rsync đệm đầu ra của nó khi nó phát hiện ra rằng nó được kết nối với một đường ống thay vì một thiết bị đầu cuối. Đây là hành vi mặc định - khi được kết nối với một đường ống, các chương trình phải xóa rõ ràng stdout để có kết quả thời gian thực, nếu không thư viện C tiêu chuẩn sẽ đệm.

Để kiểm tra điều đó, hãy thử chạy điều này thay thế:

cmd = [sys.executable, 'test_out.py']

và tạo một test_out.pytệp có nội dung:

import sys
import time
print ("Hello")
sys.stdout.flush()
time.sleep(10)
print ("World")

Việc thực thi quy trình con đó sẽ cung cấp cho bạn "Xin chào" và đợi 10 giây trước khi đưa ra "Thế giới". Nếu điều đó xảy ra với mã python ở trên mà không phải với rsync, điều đó có nghĩa là rsyncbản thân nó đang lưu đầu ra vào bộ đệm, vì vậy bạn không may mắn.

Một giải pháp sẽ là kết nối trực tiếp với a pty, sử dụng một cái gì đó như pexpect.


12
shell=Falselà điều đúng đắn khi bạn xây dựng dòng lệnh, đặc biệt là từ dữ liệu do người dùng nhập. Nhưng tuy nhiên shell=Truecũng rất hữu ích khi bạn lấy toàn bộ dòng lệnh từ nguồn đáng tin cậy (ví dụ: mã cứng trong tập lệnh).
Denis Otkidach 22/10/09

10
@Denis Otkidach: Tôi không nghĩ rằng điều đó đảm bảo cho việc sử dụng shell=True. Hãy nghĩ về điều đó - bạn đang gọi một quy trình khác trên hệ điều hành của mình, liên quan đến phân bổ bộ nhớ, sử dụng đĩa, lập lịch trình xử lý, chỉ để chia một chuỗi ! Và một bạn đã tham gia chính mình !! Bạn có thể tách trong python, nhưng dù sao thì việc viết từng tham số riêng biệt sẽ dễ dàng hơn. Ngoài ra, sử dụng một danh sách các phương tiện bạn không cần phải thoát chars vỏ đặc biệt: không gian, ;, >, <, &.. thông số bạn có thể chứa những ký tự và bạn không cần phải lo lắng! Tôi không thể thấy lý do để sử dụng shell=True, thực sự, trừ khi bạn đang chạy một lệnh chỉ trình bao.
nosklo

nosklo, bạn cần phải: p = subprocess.Popen (cmd, stdout = subprocess.PIPE, stderr = subprocess.STDOUT)
Senthil Kumaran

1
@mathtick: Tôi không chắc tại sao bạn lại thực hiện các thao tác đó như các quy trình riêng biệt ... bạn có thể cắt nội dung tệp và trích xuất trường đầu tiên dễ dàng trong python bằng cách sử dụng csvmô-đun. Nhưng như một ví dụ, đường dẫn của bạn trong python sẽ là: p = Popen(['cut', '-f1'], stdin=open('longfile.tab'), stdout=PIPE) ; p2 = Popen(['head', '-100'], stdin=p.stdout, stdout=PIPE) ; result, stderr = p2.communicate() ; print resultLưu ý rằng bạn có thể làm việc với tên tệp dài và ký tự đặc biệt của trình bao mà không cần phải thoát, bây giờ trình bao không liên quan. Ngoài ra, nó nhanh hơn rất nhiều vì có một quá trình ít hơn.
nosklo

11
sử dụng for line in iter(p.stdout.readline, b'')thay vì for line in p.stdouttrong Python 2 nếu không các dòng không được đọc trong thời gian thực ngay cả khi quy trình nguồn không đệm đầu ra của nó.
jfs

41

Tôi biết đây là một chủ đề cũ, nhưng có một giải pháp bây giờ. Gọi rsync với tùy chọn --outbuf = L. Thí dụ:

cmd=['rsync', '-arzv','--backup','--outbuf=L','source/','dest']
p = subprocess.Popen(cmd,
                     stdout=subprocess.PIPE)
for line in iter(p.stdout.readline, b''):
    print '>>> {}'.format(line.rstrip())

3
Điều này hoạt động và nên được ủng hộ để giúp người đọc trong tương lai không phải cuộn qua tất cả hộp thoại ở trên.
VectorVictor

1
@VectorVictor Nó không giải thích điều gì đang xảy ra và tại sao nó lại xảy ra. Nó có thể là chương trình của bạn hoạt động, cho đến khi: 1. bạn thêm preexec_fn=os.setpgrpvào để làm cho chương trình tồn tại tập lệnh mẹ của nó 2. bạn bỏ qua việc đọc từ đường dẫn của quá trình 3. quá trình xuất ra rất nhiều dữ liệu, lấp đầy đường ống 4. bạn bị mắc kẹt trong nhiều giờ , cố gắng tìm ra lý do tại sao chương trình bạn đang chạy lại thoát sau một khoảng thời gian ngẫu nhiên . Câu trả lời từ @nosklo đã giúp tôi rất nhiều.
danuker

15

Trên Linux, tôi đã gặp vấn đề tương tự khi loại bỏ bộ đệm. Cuối cùng tôi đã sử dụng "stdbuf -o0" (hoặc, bỏ bộ đệm từ mong đợi) để loại bỏ bộ đệm PIPE.

proc = Popen(['stdbuf', '-o0'] + cmd, stdout=PIPE, stderr=PIPE)
stdout = proc.stdout

Sau đó, tôi có thể sử dụng select.select trên stdout.

Xem thêm /unix/25372/


2
Đối với bất kỳ ai đang cố gắng lấy stdout mã C từ Python, tôi có thể xác nhận rằng giải pháp này là giải pháp duy nhất phù hợp với tôi. Để rõ ràng hơn, tôi đang nói về việc thêm 'stdbuf', '-o0' vào danh sách lệnh hiện có của tôi trong Popen.
Liều lĩnh,

Cảm ơn bạn! stdbuf -o0được chứng minh là thực sự hữu ích với một loạt các bài kiểm tra pytest / pytest-bdd mà tôi đã viết để tạo ra một ứng dụng C ++ và xác minh rằng nó phát ra một số câu lệnh nhật ký nhất định. Nếu không stdbuf -o0, các bài kiểm tra này cần 7 giây để lấy đầu ra (được đệm) từ chương trình C ++. Bây giờ chúng chạy gần như ngay lập tức!
evadeflow

11

Tùy thuộc vào trường hợp sử dụng, bạn cũng có thể muốn vô hiệu hóa bộ đệm trong chính quy trình con.

Nếu quy trình con là một quy trình Python, bạn có thể thực hiện việc này trước khi gọi:

os.environ["PYTHONUNBUFFERED"] = "1"

Hoặc cách khác, chuyển điều này trong envđối số sang Popen.

Ngược lại, nếu bạn đang sử dụng Linux / Unix, bạn có thể sử dụng stdbufcông cụ này. Ví dụ như:

cmd = ["stdbuf", "-oL"] + cmd

Xem thêm ở đây về stdbufhoặc các tùy chọn khác.


1
Bạn tiết kiệm ngày của tôi, Cảm ơn bạn PYTHONUNBUFFERED = 1
diewland

9
for line in p.stdout:
  ...

luôn chặn cho đến dòng tiếp theo.

Đối với hành vi "thời gian thực", bạn phải làm như sau:

while True:
  inchar = p.stdout.read(1)
  if inchar: #neither empty string nor None
    print(str(inchar), end='') #or end=None to flush immediately
  else:
    print('') #flush for implicit line-buffering
    break

Vòng lặp while còn lại khi process con đóng stdout của nó hoặc thoát. read()/read(-1)sẽ chặn cho đến khi tiến trình con đóng lại hoặc thoát.


1
incharkhông bao giờ được Nonesử dụng if not inchar:thay thế ( read()trả về chuỗi trống trên EOF). btw, Tệ hơn là for line in p.stdoutkhông in được ngay cả các dòng đầy đủ trong thời gian thực trong Python 2 ( for line in iter (p.stdout.readline, '') `có thể được sử dụng thay thế).
jfs

1
Tôi đã thử nghiệm điều này với python 3.4 trên osx, và nó không hoạt động.
qed

1
@qed: for line in p.stdout:hoạt động trên Python 3. Hãy chắc chắn hiểu sự khác biệt giữa ''(chuỗi Unicode) và b''(byte). Xem Python: đọc đầu vào phát trực tuyến từ subprocess.communicate ()
jfs

8

Vấn đề của bạn là:

for line in p.stdout:
    print(">>> " + str(line.rstrip()))
    p.stdout.flush()

bản thân trình lặp có thêm bộ đệm.

Hãy thử làm như sau:

while True:
  line = p.stdout.readline()
  if not line:
     break
  print line

5

Bạn không thể yêu cầu stdout in không có bộ đệm vào một đường ống (trừ khi bạn có thể viết lại chương trình in thành stdout), vì vậy đây là giải pháp của tôi:

Chuyển hướng stdout sang sterr, không được lưu vào bộ đệm. '<cmd> 1>&2'Hãy làm nó. Mở quá trình như sau: myproc = subprocess.Popen('<cmd> 1>&2', stderr=subprocess.PIPE)
Bạn không thể phân biệt với stdout hay stderr, nhưng bạn nhận được tất cả các đầu ra ngay lập tức.

Hy vọng điều này sẽ giúp bất cứ ai giải quyết vấn đề này.


4
Bạn đã thử chưa? Bởi vì nó không làm việc .. Nếu stdout được đệm trong quá trình đó, nó sẽ không được chuyển hướng đến stderr trong cùng một cách nó không được chuyển hướng đến một PIPE hoặc tập tin ..
Filipe Pina

5
Điều này rõ ràng là sai. đệm stdout xảy ra trong chính chương trình. Cú pháp shell 1>&2chỉ thay đổi tệp mà bộ mô tả tệp trỏ đến trước khi khởi chạy chương trình. Bản thân chương trình không thể phân biệt giữa chuyển hướng stdout thành stderr ( 1>&2) hoặc ngược lại ( 2>&1), vì vậy điều này sẽ không ảnh hưởng đến hành vi đệm của chương trình. Và cả hai cách 1>&2cú pháp được diễn giải bởi shell. subprocess.Popen('<cmd> 1>&2', stderr=subprocess.PIPE)sẽ thất bại vì bạn chưa chỉ định shell=True.
Will Manley

Trong trường hợp mọi người sẽ đọc cái này: Tôi đã thử sử dụng stderr thay vì stdout, nó cho thấy cùng một hành vi.
martinthenext

3

Thay đổi stdout từ quá trình rsync để được bỏ bộ đệm.

p = subprocess.Popen(cmd,
                     shell=True,
                     bufsize=0,  # 0=unbuffered, 1=line-buffered, else buffer-size
                     stdin=subprocess.PIPE,
                     stderr=subprocess.PIPE,
                     stdout=subprocess.PIPE)

3
Việc lưu đệm xảy ra ở phía rsync, việc thay đổi thuộc tính bufsize ở phía python sẽ không giúp được gì.
nosklo

14
Đối với bất kỳ ai khác đang tìm kiếm, câu trả lời của nosklo là hoàn toàn sai: hiển thị tiến trình của rsync không được lưu vào bộ đệm; vấn đề thực sự là quy trình con trả về một đối tượng tệp và giao diện trình lặp tệp có bộ đệm bên trong được ghi chép kém ngay cả với bufsize = 0, yêu cầu bạn gọi lại readline () nếu bạn cần kết quả trước khi bộ đệm lấp đầy.
Chris Adams

3

Để tránh bộ nhớ đệm của đầu ra, bạn có thể muốn thử pexpect,

child = pexpect.spawn(launchcmd,args,timeout=None)
while True:
    try:
        child.expect('\n')
        print(child.before)
    except pexpect.EOF:
        break

Tái bút : Tôi biết câu hỏi này khá cũ, vẫn cung cấp giải pháp phù hợp với tôi.

PPS : có câu trả lời này từ một câu hỏi khác


3
    p = subprocess.Popen(command,
                                bufsize=0,
                                universal_newlines=True)

Tôi đang viết GUI cho rsync trong python và có cùng các thăm dò. Vấn đề này đã gây khó khăn cho tôi trong vài ngày cho đến khi tôi tìm thấy nó trong pyDoc.

Nếu Universal_newlines là True, các đối tượng tệp stdout và stderr được mở dưới dạng tệp văn bản trong chế độ dòng mới phổ quát. Các dòng có thể được kết thúc bởi bất kỳ '\ n', quy ước cuối dòng Unix, '\ r', quy ước Macintosh cũ hoặc '\ r \ n', quy ước Windows. Tất cả các biểu diễn bên ngoài này được chương trình Python xem là '\ n'.

Có vẻ như rsync sẽ xuất ra '\ r' khi quá trình dịch diễn ra.


1

Tôi nhận thấy rằng không có đề cập đến việc sử dụng tệp tạm thời làm tệp trung gian. Phần sau giải quyết các vấn đề về bộ đệm bằng cách xuất ra tệp tạm thời và cho phép bạn phân tích cú pháp dữ liệu đến từ rsync mà không cần kết nối với pty. Tôi đã thử nghiệm điều sau trên hộp linux và đầu ra của rsync có xu hướng khác nhau giữa các nền tảng, vì vậy các biểu thức chính quy để phân tích cú pháp đầu ra có thể khác nhau:

import subprocess, time, tempfile, re

pipe_output, file_name = tempfile.TemporaryFile()
cmd = ["rsync", "-vaz", "-P", "/src/" ,"/dest"]

p = subprocess.Popen(cmd, stdout=pipe_output, 
                     stderr=subprocess.STDOUT)
while p.poll() is None:
    # p.poll() returns None while the program is still running
    # sleep for 1 second
    time.sleep(1)
    last_line =  open(file_name).readlines()
    # it's possible that it hasn't output yet, so continue
    if len(last_line) == 0: continue
    last_line = last_line[-1]
    # Matching to "[bytes downloaded]  number%  [speed] number:number:number"
    match_it = re.match(".* ([0-9]*)%.* ([0-9]*:[0-9]*:[0-9]*).*", last_line)
    if not match_it: continue
    # in this case, the percentage is stored in match_it.group(1), 
    # time in match_it.group(2).  We could do something with it here...

nó không phải trong thời gian thực. Tệp không giải quyết được vấn đề bộ đệm ở phía rsync.
jfs 29/07/12

tempfile.TemporaryFile có thể tự xóa để dễ dàng dọn dẹp hơn trong trường hợp ngoại lệ
jfs

3
while not p.poll()dẫn đến vòng lặp vô hạn nếu thoát subprocess thành công với 0, sử dụng p.poll() is Nonethay vì
JFS

Windows có thể cấm để mở tập tin đã mở, vì vậy open(file_name)có thể thất bại
JFS

1
Tôi chỉ tìm thấy câu trả lời này, không may chỉ dành cho Linux, nhưng hoạt động như một nét duyên dáng liên kết Vì vậy, tôi chỉ cần mở rộng lệnh của tôi như sau: command_argv = ["stdbuf","-i0","-o0","-e0"] + command_argvvà gọi: popen = subprocess.Popen(cmd, stdout=subprocess.PIPE) và bây giờ tôi có thể đọc từ mà không cần bất kỳ đệm
Arvid Terzibaschian

0

nếu bạn chạy một cái gì đó như thế này trong một luồng và lưu thuộc tính ffmpeg_time trong một thuộc tính của một phương thức để bạn có thể truy cập nó, nó sẽ hoạt động rất tốt, tôi nhận được kết quả đầu ra như thế này: đầu ra giống như nếu bạn sử dụng luồng trong tkinter

input = 'path/input_file.mp4'
output = 'path/input_file.mp4'
command = "ffmpeg -y -v quiet -stats -i \"" + str(input) + "\" -metadata title=\"@alaa_sanatisharif\" -preset ultrafast -vcodec copy -r 50 -vsync 1 -async 1 \"" + output + "\""
process = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, universal_newlines=True, shell=True)
for line in self.process.stdout:
    reg = re.search('\d\d:\d\d:\d\d', line)
    ffmpeg_time = reg.group(0) if reg else ''
    print(ffmpeg_time)

-1

Trong Python 3, đây là một giải pháp, loại bỏ một lệnh khỏi dòng lệnh và cung cấp các chuỗi được giải mã độc đáo theo thời gian thực khi chúng được nhận.

Người nhận (receiver.py ):

import subprocess
import sys

cmd = sys.argv[1:]
p = subprocess.Popen(cmd, stdout=subprocess.PIPE)
for line in p.stdout:
    print("received: {}".format(line.rstrip().decode("utf-8")))

Ví dụ chương trình đơn giản có thể tạo đầu ra thời gian thực (dummy_out.py ):

import time
import sys

for i in range(5):
    print("hello {}".format(i))
    sys.stdout.flush()  
    time.sleep(1)

Đầu ra:

$python receiver.py python dummy_out.py
received: hello 0
received: hello 1
received: hello 2
received: hello 3
received: hello 4
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.