实时捕获子进程的标准输出
实时捕获子进程的标准输出
我想在Windows中使用subprocess.Popen()
来运行rsync.exe,并在Python中打印stdout。
我的代码可以工作,但它在文件传输完成之前无法捕获进度!我想实时打印每个文件的进度。
现在使用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()
问题出现的原因是在使用subprocess时,无法实时捕获子进程的标准输出(stdout)。解决方法是禁用子进程本身的缓冲机制,并使用特定的工具或参数来实现。
如果子进程是Python进程,可以在调用之前设置os.environ["PYTHONUNBUFFERED"] = "1"
,或者将其作为env
参数传递给Popen
。
如果在Linux/Unix上,可以使用stdbuf
工具来实现。例如,可以将stdbuf
命令添加到cmd
列表中:cmd = ["stdbuf", "-oL"] + cmd
。
更多关于stdbuf
或其他选项的信息,请参考这里。
通过上述方法,可以实现从子进程实时捕获标准输出的功能。
在上述内容中,问题的出现原因是由于程序中的子进程输出了大量数据,填满了管道,导致主程序无法实时捕获子进程的stdout。为了解决这个问题,可以在调用子进程时使用`--outbuf=L`选项来设置输出缓冲区的大小。以下是解决方法的代码示例:
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())
这段代码通过设置输出缓冲区的大小来实现了实时捕获子进程的stdout。这个解决方法可以帮助读者节省时间和精力,避免浪费时间在解决这个问题上。
除了解决方法,上述内容还提到了问题发生的原因。在程序中添加了`preexec_fn=os.setpgrp`以使子进程能够在父脚本退出后继续运行,并且没有从子进程的管道中读取数据时,当子进程输出大量数据填满管道时,程序可能会在随机的时间之后退出。这个问题给读者带来了困扰,并且没有明确说明发生了什么以及为什么发生。这也是为什么上述内容中提到其他读者的答案对问题的解决有很大帮助的原因。
在使用subprocess时的一些经验规则:
- 永远不要使用
shell=True
。这会额外调用一个shell进程来执行程序。 - 调用进程时,参数是以列表的形式传递的。在Python中,
sys.argv
是一个列表,C语言中的argv
也是一个列表。所以在调用子进程时,要传递一个列表给Popen
,而不是一个字符串。 - 当你不需要读取
stderr
时,不要重定向到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
本身正在对输出进行缓冲,这就无法实时获取结果了。
解决方法是使用pexpect
直接连接到一个pty
。
shell=False
是构建命令行时的正确方法,特别是当命令行的内容来自于用户输入数据时。但是,当你从可信源(例如脚本中的硬编码)获取整个命令行时,shell=True
也是有用的。
Otkidach:我不认为这足以证明使用shell=True
。想一想 - 你在操作系统上调用另一个进程,涉及到内存分配、磁盘使用、处理器调度,只是为了分割一个字符串!而且这个字符串还是你自己拼接的!!你可以在Python中分割字符串,但是用独立的参数写起来更容易。另外,使用一个列表意味着你不必转义特殊的shell字符:空格、;
、>
、<
、&
...你的参数中可以包含这些字符,而不必担心!我真的看不出为什么要使用shell=True
,除非你只运行一个仅限于shell的命令。
nosklo:应该是这样的:p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
shell=False是不好的,但它允许方便地进行管道操作。有没有一种简便的方法来运行一条管道命令?例如运行命令'cat longfile.tab | cut -f1 | head -100'.split()
我不确定为什么要将这些操作作为单独的进程进行...你可以使用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特殊字符而无需转义。此外,这样做速度更快,因为少了一个进程。
我在考虑的情况是,你想在一个超过1300万行的文件中使用grep匹配正则表达式。我尝试了用Python实现grep部分,但与grep相比,速度非常慢。也许有其他因素导致了速度慢?我很快就放弃了使用Python进行搜索。
是的,grep是一款非常高效的软件,它非常努力地完成它的工作。你无法用Python打败它。不过它不应该是“非常慢”,只是慢一些。你是否有你正在使用的代码的发布位置?也许你想在这里提一个新问题,这样我们可以讨论是否有什么问题以及最佳解决方案是什么...
我将尝试发布一个干净的Python grep测试代码...更好地了解速度差异将是件好事。
在Python 2中使用for line in iter(p.stdout.readline, b'')
而不是for line in p.stdout
,否则即使源进程不缓冲其输出,也无法实时读取行。
我经常使用shell=True
的情况是在大型管道中。是的,仅仅通过grep进行管道操作可能不值得,但在进行生物信息学等操作时,4-5个命令的链并不罕见。当我将一个个人使用的shell脚本转换为Python时,我不会花时间手动将每个shell管道转换为等效的Python函数。我会将管道剪切并包装在shell字符串中,然后使用shell=True运行它们,因为这些管道“足够快”。
这对我来说是一个错误,出现了TypeError: Can't convert 'bytes' object to str implicitly
的错误(在Python 3中)。
我确实有一个使用shell=True
的用例:将命令复制并粘贴到后续的shell中。所以应该是“几乎不用”,而不是“永远不用”。
当然,如果你认为为了能够将命令复制并粘贴到后续的shell中而额外调用一个无用的进程并处理引号问题是值得的,那么就这么做吧,但这并不是一个好的用例。
说你永远不应该使用shell=True是没有必要的。如果我有一个用于个人使用的shell脚本,并且我将其转换为Python,我不会花时间手动将每个shell管道转换为等效的Python函数。我会剪切并粘贴管道,将其包装在shell字符串中,并使用shell=True运行它们,因为这些管道“已经够快了”。