来自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) 

本来我是通过teepipe道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' )。

你有两种方式来做这件事,无论是从readreadline函数创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那里以非阻塞的方式阅读。 当使用PIPEreadread函数将被阻塞,直到任何一个字符被写入到pipe道或一条线被分别写入pipe道。

执行摘要(或“tl; dr”版本):最多只有一个subprocess.PIPE很简单,否则很难。

这可能是时间来解释subprocess.Popen是怎么做的。

(注意:这是为了Python 2.x,尽pipe3.x是类似的,而且我对Windows的变体很模糊,我理解POSIX的东西要好得多。

Popen函数需要同时处理零到三个I / Ostream。 像往常一样,这些被标记为stdinstdoutstderr

您可以提供:

  • None ,表示您不想redirectstream。 它将像往常一样inheritance这些。 请注意,至less在POSIX系统上,这并不意味着它将使用Python的sys.stdout ,只是Python的实际标准输出; 看看演示结束。
  • 一个int值。 这是一个“原始”文件描述符(至less在POSIX中)。 (注意: PIPESTDOUT在内部实际上是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 ,但让stdoutstderr不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但是单独留下stdinstderr 。 再次,这很容易:只要调用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.stdoutself.stderr结果,并且父线程传递self.stdininput数据(然后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道上读取stdoutstderr (不pipe任何stdinredirect),你也需要避免死锁。 这里的死锁场景是不同的 – 当你从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器的实例传递给Popensubprocess.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()