从子进程实时捕获标准输出


87

我想subprocess.Popen()在Windows中使用rsync.exe,并在Python中打印标准输出。

我的代码可以运行,但是直到文件传输完成后才能捕获进度!我想实时打印每个文件的进度。

既然我听说使用Python 3.1,现在应该会更好地处理IO。

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 2014年

有人可以解释为什么您不能仅将stdout设置为sys.stdout而不是subprocess.PIPE吗?
Mike Mike

Answers:


96

一些经验法则subprocess

  • 切勿使用shell=True。它不必要地调用额外的shell进程来调用您的程序。
  • 调用进程时,参数作为列表传递。sys.argv在python中是一个列表,argv在C中也是如此。因此,您将列表传递给Popen来调用子流程,而不是字符串。
  • 不阅读时不要重定向stderr到a 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”,并等待10秒钟后才能给出“ World”。如果上述情况发生在上面的python代码上,而不是发生rsync,则意味着rsync它本身正在缓冲输出,因此您很不走运。

一种解决方案是pty使用类似的方法直接连接到pexpect


12
shell=False当您构造命令行(尤其是根据用户输入的数据)时,这是对的。但是,shell=True当您从受信任的来源(例如,脚本中的硬编码)获取整个命令行时,它也很有用。
Denis Otkidach 09年

10
@Denis Otkidach:我认为不应该使用shell=True。考虑一下-您正在OS上调用另一个进程,涉及内存分配,磁盘使用,处理器调度,只是为了分割字符串!还有一个你加入了自己!您可以在python中拆分,但是无论如何要更容易地分别编写每个参数。同样,使用列表意味着您不必转义特殊的shell字符:spaces ;><&..。您的参数可以包含这些字符,您不必担心!shell=True除非您正在运行仅shell命令,否则我看不出使用的理由。
09年

Nosklo,应为:p = subprocess.Popen(cmd,stdout = subprocess.PIPE,stderr = subprocess.STDOUT)
Senthil Kumaran,2009年

1
@mathtick:我不确定为什么您将这些操作作为单独的进程进行...您可以使用csv模块在Python中轻松剪切文件内容并提取第一个字段。但作为示例,您在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请注意,由于无需使用shell,因此您可以使用长文件名和shell特殊字符而不必进行转义。另外,由于减少了流程,因此速度更快。
nosklo 2010年

11
使用for line in iter(p.stdout.readline, b'')代替for line in p.stdoutPython 2,否则即使源进程没有缓冲其输出,行也不会实时读取。
jfs 2013年

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

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

然后,我可以在标准输出上使用select.select。

另请参阅/unix/25372/


2
对于任何尝试从Python获取C代码标准输出的人,我都可以确认该解决方案是唯一对我有用的解决方案。明确地说,我正在谈论将'stdbuf','-o0'添加到我在Popen中的现有命令列表中。
鲁ck

谢谢!stdbuf -o0事实证明,在我编写的一组pytest / pytest-bdd测试中确实很有用,这些测试产生了一个C ++应用程序并验证它发出了某些日志语句。不使用stdbuf -o0,这些测试需要7秒钟才能从C ++程序获取(缓冲的)输出。现在它们几乎立即运行!
evadeflow '19

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上返回空字符串)。顺便说一句,更糟糕的for line in p.stdout是在Python 2中甚至无法实时打印整行for line in (可以改用iter(p.stdout.readline,'')`)。
jfs

1
我已经在osx上使用python 3.4测试了它,但是它不起作用。
QED

1
@qed:for line in p.stdout:在Python 3上有效。请务必了解''(Unicode字符串)和b''(字节)之间的区别。见的Python:读取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的程序),所以这是我的解决方案:

将stdout重定向到未缓冲的sterr。 '<cmd> 1>&2'应该这样做。按如下所示打开过程:myproc = subprocess.Popen('<cmd> 1>&2', stderr=subprocess.PIPE)
您无法与stdout或stderr区分开,但是会立即获得所有输出。

希望这对解决这个问题的任何人有帮助。


4
你试过了吗?因为它不工作。如果标准输出是在这个过程中缓冲,也不会被重定向到stderr在它不重定向到一个管道或文件的方式..
菲利普翩

5
这是完全错误的。stdout缓冲发生在程序本身内。Shell语法1>&2只是在启动程序之前更改文件描述符指向的文件。程序本身无法区分将stdout重定向到stderr(1>&2)还是反之(a 2>&1),因此这不会对程序的缓冲行为产生影响1>&2subprocess.Popen('<cmd> 1>&2', stderr=subprocess.PIPE)会因为你没有指定而失败shell=True
Will Manley

万一人们会读到这个:我尝试使用stderr代替stdout,它显示出完全相同的行为。
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端,在python端更改bufsize属性无济于事。
nosklo

14
对于其他正在搜索的人,nosklo的答案是完全错误的:rsync的进度显示未缓冲;真正的问题是子进程返回一个文件对象,并且文件迭代器接口的内部缓冲区即使bufsize = 0也记录得很差,如果在缓冲区填充之前需要结果,则需要重复调​​用readline()。
克里斯·亚当斯

3

为了避免缓存输出,您可以尝试使用pexpect,

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

PS:我知道这个问题已经很老了,仍然提供了对我有用的解决方案。

PPS:从另一个问题得到了这个答案


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

我在python中为rsync编写GUI,并且具有相同的探针。这个问题困扰了我好几天,直到我在pyDoc中找到它为止。

如果Universal_newlines为True,则在通用换行模式下,文件对象stdout和stderr将作为文本文件打开。行可以由Unix行尾约定“ \ n”,旧的Macintosh约定“ \ r”或Windows约定“ \ r \ n”中的任何一个终止。所有这些外部表示在Python程序中都被视为“ \ n”。

似乎在进行翻译时rsync将输出“ \ r”。


1

我注意到,没有提到将临时文件用作中间文件。以下内容通过输出到临时文件来解决缓冲问题,并允许您解析来自rsync的数据而无需连接到pty。我在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 2012年

tempfile.TemporaryFile可以删除自身,以便在发生异常情况时更轻松地进行清理
jfs 2012年

3
while not p.poll()如果子流程成功退出并导致0,则会导致无限循环,请p.poll() is None改用
jfs

Windows可能禁止打开已打开的文件,因此open(file_name)可能会失败
jfs

1
我刚刚找到了这个答案,不幸的是,仅适用于linux,但它像一个超级链接一样工作, 所以我只按如下方式扩展了我的命令: command_argv = ["stdbuf","-i0","-o0","-e0"] + command_argv并调用:popen = subprocess.Popen(cmd, stdout=subprocess.PIPE) 现在我可以在没有任何缓冲的情况下进行读取了
Arvid Terzibaschian

0

如果您在线程中运行类似的东西,并将ffmpeg_time属性保存在方法的属性中,以便您可以访问它,那将非常 有用。

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
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.