【问题标题】:pipe large amount of data to stdin while using subprocess.Popen使用 subprocess.Popen 时将大量数据传输到标准输入
【发布时间】:2011-08-20 03:52:34
【问题描述】:

我有点难以理解解决这个简单问题的 python 方法是什么。

我的问题很简单。如果您使用以下代码,它将挂起。这在子流程模块文档中有详细记录。

import subprocess

proc = subprocess.Popen(['cat','-'],
                        stdin=subprocess.PIPE,
                        stdout=subprocess.PIPE,
                        )
for i in range(100000):
    proc.stdin.write('%d\n' % i)
output = proc.communicate()[0]
print output

搜索解决方案(有一个非常有见地的线程,但我现在已经丢失了)我找到了这个使用显式分叉的解决方案(以及其他解决方案):

import os
import sys
from subprocess import Popen, PIPE

def produce(to_sed):
    for i in range(100000):
        to_sed.write("%d\n" % i)
        to_sed.flush()
    #this would happen implicitly, anyway, but is here for the example
    to_sed.close()

def consume(from_sed):
    while 1:
        res = from_sed.readline()
        if not res:
            sys.exit(0)
            #sys.exit(proc.poll())
        print 'received: ', [res]

def main():
    proc = Popen(['cat','-'],stdin=PIPE,stdout=PIPE)
    to_sed = proc.stdin
    from_sed = proc.stdout

    pid = os.fork()
    if pid == 0 :
        from_sed.close()
        produce(to_sed)
        return
    else :
        to_sed.close()
        consume(from_sed)

if __name__ == '__main__':
    main()

虽然这个解决方案在概念上很容易理解,但它使用了一个更多的进程,并且与子进程模块相比,它的级别太低了(那只是为了隐藏这类事情......)。

我想知道:是否有使用不会挂起的子进程模块的简单而干净的解决方案,或者为了实现这种模式,我必须退后一步并实现旧式选择循环或显式分叉?

谢谢

【问题讨论】:

  • 您可以使用线程而不是 fork(与非 UNIX 更好的兼容性,可以说更具可读性),但除此之外,我认为您给出的示例很好。一个选择循环可能也可以在一个线程中“多路复用”操作,但它不会比这更简单。
  • 使用Popen.wait() 的天真阻塞应该会造成死锁(并挂起),但我使用Popen.communicate() 来摆脱这种情况。我认为它使用了一些内部轮询循环将数据填充到缓冲区中。试一试是真的挂了,还是运行时间长?
  • uhmmm ...由于子流程模块是对低级流程管理的抽象,我很惊讶它没有涵盖这个简单的用户案例。

标签: python subprocess popen


【解决方案1】:

如果您想要一个纯 Python 解决方案,您需要将阅读器或编写器放在单独的线程中。 threading 包是一种轻量级的方法,可以方便地访问常用对象并且没有混乱的分叉。

import subprocess
import threading
import sys

proc = subprocess.Popen(['cat','-'],
                        stdin=subprocess.PIPE,
                        stdout=subprocess.PIPE,
                        )
def writer():
    for i in range(100000):
        proc.stdin.write(b'%d\n' % i)
    proc.stdin.close()
thread = threading.Thread(target=writer)
thread.start()
for line in proc.stdout:
    sys.stdout.write(line.decode())
thread.join()
proc.wait()

很高兴看到 subprocess 模块经过现代化改造以支持流和协程,这将允许更优雅地构建混合 Python 片段和 shell 片段的管道。

【讨论】:

  • 以防万一不是很明显:如果您不需要 Python 中的输出;删除stdout=PIPE,您将不需要单独的线程——您可以在同一个线程中写入proc.stdin。不相关:使用with proc.stdin关闭它,即使在写入时发生异常。
【解决方案2】:

我正在寻找一个示例代码来逐步迭代流程输出,因为该流程从提供的迭代器(也以增量方式)消耗其输入。基本上:

import string
import random

# That's what I consider a very useful function, though didn't
# find any existing implementations.
def process_line_reader(args, stdin_lines):
    # args - command to run, same as subprocess.Popen
    # stdin_lines - iterable with lines to send to process stdin
    # returns - iterable with lines received from process stdout
    pass

# Returns iterable over n random strings. n is assumed to be infinity if negative.
# Just an example of function that returns potentially unlimited number of lines.
def random_lines(n, M=8):
    while 0 != n:
        yield "".join(random.choice(string.letters) for _ in range(M))
        if 0 < n:
            n -= 1

