하위 프로세스에서 실시간으로 stdout 포착


87

subprocess.Popen()Windows에서 rsync.exe 를 만들고 Python에서 stdout을 인쇄하고 싶습니다 .

내 코드는 작동하지만 파일 전송이 완료 될 때까지 진행 상황을 파악하지 못합니다! 각 파일의 진행 상황을 실시간으로 인쇄하고 싶습니다.

IO를 처리하는 것이 더 나을 것이라고 들었으므로 이제 Python 3.1을 사용하십시오.

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
(Google에서 왔습니까?) PIPE의 버퍼 중 하나가 채워지고 읽지 않으면 모든 PIPE가 교착 상태가됩니다. 예 : stderr이 채워질 때 stdout 교착 상태. 읽을 의도가없는 PIPE는 절대 통과하지 마십시오.
Nasser Al-Wohaibi

누군가 stdout을 subprocess.PIPE 대신 sys.stdout으로 설정할 수없는 이유를 설명 할 수 있습니까?
Mike

답변:


97

일부에 대한 엄지 손가락의 규칙 subprocess.

  • 사용 하지 마십시오shell=True . 프로그램을 호출하기 위해 불필요하게 추가 쉘 프로세스를 호출합니다.
  • 프로세스를 호출 할 때 인수는 목록으로 전달됩니다. sys.argv파이썬에서는 목록이고 argvC에서도 마찬가지입니다. 따라서 문자열이 아닌 하위 프로세스를 호출 하기 위해 목록 을 전달 Popen합니다.
  • 리디렉션하지 마십시오 stderrA와 PIPE당신이 그것을 읽을하지 않을 때.
  • 글을 쓰지 않을 stdin때 리디렉션하지 마십시오 .

예:

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

즉, rsync가 터미널 대신 파이프에 연결되어 있음을 감지하면 출력을 버퍼링 할 가능성이 있습니다. 이것이 기본 동작입니다. 파이프에 연결될 때 프로그램은 실시간 결과를 위해 명시 적으로 stdout을 플러시해야합니다. 그렇지 않으면 표준 C 라이브러리가 버퍼링됩니다.

이를 테스트하려면 대신 다음을 실행하십시오.

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

test_out.py다음 내용 으로 파일을 만듭니다 .

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

해당 하위 프로세스를 실행하면 "Hello"가 표시되고 "World"를 제공하기 전에 10 초 동안 기다려야합니다. 즉, 위의하지와 파이썬 코드를 발생하는 경우 rsync, 그 수단 rsync은 운이 그래서 자체가 출력을 버퍼링한다.

해결책은 pty같은 것을 사용하여에 직접 연결 하는 것 pexpect입니다.


12
shell=False특히 사용자가 입력 한 데이터에서 명령 줄을 구성 할 때 옳습니다. 그러나 그럼에도 불구하고 shell=True신뢰할 수있는 소스 (예 : 스크립트에 하드 코딩 된)에서 전체 명령 줄을 가져올 때도 유용합니다.
Denis Otkidach 09 년

10
@Denis Otkidach : 나는 그것이 shell=True. 생각해보십시오. OS에서 메모리 할당, 디스크 사용량, 프로세서 스케줄링과 관련된 다른 프로세스를 호출 하여 문자열분할하는 것입니다 ! 그리고 하나는 당신이 가입했습니다! 파이썬으로 분할 할 수 있지만 어쨌든 각 매개 변수를 개별적으로 작성하는 것이 더 쉽습니다. 또한 수단 목록을 사용하여 당신은 특별한 쉘 문자를 이스케이프 할 필요가 없습니다 : 공백, ;, >, <, &... 귀하의 매개 변수가 그 문자를 포함 할 수 있습니다 당신은 걱정할 필요가 없습니다! shell=True쉘 전용 명령을 실행하지 않는 한 실제로을 사용하는 이유를 알 수 없습니다 .
nosklo

nosklo, 다음과 같아야합니다. p = subprocess.Popen (cmd, stdout = subprocess.PIPE, stderr = subprocess.STDOUT)
Senthil Kumaran

1
@mathtick : 왜 이러한 작업을 별도의 프로세스로 수행하는지 모르겠습니다 csv. 모듈 을 사용하여 파일 내용을 자르고 Python에서 첫 번째 필드를 쉽게 추출 할 수 있습니다 . 그러나 예를 들어 파이썬의 파이프 라인은 다음과 같습니다. p = Popen(['cut', '-f1'], stdin=open('longfile.tab'), stdout=PIPE) ; p2 = Popen(['head', '-100'], stdin=p.stdout, stdout=PIPE) ; result, stderr = p2.communicate() ; print result이제 셸이 관련되지 않았으므로 이스케이프 할 필요없이 긴 파일 이름과 셸 특수 문자로 작업 할 수 있습니다. 또한 프로세스가 하나 적기 때문에 훨씬 빠릅니다.
nosklo

11
Python 2 for line in iter(p.stdout.readline, b'')대신 사용 하지 for line in p.stdout않으면 소스 프로세스가 출력을 버퍼링하지 않더라도 줄을 실시간으로 읽지 않습니다.
jfs

