来自subprocess命令的实时输出
我使用Python脚本作为stream体动力学代码的驱动程序。 当运行模拟时,我使用subprocess.Popen
来运行代码,将stdout和stderr的输出收集到subprocess.PIPE
—然后我可以打印(并保存到日志文件)输出信息,并检查是否有错误。 问题是,我不知道代码是如何进展的。 如果我直接从命令行运行它,它会给出关于它在什么时候迭代的输出,什么时间,什么是下一个时间步,等等。
有没有一种方法来存储输出(用于logging和错误检查),并且还产生实时stream输出?
我的代码的相关部分:
ret_val = subprocess.Popen( run_command, stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=True ) output, errors = ret_val.communicate() log_file.write(output) print output if( ret_val.returncode ): print "RUN failed\n\n%s\n\n" % (errors) success = False if( errors ): log_file.write("\n\n%s\n\n" % errors)
本来我是通过tee
pipe道run_command
,以便副本直接到日志文件,并仍然输出stream到terminal – 但这样我不能存储任何错误(我知道)。
编辑:
临时解决scheme:
ret_val = subprocess.Popen( run_command, stdout=log_file, stderr=subprocess.PIPE, shell=True ) while not ret_val.poll(): log_file.flush()
然后在另一个terminal上运行tail -f log.txt
(st log_file = 'log.txt'
)。
你有两种方式来做这件事,无论是从read
或readline
函数创build一个迭代器,并做:
import subprocess import sys with open('test.log', 'w') as f: process = subprocess.Popen(your_command, stdout=subprocess.PIPE) for c in iter(lambda: process.stdout.read(1), ''): sys.stdout.write(c) f.write(c)
要么
import subprocess import sys with open('test.log', 'w') as f: process = subprocess.Popen(your_command, stdout=subprocess.PIPE) for line in iter(process.stdout.readline, ''): sys.stdout.write(line) f.write(line)
或者你可以创build一个reader
和一个writer
文件。 把writer
传给Popen
并从reader
那里reader
import io import time import subprocess import sys filename = 'test.log' with io.open(filename, 'wb') as writer, io.open(filename, 'rb', 1) as reader: process = subprocess.Popen(command, stdout=writer) while process.poll() is None: sys.stdout.write(reader.read()) time.sleep(0.5) # Read the remaining sys.stdout.write(reader.read())
这样你就可以将数据写在test.log
和标准输出中。
文件方法的唯一好处是你的代码不会被阻塞。 所以你可以随时做任何你想要的事情,并且只要你想从reader
那里以非阻塞的方式阅读。 当使用PIPE
, read
和read
函数将被阻塞,直到任何一个字符被写入到pipe道或一条线被分别写入pipe道。
执行摘要(或“tl; dr”版本):最多只有一个subprocess.PIPE
很简单,否则很难。
这可能是时间来解释subprocess.Popen
是怎么做的。
(注意:这是为了Python 2.x,尽pipe3.x是类似的,而且我对Windows的变体很模糊,我理解POSIX的东西要好得多。
Popen
函数需要同时处理零到三个I / Ostream。 像往常一样,这些被标记为stdin
, stdout
和stderr
。
您可以提供:
-
None
,表示您不想redirectstream。 它将像往常一样inheritance这些。 请注意,至less在POSIX系统上,这并不意味着它将使用Python的sys.stdout
,只是Python的实际标准输出; 看看演示结束。 - 一个
int
值。 这是一个“原始”文件描述符(至less在POSIX中)。 (注意:PIPE
和STDOUT
在内部实际上是int
,但是是“不可能的”描述符,-1和-2。) - 一个stream – 真的,任何一个带有
fileno
方法的对象。Popen
将使用stream.fileno()
find该stream的描述符,然后继续处理int
值。 -
subprocess.PIPE
,指示Python应该创build一个pipe道。 -
subprocess.STDOUT
(仅适用于stderr
):告诉Python使用与stdout
相同的描述符。 如果你为stdout
提供了一个(非None
)值,那么这是唯一有意义的,即使如此,只有当你设置stdout=subprocess.PIPE
时才需要 。 (否则,你可以提供你提供的stdout
相同的参数,例如,Popen(..., stdout=stream, stderr=stream)
。)
最简单的情况(没有pipe道)
如果你什么都不redirect(把所有三个都作为默认的None
值或者提供显式的None
), Pipe
很容易。 它只需要分离subprocess,让它运行。 或者,如果您redirect到非PIPE
int
或stream的fileno()
– 它仍然很容易,因为操作系统完成所有工作。 Python只需要分离subprocess,将stdin,stdout和/或stderr连接到提供的文件描述符。
仍然简单的情况:一个pipe道
如果只redirect一个stream, Pipe
仍然非常容易。 让我们一次select一个stream,观看。
假设你想提供一些stdin
,但让stdout
和stderr
不redirect,或者去一个文件描述符。 作为父进程,您的Python程序只需使用write()
将数据发送到pipe道。 你可以自己做,例如:
proc = subprocess.Popen(cmd, stdin=subprocess.PIPE) proc.stdin.write('here, have some data\n') # etc
或者你可以将stdin数据传递给proc.communicate()
,然后执行stdin.write
显示的stdin.write
。 没有输出回来所以communicate()
只有一个其他的真正的工作:它也closures你的pipe道。 (如果你不调用proc.communicate()
你必须调用proc.stdin.close()
来closurespipe道,这样子proc.stdin.close()
就知道没有更多的数据了。)
假设你想捕获stdout
但是单独留下stdin
和stderr
。 再次,这很容易:只要调用proc.stdout.read()
(或等效),直到没有更多的输出。 由于proc.stdout()
是一个普通的Python I / Ostream,所以你可以使用它的所有常规结构,比如:
for line in proc.stdout:
或者,再次,您可以使用proc.communicate()
,它只是为您执行read()
。
如果你只想捕捉stderr
,它和stdout
。
事情变得困难之前还有一个窍门。 假设你想捕获stdout
,并捕获stderr
但是在stdout的同一个pipe道上:
proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
在这种情况下, subprocess
“作弊”! 嗯,它必须这样做,所以它不是真正的作弊:它启动subprocess的stdout和它的stderr指向(单一)pipe道描述符反馈到它的父(Python)进程。 在父节点上,再次只有一个pipe道描述符用于读取输出。 所有“stderr”输出都显示在proc.stdout
,如果调用proc.communicate()
,stderr结果(元组中的第二个值)将为None
,而不是string。
困难的情况下:两个或更多的pipe道
当你想使用至less两个pipe道时,问题都出现了。 事实上, subprocess
进程代码本身有这个位:
def communicate(self, input=None): ... # Optimization: If we are only using one pipe, or no pipe at # all, using select() or threads is unnecessary. if [self.stdin, self.stdout, self.stderr].count(None) >= 2:
但是,唉,这里我们已经做了至less两个,也许三个不同的pipe道,所以count(None)
返回1或0.我们必须努力工作。
在Windows上,它使用threading.Thread
来累积self.stdout
和self.stderr
结果,并且父线程传递self.stdin
input数据(然后closurespipe道)。
在POSIX上,如果可用则使用poll
,否则select
,累积输出并提供stdininput。 所有这些都在(单个)父进程/线程中运行。
线程或轮询/select在这里需要避免死锁。 假设,例如,我们已经将所有三个streamredirect到三个单独的pipe道。 进一步假设,在写入过程暂停之前,有多less数据可以填充到pipe道中有一个小的限制,等待读取过程从另一端“清理”pipe道。 让我们把这个小的限制设置为一个字节,只是为了说明。 (这实际上是如何工作的,除了限制比一个字节大得多。)
如果父进程试图向proc.stdin
写入几个字节,比如'go\n'
,则第一个字节进入,然后第二个字节导致Python进程挂起,等待subprocess读取第一个字节,清空pipe道。
同时,假设subprocess决定打印一个友好的“你好,别慌!” 问候。 H
进入标准输出pipe道,但是e
导致它暂停,等待其父母读取H
,清空标准输出pipe道。
现在我们被困住了:Python进程正在睡觉,等待完成说“去”,subprocess也在睡觉,等待完成说“你好!不要惊慌!”。
subprocess.Popen
代码避免了线程或select/轮询的这个问题。 当字节可以通过pipe道,他们去。 当它们不能时,只有一个线程(不是整个进程)必须进入hibernate状态 – 或者在select / poll的情况下,Python进程同时等待“可写入”或“可用数据”,写入进程的stdin只有当有空间时,只有在数据准备好的时候才读取它的stdout和/或stderr。 proc.communicate()
代码(实际上_communicate
情况下处理的位置)一旦所有标准input数据(如果有)已发送,所有标准输出和/或标准错误数据已被累计返回。
如果你想在两个不同的pipe道上读取stdout
和stderr
(不pipe任何stdin
redirect),你也需要避免死锁。 这里的死锁场景是不同的 – 当你从stdout
提取数据的时候,subprocess把长的东西写到stderr
,反之亦然 – 但是它仍然存在。
演示
我承诺说明,未redirect的Python subprocess
进程写入底层的stdout而不是sys.stdout
。 所以,这是一些代码:
from cStringIO import StringIO import os import subprocess import sys def show1(): print 'start show1' save = sys.stdout sys.stdout = StringIO() print 'sys.stdout being buffered' proc = subprocess.Popen(['echo', 'hello']) proc.wait() in_stdout = sys.stdout.getvalue() sys.stdout = save print 'in buffer:', in_stdout def show2(): print 'start show2' save = sys.stdout sys.stdout = open(os.devnull, 'w') print 'after redirect sys.stdout' proc = subprocess.Popen(['echo', 'hello']) proc.wait() sys.stdout = save show1() show2()
运行时:
$ python out.py start show1 hello in buffer: sys.stdout being buffered start show2 hello
请注意,如果添加stdout=sys.stdout
,则第一个例程将失败,因为StringIO
对象没有fileno
。 第二个会省略hello
如果你添加stdout=sys.stdout
因为sys.stdout
已被redirect到os.devnull
。
(如果你redirectPython的文件描述符-1,子open(os.devnull, 'w')
将遵循这个redirectopen(os.devnull, 'w')
调用产生一个其fileno()
大于2的stream。
如果你能够使用第三方库,你可能会使用像sarge
(披露:我是它的维护者)。 这个库允许从subprocess输出stream的非阻塞访问 – 它被分层在subprocess
模块上。
我们也可以使用默认的文件迭代器来读取标准输出,而不是使用它与readline()构造。
import subprocess import sys process = subprocess.Popen(your_command, stdout=subprocess.PIPE) for line in process.stdout: sys.stdout.write(line)
一个好的但“重量级”的解决scheme是使用扭曲 – 见底部。
如果你愿意只依靠这些标准来生活,
import subprocess import sys popenobj = subprocess.Popen(["ls", "-Rl"], stdout=subprocess.PIPE) while not popenobj.poll(): stdoutdata = popenobj.stdout.readline() if stdoutdata: sys.stdout.write(stdoutdata) else: break print "Return code", popenobj.returncode
(如果你使用read(),它会尝试读取整个没有用的“文件”,这里我们真正可以使用的是读取pipe道中所有数据的东西)
也可以试着用线程来解决这个问题,例如:
import subprocess import sys import threading popenobj = subprocess.Popen("ls", stdout=subprocess.PIPE, shell=True) def stdoutprocess(o): while True: stdoutdata = o.stdout.readline() if stdoutdata: sys.stdout.write(stdoutdata) else: break t = threading.Thread(target=stdoutprocess, args=(popenobj,)) t.start() popenobj.wait() t.join() print "Return code", popenobj.returncode
现在我们可以通过两个线程添加stderr。
但是请注意,subprocess文档不鼓励直接使用这些文件,并build议使用communicate()
(主要涉及死锁,我认为这不是上述问题),解决scheme有点klunky,所以它看起来像子进程模块完全相当于这个工作 (另见: http : //www.python.org/dev/peps/pep-3145/ ),我们需要看看其他的东西。
更复杂的解决scheme是使用Twisted ,如下所示: https : //twistedmatrix.com/documents/11.1.0/core/howto/process.html
你用Twisted做这件事的方法是使用reactor.spawnprocess()
创build你的进程,并提供一个ProcessProtocol
,然后asynchronous处理输出。 Twisted示例Python代码位于: https : //twistedmatrix.com/documents/11.1.0/core/howto/listings/process/process.py
它看起来像行缓冲输出将为您工作,在这种情况下可能适合下面的内容。 (注意:这是未经testing的。)这只会实时给出subprocess的stdout。 如果你想同时拥有stderr和stdout,那么你必须用select
来做更复杂的事情。
proc = subprocess.Popen(run_command, stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=True) while proc.poll() is None: line = proc.stdout.readline() print line log_file.write(line + '\n') # Might still be data on stdout at this point. Grab any # remainder. for line in proc.stdout.read().split('\n'): print line log_file.write(line + '\n') # Do whatever you want with proc.stderr here...
为什么不直接将stdout
设置为sys.stdout
? 而且,如果您还需要输出到日志,那么您可以简单地覆盖f的写入方法。
import sys import subprocess class SuperFile(open.__class__): def write(self, data): sys.stdout.write(data) super(SuperFile, self).write(data) f = SuperFile("log.txt","w+") process = subprocess.Popen(command, stdout=f, stderr=f)
我会在这里出去,build议使用check_call
。 按照文档,它启动一个阻塞操作,并适合这个目的很好。
用参数运行命令。 等待命令完成。 如果返回码为零,则返回,否则引发CalledProcessError。 CalledProcessError对象将在returncode属性中具有返回码。
cmd = "some_crazy_long_script.sh" args = { 'shell': True, 'cwd': if_you_need_it } subprocess.check_call(cmd, **args)
这是我在其中一个项目中使用的一个类。 它将一个subprocess的输出redirect到日志。 起初,我试图简单地覆盖写入方法,但不起作用,因为subprocess永远不会调用它(redirect发生在filedescriptor级别)。 所以我使用自己的pipe道,就像在subprocess模块中完成的一样。 这样做的好处是可以封装适配器中的所有日志logging/打印逻辑,您只需将logging器的实例传递给Popen
: subprocess.Popen("/path/to/binary", stderr = LogAdapter("foo"))
class LogAdapter(threading.Thread): def __init__(self, logname, level = logging.INFO): super().__init__() self.log = logging.getLogger(logname) self.readpipe, self.writepipe = os.pipe() logFunctions = { logging.DEBUG: self.log.debug, logging.INFO: self.log.info, logging.WARN: self.log.warn, logging.ERROR: self.log.warn, } try: self.logFunction = logFunctions[level] except KeyError: self.logFunction = self.log.info def fileno(self): #when fileno is called this indicates the subprocess is about to fork => start thread self.start() return self.writepipe def finished(self): """If the write-filedescriptor is not closed this thread will prevent the whole program from exiting. You can use this method to clean up after the subprocess has terminated.""" os.close(self.writepipe) def run(self): inputFile = os.fdopen(self.readpipe) while True: line = inputFile.readline() if len(line) == 0: #no new data was added break self.logFunction(line.strip())
如果你不需要日志logging,但只是想使用print()
那么显然可以删除大部分的代码,并保持较短的时间。 您还可以通过__enter__
和__exit__
方法来扩展它,并在__exit__
finished
调用,以便您可以轻松地将其用作上下文。
我试过的所有上述解决scheme都失败,要么分开stderr和标准输出(多个pipe道),要么当操作系统pipe道缓冲区已满时发生,当您正在运行的命令输出速度太快(有一个警告在python poll()子过程手册)。 我发现的唯一可靠的方法是通过select,但这是一个posix的解决scheme:
import subprocess import sys import os import select # returns command exit status, stdout text, stderr text # rtoutput: show realtime output while running def run_script(cmd,rtoutput=0): p = subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) poller = select.poll() poller.register(p.stdout, select.POLLIN) poller.register(p.stderr, select.POLLIN) coutput='' cerror='' fdhup={} fdhup[p.stdout.fileno()]=0 fdhup[p.stderr.fileno()]=0 while sum(fdhup.values()) < len(fdhup): try: r = poller.poll(1) except select.error, err: if err.args[0] != EINTR: raise r=[] for fd, flags in r: if flags & (select.POLLIN | select.POLLPRI): c = os.read(fd, 1024) if rtoutput: sys.stdout.write(c) sys.stdout.flush() if fd == p.stderr.fileno(): cerror+=c else: coutput+=c else: fdhup[fd]=1 return p.poll(), coutput.strip(), cerror.strip()
import sys import subprocess f = open("log.txt","w+") process = subprocess.Popen(command, stdout=subprocess.PIPE) for line in iter(process.stdout.readline, ''): sys.stdout.write(line) f.write(line.replace("\n","")) f.close()