# That's what I consider to be a very convenient use case for
# function proposed above.
def print_many_uniq_numbered_random_lines():
    i = 0
    for line in process_line_reader(["uniq", "-i"], random_lines(100500 * 9000)):
        # Key idea here is that `process_line_reader` will feed random lines into
        # `uniq` process stdin as lines are consumed from returned iterable.
        print "#%i: %s" % (i, line)
        i += 1

这里建议的一些解决方案允许使用线程(但并不总是很方便)或使用 asyncio(在 Python 2.x 中不可用)来执行此操作。下面是允许这样做的工作实现示例。

import subprocess
import os
import fcntl
import select

class nonblocking_io(object):
    def __init__(self, f):
        self._fd = -1
        if type(f) is int:
            self._fd = os.dup(f)
            os.close(f)
        elif type(f) is file:
            self._fd = os.dup(f.fileno())
            f.close()
        else:
            raise TypeError("Only accept file objects or interger file descriptors")
        flag = fcntl.fcntl(self._fd, fcntl.F_GETFL)
        fcntl.fcntl(self._fd, fcntl.F_SETFL, flag | os.O_NONBLOCK)
    def __enter__(self):
        return self
    def __exit__(self, type, value, traceback):
        self.close()
        return False
    def fileno(self):
        return self._fd
    def close(self):
        if 0 <= self._fd:
            os.close(self._fd)
            self._fd = -1

class nonblocking_line_writer(nonblocking_io):
    def __init__(self, f, lines, autoclose=True, buffer_size=16*1024, encoding="utf-8", linesep=os.linesep):
        super(nonblocking_line_writer, self).__init__(f)
        self._lines = iter(lines)
        self._lines_ended = False
        self._autoclose = autoclose
        self._buffer_size = buffer_size
        self._buffer_offset = 0
        self._buffer = bytearray()
        self._encoding = encoding
        self._linesep = bytearray(linesep, encoding)
    # Returns False when `lines` iterable is exhausted and all pending data is written
    def continue_writing(self):
        while True:
            if self._buffer_offset < len(self._buffer):
                n = os.write(self._fd, self._buffer[self._buffer_offset:])
                self._buffer_offset += n
                if self._buffer_offset < len(self._buffer):
                    return True
            if self._lines_ended:
                if self._autoclose:
                    self.close()
                return False
            self._buffer[:] = []
            self._buffer_offset = 0
            while len(self._buffer) < self._buffer_size:
                line = next(self._lines, None)
                if line is None:
                    self._lines_ended = True
                    break
                self._buffer.extend(bytearray(line, self._encoding))
                self._buffer.extend(self._linesep)

class nonblocking_line_reader(nonblocking_io):
    def __init__(self, f, autoclose=True, buffer_size=16*1024, encoding="utf-8"):
        super(nonblocking_line_reader, self).__init__(f)
        self._autoclose = autoclose
        self._buffer_size = buffer_size
        self._encoding = encoding
        self._file_ended = False
        self._line_part = ""
    # Returns (lines, more) tuple, where lines is iterable with lines read and more will
    # be set to False after EOF.
    def continue_reading(self):
        lines = []
        while not self._file_ended:
            data = os.read(self._fd, self._buffer_size)
            if 0 == len(data):
                self._file_ended = True
                if self._autoclose:
                    self.close()
                if 0 < len(self._line_part):
                    lines.append(self._line_part.decode(self._encoding))
                    self._line_part = ""
                break
            for line in data.splitlines(True):
                self._line_part += line
                if self._line_part.endswith(("\n", "\r")):
                    lines.append(self._line_part.decode(self._encoding).rstrip("\n\r"))
                    self._line_part = ""
            if len(data) < self._buffer_size:
                break
        return (lines, not self._file_ended)