41

나는 이것이 오래된 주제라는 것을 알고 있지만 지금 해결책이 있습니다. --outbuf = L 옵션을 사용하여 rsync를 호출합니다. 예:

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
이것은 작동하며 향후 독자가 위의 모든 대화 상자를 스크롤하는 것을 방지하기 위해 업 보팅되어야합니다.
VectorVictor 2016

1
@VectorVictor 무슨 일이 일어나고 있는지, 왜 일어나고 있는지 설명하지 않습니다. 다음이 될 때까지 프로그램이 작동 할 수 있습니다. 1. preexec_fn=os.setpgrp프로그램이 상위 스크립트를 유지하도록 추가 합니다. 2. 프로세스의 파이프에서 읽기를 건너 뜁니다. 3. 프로세스가 많은 데이터를 출력하여 파이프를 채 웁니다. 4. 몇 시간 동안 갇혀 있습니다. , 실행중인 프로그램이 임의의 시간 후에 종료되는 이유를 알아 내려고합니다 . @nosklo의 답변은 저에게 많은 도움이되었습니다.
danuker

15

Linux에서는 버퍼링을 제거하는 것과 동일한 문제가있었습니다. 마지막으로 "stdbuf -o0"(또는 예상에서 버퍼링 해제)을 사용하여 PIPE 버퍼링을 제거했습니다.

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

그런 다음 stdout에서 select.select를 사용할 수 있습니다.

참조 /unix/25372/


2
Python에서 C 코드 stdout을 가져 오려는 모든 사람을 위해이 솔루션이 나를 위해 일한 유일한 솔루션임을 확인할 수 있습니다. 명확하게 말하면 Popen의 기존 명령 목록에 'stdbuf', '-o0'을 추가하는 것에 대해 이야기하고 있습니다.
Reckless

감사합니다! C ++ 앱을 생성하고 특정 로그 문을 내보내는 지 확인하는 여러 pytest / pytest-bdd 테스트에서 정말 유용한 stdbuf -o0것으로 입증되었습니다 . 이없는 경우 이러한 테스트는 C ++ 프로그램에서 (버퍼링 된) 출력을 가져 오는 데 7 초가 필요했습니다. 이제 그들은 거의 즉시 실행됩니다! stdbuf -o0
evadeflow

11

사용 사례에 따라 하위 프로세스 자체에서 버퍼링을 비활성화 할 수도 있습니다.

하위 프로세스가 Python 프로세스 인 경우 호출 전에 다음을 수행 할 수 있습니다.

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

또는 env인수에 이것을 전달하십시오 Popen.

그렇지 않고 Linux / Unix를 사용하는 경우 stdbuf도구를 사용할 수 있습니다 . 예 :

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

또는 기타 옵션 에 대해서는 여기 를 참조하십시오 stdbuf.


1
당신은 PYTHONUNBUFFERED = 1, 내 하루 감사를 저장
diewland

9
for line in p.stdout:
  ...

다음 줄 바꿈까지 항상 차단됩니다.

"실시간"동작의 경우 다음과 같이해야합니다.

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

while 루프는 자식 프로세스가 표준 출력을 닫거나 종료 할 때 남겨집니다. read()/read(-1)자식 프로세스가 표준 출력을 닫거나 종료 될 때까지 차단됩니다.