class process_line_reader(object):
    def __init__(self, args, stdin_lines):
        self._p = subprocess.Popen(args, stdin=subprocess.PIPE, stdout=subprocess.PIPE)
        self._reader = nonblocking_line_reader(self._p.stdout)
        self._writer = nonblocking_line_writer(self._p.stdin, stdin_lines)
        self._iterator = self._communicate()
    def __iter__(self):
        return self._iterator
    def __enter__(self):
        return self._iterator
    def __exit__(self, type, value, traceback):
        self.close()
        return False
    def _communicate(self):
        read_set = [self._reader]
        write_set = [self._writer]
        while read_set or write_set:
            try:
                rlist, wlist, xlist = select.select(read_set, write_set, [])
            except select.error, e:
                if e.args[0] == errno.EINTR:
                    continue
                raise
            if self._reader in rlist:
                stdout_lines, more = self._reader.continue_reading()
                for line in stdout_lines:
                    yield line
                if not more:
                    read_set.remove(self._reader)
            if self._writer in wlist:
                if not self._writer.continue_writing():
                    write_set.remove(self._writer)
        self.close()
    def lines(self):
        return self._iterator
    def close(self):
        if self._iterator is not None:
            self._reader.close()
            self._writer.close()
            self._p.wait()
            self._iterator = None