1
inchar대신 None사용 if not inchar:하지 않습니다 ( read()EOF에 빈 문자열 반환). btw, for line in p.stdoutPython 2에서 실시간으로 전체 줄을 인쇄하지 않는 것이 더 나쁩니다 ( for line in iter (p.stdout.readline, '')`대신 사용할 수 있음).
jfs

1
나는 이것을 osx에서 python 3.4로 테스트했지만 작동하지 않습니다.
qed

1
@qed : for line in p.stdout:Python 3에서 작동합니다. ''(유니 코드 문자열)과 b''(바이트) 의 차이점을 이해해야합니다 . 참조 파이썬 : subprocess.communicate 입력을 스트리밍 읽기 ()
JFS

8

당신의 문제는 :

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

반복자 자체에는 추가 버퍼링이 있습니다.

다음과 같이 시도하십시오.

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

5

stdout으로 버퍼링되지 않은 상태로 파이프에 인쇄 할 수 없습니다 (표준 출력으로 인쇄하는 프로그램을 다시 작성할 수없는 경우), 그래서 여기 내 해결책이 있습니다 :

stdout을 버퍼링되지 않은 sterr로 리디렉션합니다. '<cmd> 1>&2'해야합니다. 다음과 같이 프로세스를 엽니 다. myproc = subprocess.Popen('<cmd> 1>&2', stderr=subprocess.PIPE)
stdout 또는 stderr과 구별 할 수 없지만 모든 출력이 즉시 표시됩니다.

이것이이 문제를 해결하는 데 도움이되기를 바랍니다.


4
시도해 보셨습니까? 작동하지 않기 때문에 .. stdout이 해당 프로세스에서 버퍼링되면 PIPE 또는 파일로 리디렉션되지 않는 것과 같은 방식으로 stderr로 리디렉션되지 않습니다 ..
Filipe Pina

5
이것은 명백한 잘못입니다. stdout 버퍼링은 프로그램 자체 내에서 발생합니다. 쉘 구문 1>&2은 프로그램을 시작하기 전에 파일 설명자가 가리키는 파일을 변경합니다. 프로그램 자체는 stdout을 stderr ( 1>&2) 또는 그 반대 ( 2>&1)로 리디렉션하는 것을 구별 할 수 없으므로 프로그램 의 버퍼링 동작에 영향을 미치지 않으며 어떤 방식 으로든 1>&2구문이 쉘에 의해 해석됩니다. subprocess.Popen('<cmd> 1>&2', stderr=subprocess.PIPE)을 지정하지 않았기 때문에 실패합니다 shell=True.
윌 맨리

사람들이 이것을 읽는 경우 : stdout 대신 stderr을 사용해 보았지만 똑같은 동작을 보여줍니다.
martinthenext

3

rsync 프로세스에서 stdout을 버퍼링 해제로 변경하십시오.

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
버퍼링은 rsync 측에서 발생하므로 파이썬 측에서 bufsize 속성을 변경해도 도움이되지 않습니다.
nosklo

14
검색하는 다른 사람에게는 nosklo의 대답이 완전히 잘못되었습니다. rsync의 진행률 표시는 버퍼링되지 않습니다. 실제 문제는 하위 프로세스가 파일 객체를 반환하고 파일 반복기 인터페이스에 bufsize = 0 인 경우에도 내부 버퍼가 잘못 문서화되어 있으므로 버퍼가 채워지기 전에 결과가 필요한 경우 반복적으로 readline ()을 호출해야한다는 것입니다.
Chris Adams 2012

3

출력 캐싱을 피하기 위해 pexpect를 시도해 볼 수 있습니다.

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

추신 : 나는이 질문이 꽤 오래되었고 여전히 나를 위해 일한 솔루션을 제공한다는 것을 알고 있습니다.

PPS : 다른 질문에서이 답변을 받았습니다.


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

파이썬에서 rsync에 대한 GUI를 작성하고 있으며 동일한 probelm이 있습니다. 이 문제는 pyDoc에서 이것을 찾을 때까지 며칠 동안 나를 괴롭 혔습니다.

universal_newlines가 True이면 파일 객체 stdout 및 stderr은 범용 줄 바꿈 모드에서 텍스트 파일로 열립니다. 줄은 '\ n', Unix 줄 끝 규칙, '\ r', 이전 Macintosh 규칙 또는 '\ r \ n', Windows 규칙 중 하나로 끝날 수 있습니다. 이러한 모든 외부 표현은 Python 프로그램에서 '\ n'으로 표시됩니다.

번역이 진행 중일 때 rsync가 '\ r'을 출력하는 것 같습니다.


1

임시 파일을 중간으로 사용하는 것에 대한 언급이 없다는 것을 알았습니다. 다음은 임시 파일로 출력하여 버퍼링 문제를 해결하고 pty에 연결하지 않고 rsync에서 오는 데이터를 구문 분석 할 수 있도록합니다. Linux 상자에서 다음을 테스트했으며 rsync의 출력은 플랫폼마다 다른 경향이 있으므로 출력을 구문 분석하는 정규식이 다를 수 있습니다.

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...

실시간이 아닙니다. 파일은 rsync 측의 버퍼링 문제를 해결하지 못합니다.
jfs

tempfile.TemporaryFile은 예외의 경우 더 쉽게 정리할 수 있도록 자신을 삭제할 수 있습니다
jfs

3
while not p.poll()하위 프로세스가 0으로 성공적으로 종료되면 무한 루프로 이어지고 p.poll() is None대신 사용
jfs

Windows가 이미 열린 파일을 열지 못하도록 open(file_name)할 수 있으므로 실패 할 수 있습니다.
jfs

1
난 그냥 불행하게도 단지 리눅스의 경우,이 답을 발견하지만, 매력처럼 작동 링크를 다음과 같이 난 그냥 내 명령을 확장 그래서 : command_argv = ["stdbuf","-i0","-o0","-e0"] + command_argv및 전화 : popen = subprocess.Popen(cmd, stdout=subprocess.PIPE) 지금은 버퍼링없이 읽을 수 있습니다
의 Arvid Terzibaschian에게

0

스레드에서 이와 같은 것을 실행하고 메소드의 속성에 ffmpeg_time 속성을 저장하여 액세스 할 수 있다면 다음과 같은 출력을 얻을 수 있습니다 . 출력은 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

Python 3에서 다음은 명령 줄에서 명령을 내리고 수신 된대로 멋지게 디코딩 된 문자열을 실시간으로 전달하는 솔루션입니다.

수신기 ( 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")))

실시간 출력을 생성 할 수있는 간단한 프로그램의 예 ( dummy_out.py) :

import time
import sys

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

산출:

$python receiver.py python dummy_out.py
received: hello 0
received: hello 1
received: hello 2
received: hello 3
received: hello 4
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.