【讨论】:

    【解决方案3】:

    我能想到的最简单的解决方案:

    from subprocess import Popen, PIPE
    from threading import Thread
    
    s = map(str,xrange(10000)) # a large string
    p = Popen(['cat'], stdin=PIPE, stdout=PIPE, bufsize=1)
    Thread(target=lambda: any((p.stdin.write(b) for b in s)) or p.stdin.close()).start()
    print (p.stdout.read())
    

    缓冲版本:

    from subprocess import Popen, PIPE
    from threading import Thread
    
    s = map(str,xrange(10000)) # a large string
    n = 1024 # buffer size
    p = Popen(['cat'], stdin=PIPE, stdout=PIPE, bufsize=n)
    Thread(target=lambda: any((p.stdin.write(c) for c in (s[i:i+n] for i in xrange(0, len(s), n)))) or p.stdin.close()).start()
    print (p.stdout.read())
    

    【讨论】:

      【解决方案4】:

      只要cat 的标准输出操作系统管道缓冲区已满,您的代码就会死锁。如果你使用stdout=PIPE;您必须及时使用它,否则可能会发生您的情况的死锁。

      如果您在进程运行时不需要输出;您可以将其重定向到一个临时文件:

      #!/usr/bin/env python3
      import subprocess
      import tempfile
      
      with tempfile.TemporaryFile('r+') as output_file:
          with subprocess.Popen(['cat'],
                                stdin=subprocess.PIPE,
                                stdout=output_file,
                                universal_newlines=True) as process:
              for i in range(100000):
                  print(i, file=process.stdin)
          output_file.seek(0)  # rewind (and sync with the disk)
          print(output_file.readline(), end='')  # get  the first line of the output
      

      如果输入/输出很小(适合内存);您可以使用同时为您读/写的.communicate() 一次性传递所有输入并获得所有输出:

      #!/usr/bin/env python3
      import subprocess
      
      cp = subprocess.run(['cat'], input='\n'.join(['%d' % i for i in range(100000)]),
                          stdout=subprocess.PIPE, universal_newlines=True)
      print(cp.stdout.splitlines()[-1]) # print the last line
      

      要手动并发读/写,您可以使用线程、asyncio、fcntl 等。@Jed provided a simple thread-based solution。这是基于asyncio的解决方案:

      #!/usr/bin/env python3
      import asyncio
      import sys
      from subprocess import PIPE
      
      async def pump_input(writer):
           try:
               for i in range(100000):
                   writer.write(b'%d\n' % i)
                   await writer.drain()
           finally:
               writer.close()
      
      async def run():
          # start child process
          # NOTE: universal_newlines parameter is not supported
          process = await asyncio.create_subprocess_exec('cat', stdin=PIPE, stdout=PIPE)
          asyncio.ensure_future(pump_input(process.stdin)) # write input
          async for line in process.stdout: # consume output
              print(int(line)**2) # print squares
          return await process.wait()  # wait for the child process to exit
      
      
      if sys.platform.startswith('win'):
          loop = asyncio.ProactorEventLoop() # for subprocess' pipes on Windows
          asyncio.set_event_loop(loop)
      else:
          loop = asyncio.get_event_loop()
      loop.run_until_complete(run())
      loop.close()
      

      在 Unix 上,您可以使用基于 fcntl 的解决方案:

      #!/usr/bin/env python3
      import sys
      from fcntl import fcntl, F_GETFL, F_SETFL
      from os import O_NONBLOCK
      from shutil import copyfileobj
      from subprocess import Popen, PIPE, _PIPE_BUF as PIPE_BUF
      
      def make_blocking(pipe, blocking=True):
          fd = pipe.fileno()
          if not blocking:
              fcntl(fd, F_SETFL, fcntl(fd, F_GETFL) | O_NONBLOCK) # set O_NONBLOCK
          else:
              fcntl(fd, F_SETFL, fcntl(fd, F_GETFL) & ~O_NONBLOCK) # clear it
      
      
      with Popen(['cat'], stdin=PIPE, stdout=PIPE) as process:
          make_blocking(process.stdout, blocking=False)
          with process.stdin:
              for i in range(100000):
                  #NOTE: the mode is block-buffered (default) and therefore
                  # `cat` won't see it immidiately
                  process.stdin.write(b'%d\n' % i)
                  # a deadblock may happen here with a *blocking* pipe
                  output = process.stdout.read(PIPE_BUF)
                  if output is not None:
                      sys.stdout.buffer.write(output)
          # read the rest
          make_blocking(process.stdout)
          copyfileobj(process.stdout, sys.stdout.buffer)
      

      【讨论】:

        【解决方案5】:

        在 python 3.5 中使用 aiofiles 和 asyncio:

        有点复杂,但你只需要 1024 字节的内存就可以在标准输入中写入!

        import asyncio
        import aiofiles
        import sys
        from os.path import dirname, join, abspath
        import subprocess as sb
        
        
        THIS_DIR = abspath(dirname(__file__))
        SAMPLE_FILE = join(THIS_DIR, '../src/hazelnut/tests/stuff/sample.mp4')
        DEST_PATH = '/home/vahid/Desktop/sample.mp4'
        
        
        async def async_file_reader(f, buffer):
            async for l in f:
                if l:
                    buffer.append(l)
                else:
                    break
            print('reader done')
        
        
        async def async_file_writer(source_file, target_file):
            length = 0
            while True:
                input_chunk = await source_file.read(1024)
                if input_chunk:
                    length += len(input_chunk)
                    target_file.write(input_chunk)
                    await target_file.drain()
                else:
                    target_file.write_eof()
                    break
        
            print('writer done: %s' % length)
        
        
        async def main():
            dir_name = dirname(DEST_PATH)
            remote_cmd = 'ssh localhost mkdir -p %s && cat - > %s' % (dir_name, DEST_PATH)
        
            stdout, stderr = [], []
            async with aiofiles.open(SAMPLE_FILE, mode='rb') as f:
                cmd = await asyncio.create_subprocess_shell(
                    remote_cmd,
                    stdin=sb.PIPE,
                    stdout=sb.PIPE,
                    stderr=sb.PIPE,
                )
        
                await asyncio.gather(*(
                    async_file_reader(cmd.stdout, stdout),
                    async_file_reader(cmd.stderr, stderr),
                    async_file_writer(f, cmd.stdin)
                ))
        
                print('EXIT STATUS: %s' % await cmd.wait())
        
            stdout, stderr = '\n'.join(stdout), '\n'.join(stderr)
        
            if stdout:
                print(stdout)
        
            if stderr:
                print(stderr, file=sys.stderr)
        
        
        if __name__ == '__main__':
            loop = asyncio.get_event_loop()
            loop.run_until_complete(main())
        

        结果:

        writer done: 383631
        reader done
        reader done
        EXIT STATUS: 0
        

        【讨论】:

          【解决方案6】:

          这是我用来加载 6G mysql 转储文件的东西,通过子进程加载。远离 shell=True。不安全并且从进程开始浪费资源。

          import subprocess
          
          fhandle = None
          
          cmd = [mysql_path,
                "-u", mysql_user, "-p" + mysql_pass],
                "-h", host, database]
          
          fhandle = open(dump_file, 'r')
          p = subprocess.Popen(cmd, stdin=fhandle, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
          
          (stdout,stderr) = p.communicate()
          
          fhandle.close()
          

          【讨论】:

            【解决方案7】:

            如果您不想将所有数据都保存在内存中,则必须使用 select。例如。类似:

            import subprocess
            from select import select
            import os
            
            proc = subprocess.Popen(['cat'], stdin=subprocess.PIPE, stdout=subprocess.PIPE)
            
            i = 0;
            while True:
                rlist, wlist, xlist = [proc.stdout], [], []
                if i < 100000:
                    wlist.append(proc.stdin)
                rlist, wlist, xlist = select(rlist, wlist, xlist)
                if proc.stdout in rlist:
                    out = os.read(proc.stdout.fileno(), 10)
                    print out,
                    if not out:
                        break
                if proc.stdin in wlist:
                    proc.stdin.write('%d\n' % i)
                    i += 1
                    if i >= 100000:
                        proc.stdin.close()
            

            【讨论】:

            • 是的,这在概念上是正确的解决方案。可能有点复杂,但如果 Popen 没有开箱即用地实现这些模式,这就是我实现它的方式......
            • 我不认为它开箱即用地实现了这一点,因为通常,当您需要使用它时,您还需要对轮询/选择循环进行精细控制。你检查过asyncore 模块吗?
            • 我发现了这篇有趣的博文:dcreager.net/2009/08/13/subprocess-callbacks
            • 这不是唯一完美的解决方案。新的 asyncio 和 asyncore 模块会更好。
            【解决方案8】:

            现在我知道这不会完全满足您的纯粹主义者,因为输入必须适合内存,并且您无法与输入输出交互地工作,但至少这在您的示例中可以正常工作.通信方法可以选择将输入作为参数,如果您以这种方式为进程提供输入,它将起作用。

            import subprocess
            
            proc = subprocess.Popen(['cat','-'],
                                    stdin=subprocess.PIPE,
                                    stdout=subprocess.PIPE,
                                    )
            
            input = "".join('{0:d}\n'.format(i) for i in range(100000))
            output = proc.communicate(input)[0]
            print output
            

            至于更大的问题,你可以继承Popen,重写__init__以接受流类对象作为stdin、stdout、stderr的参数,并重写_communicate方法(对于跨平台来说很麻烦,你需要这样做两次,查看 subprocess.py 源代码)以在 stdin 流上调用 read() 并将输出 write() 到 stdout 和 stderr 流。这种方法让我感到困扰的是,据我所知,它还没有完成。当以前没有做过明显的事情时,通常是有原因的(它不能按预期工作),但我不明白为什么它不应该这样做,除了你需要流在 Windows 中是线程安全的.

            【讨论】:

              【解决方案9】:

              这是一个使用管道从 gzip 一次读取一条记录的示例 (Python 3):

              cmd = 'gzip -dc compressed_file.gz'
              pipe = Popen(cmd, stdout=PIPE).stdout
              
              for line in pipe:
                  print(":", line.decode(), end="")
              

              我知道有一个标准模块,它只是作为示例。您可以使用通信方法一次性读取整个输出(如 shell 反引号),但显然您必须注意内存大小。

              这是一个在 Linux 上将记录写入 lp(1) 程序的示例(再次是 Python 3):

              cmd = 'lp -'
              proc = Popen(cmd, stdin=PIPE)
              proc.communicate(some_data.encode())
              

              【讨论】:

              • 这是您随处可见的标准示例。关键是我不希望输入从另一个进程通过管道传输,并且我想避免在将所有输入发送给消费者之前将所有输入写入内存...将所有内容一次传递给 proc.communicate 当然可以解决问题...
              【解决方案10】:

              对于这种事情,shell 比 subprocess 好很多。

              编写非常简单的 Python 应用程序,从 sys.stdin 读取并写入 sys.stdout

              使用 shell 管道将简单的应用程序连接在一起。

              如果您愿意,可以使用 subprocess 启动管道,或者直接编写一个单行 shell 脚本。

              python part1.py | python part2.py
              

              这是非常非常有效的。只要您保持非常简单,它也可以移植到所有 Linux(和 Windows)。

              【讨论】:

              • 我知道有一种 1001 的方法。我要的是 python 方式 :) 称我为纯粹主义者 :)
              • @user741720:我给了你 Pythonic 的解决方案。使用sys.stdinsys.stdout 并避免不必要地使用复杂的subprocess 代码。纯粹的方法是尽可能少地编写代码,并尽可能干净地编写那一点代码。如果您不在已经高度优化的操作系统代码中间插入额外的 Python 处理,操作系统会做到这一点(并且最快且开销最小)。
              猜你喜欢
              • 1970-01-01
              • 2020-11-25
              • 2011-11-30
              • 1970-01-01
              • 1970-01-01
              • 1970-01-01
              • 1970-01-01
              • 1970-01-01
              相关资源
              最近更新 更多