如何将一个字符串传递给subprocess.Popen(使用stdin参数)?

345

如果我执行以下操作:

import subprocess
from cStringIO import StringIO
subprocess.Popen(['grep','f'],stdout=subprocess.PIPE,stdin=StringIO('one\ntwo\nthree\nfour\nfive\nsix\n')).communicate()[0]

我得到:

Traceback (most recent call last):
  File "<stdin>", line 1, in ?
  File "/build/toolchain/mac32/python-2.4.3/lib/python2.4/subprocess.py", line 533, in __init__
    (p2cread, p2cwrite,
  File "/build/toolchain/mac32/python-2.4.3/lib/python2.4/subprocess.py", line 830, in _get_handles
    p2cread = stdin.fileno()
AttributeError: 'cStringIO.StringI' object has no attribute 'fileno'

显然,cStringIO.StringIO对象在关闭方面与文件对象不够相似,无法满足subprocess.Popen的要求。我该如何解决这个问题?


3
与其因为这个被删除而争论我的答案,我将它作为评论添加上去……建议阅读:Doug Hellmann的Python模块每周博客文章——子进程 - Daryl Spitzer
4
这篇博客文章存在多个错误,例如:第一个代码示例:call(['ls', '-1'], shell=True) 是错误的。我建议阅读subprocess标签描述中的常见问题。特别是为什么使用参数为序列时,subprocess.Popen不起作用? 解释了为什么 call(['ls', '-1'], shell=True) 是错误的。我记得在博客文章下留过评论,但出于某种原因现在找不到它们了。 - jfs
2
请参考较新的subprocess.run,网址为https://dev59.com/MFYM5IYBdhLWcg3wgwXH#59496029 - user3064538
12个回答

392

Popen.communicate() 文档:

请注意,如果您想向进程的标准输入发送数据,则需要使用 stdin=PIPE 创建 Popen 对象。同样,要在结果元组中获取除了 None 之外的任何内容,您也需要给出 stdout=PIPE 和/或 stderr=PIPE。

替换 os.popen*

    pipe = os.popen(cmd, 'w', bufsize)
    # ==>
    pipe = Popen(cmd, shell=True, bufsize=bufsize, stdin=PIPE).stdin

警告:建议使用 communicate() 而不是 stdin.write()、stdout.read() 或 stderr.read(),以避免其他操作系统管道缓冲区填满并阻塞子进程导致死锁。

因此,您可以将示例编写为:

from subprocess import Popen, PIPE, STDOUT

p = Popen(['grep', 'f'], stdout=PIPE, stdin=PIPE, stderr=STDOUT)    
grep_stdout = p.communicate(input=b'one\ntwo\nthree\nfour\nfive\nsix\n')[0]
print(grep_stdout.decode())
# -> four
# -> five
# ->

在Python 3.5+(3.6+用于encoding)中,您可以使用subprocess.run,将输入作为字符串传递给外部命令,并一次性获得其退出状态和输出字符串:

#!/usr/bin/env python3
from subprocess import run, PIPE

p = run(['grep', 'f'], stdout=PIPE,
        input='one\ntwo\nthree\nfour\nfive\nsix\n', encoding='ascii')
print(p.returncode)
# -> 0
print(p.stdout)
# -> four
# -> five
# -> 

17
这不是一个好的解决方案。特别地,如果这样做,您无法异步处理p.stdout.readline输出,因为您必须等待整个stdout到达。而且它也是内存效率低下的。 - OTZ
10
什么是更好的解决方案? - Nick T
13
“更好”取决于上下文。牛顿定律适用于它们所涉及的领域,但你需要使用特殊相对论来设计GPS。请参见在Python中的subprocess.PIPE上进行非阻塞读取 - jfs
9
请注意 communicate 方法的注释:“如果数据量很大或不可限制,请勿使用此方法”。 - Owen
2
使用 subprocess.run() 时,您需要 Python3.6 以上版本才能使用 input 参数。如果您这样做,旧版的 Python3 也可以正常工作: p = run(['grep', 'f'], stdout=PIPE, input=some_string.encode('ascii')) - TaborKelly
显示剩余9条评论

50

我想出了这个解决办法:

>>> p = subprocess.Popen(['grep','f'],stdout=subprocess.PIPE,stdin=subprocess.PIPE)
>>> p.stdin.write(b'one\ntwo\nthree\nfour\nfive\nsix\n') #expects a bytes type object
>>> p.communicate()[0]
'four\nfive\n'
>>> p.stdin.close()

还有更好的选择吗?


27
@Moe:不鼓励使用stdin.write(),应该使用p.communicate()。请参见我的回答。 - jfs
12
根据subprocess文档: 警告 - 使用 communicate() 而不是 .stdin.write,.stdout.read 或 .stderr.read 来避免由于其他操作系统管道缓冲区填满并阻塞子进程而导致死锁。 - Jason Mock
2
如果你有信心你的标准输出/错误永远不会填满(例如,它将被写入文件中,或者另一个线程正在处理),并且你有大量数据要发送到标准输入,那么我认为这是一种很好的解决方法。 - Lucretiel
1
特别是,以这种方式执行仍然确保关闭stdin,因此如果子进程是一个永远消耗输入的进程,则communicate将关闭管道并允许进程优雅地结束。 - Lucretiel
@Lucretiel,如果该进程会一直占用标准输入(stdin),那么它很可能仍然可以无限地写入标准输出(stdout),因此我们需要完全不同的技术来处理它(不能像read()那样读取它,即使没有参数也不能像communicate()那样操作)。 - Charles Duffy
@Lucretiel,无论如何,为了避免死锁,您需要在不同的线程中执行p.stdin.write(),而这个答案并没有展示必要的技术。p.stdin.write()可能有其用处,但它的用处不在于一个如此简短和简单的答案中,因为它无法安全地演示如何使用它。 - Charles Duffy

41

如果您使用的是Python 3.4或更高版本,则有一个漂亮的解决方案。使用input参数替代stdin参数,该参数接受一个字节参数:

output_bytes = subprocess.check_output(
    ["sed", "s/foo/bar/"],
    input=b"foo",
)

这适用于check_outputrun,但是由于某些原因不适用于callcheck_call

在Python 3.7+中,你还可以添加text=True使得check_output以字符串形式输入并返回一个字符串(而不是bytes):

output_string = subprocess.check_output(
    ["sed", "s/foo/bar/"],
    input="foo",
    text=True,
)

7
你说得对,这很奇怪。我认为应该将此视为 Python 的一个 bug,我看不出为什么 check_output 应该有一个 input 参数,但 call 却没有。 - Flimm
3
这是适用于Python 3.4+(在Python 3.6中使用)的最佳答案。确实无法与check_call一起使用,但可以与run一起使用。根据文档,只要传递一个编码参数,它还可以处理字符串类型的input。 - Nikolaos Georgiou
@Flimm 原因很明显:runcheck_output在底层使用communicate,而callcheck_call则不使用。communicate更加繁重,因为它涉及到使用select来处理流,而callcheck_call则更加简单和快速。 - Vadim Fint

31

有点惊讶没有人建议创建一个管道,我认为这是将字符串传递给子进程的最简单方式:

read, write = os.pipe()
os.write(write, "stdin input here")
os.close(write)

subprocess.check_call(['your-command'], stdin=read)

3
ossubprocess的文档都认为你应该优先选择后者。这是一种遗留解决方案,有一个(稍微不太简洁的)标准替代方案;被接受的答案引用了相关的文档。 - tripleee
1
我不确定这是正确的,tripleee。引用的文档说明了使用进程创建的管道的困难之处,但在这个解决方案中,它创建了一个管道并将其传递进去。我相信它避免了在进程已经启动后管理管道可能导致的潜在死锁问题。 - Graham Christensen
os.popen已被弃用,建议使用subprocess。 - hd1
3
此功能会导致死锁,可能丢失数据。subprocess 模块已经提供了此功能,请使用它,而不是重新实现它(尝试写入大于操作系统管道缓冲区的值)。 - jfs
你值得拥有最好的,优秀的人,感谢你提供最简单、最聪明的解决方案。 - Felipe Buccioni
2
@tripleee,subprocess模块中的管道实现非常糟糕,无法控制。您甚至无法获取有关内置缓冲区大小的信息,更不用说告诉它管道的读取和写入端口,也不能更改内置缓冲区。简而言之:subprocess管道是垃圾,请勿使用。 - wvxvw

15

我正在使用Python3,发现在将字符串传入stdin之前,需要对其进行编码:

我在使用Python3,发现在将字符串传入stdin之前需要先对其进行编码:

p = Popen(['grep', 'f'], stdout=PIPE, stdin=PIPE, stderr=PIPE)
out, err = p.communicate(input='one\ntwo\nthree\nfour\nfive\nsix\n'.encode())
print(out)

5
您不需要专门对输入进行编码,只需提供一个类似于字节的对象(例如b'something')。它将返回字节类型的错误和输出。如果您想避免这种情况,可以在调用Popen时传递universal_newlines=True参数。这样它将接受字符串形式的输入,并且也将返回字符串形式的 err 和 out。 - Six
2
但要注意,universal_newlines=True 也会将您的换行符转换为与您的系统匹配。 - Nacht
1
如果您正在使用Python 3,请查看我的答案,以获取更方便的解决方案。 - Flimm

12

显然,cStringIO.StringIO对象的close方法不足以满足subprocess.Popen对文件操作的要求。

很抱歉,管道是一个底层的操作系统概念,因此它绝对需要一个由操作系统级文件描述符表示的文件对象。你的解决方法是正确的。


11
from subprocess import Popen, PIPE
from tempfile import SpooledTemporaryFile as tempfile
f = tempfile()
f.write('one\ntwo\nthree\nfour\nfive\nsix\n')
f.seek(0)
print Popen(['/bin/grep','f'],stdout=PIPE,stdin=f).stdout.read()
f.close()

3
临时文件包装器,专门在超过一定大小或需要fileno时,从StringIO切换到真正的文件。提示:tempfile.SpooledTemporaryFile.__doc__是这样写的。 - Doug F

7
"""
Ex: Dialog (2-way) with a Popen()
"""

p = subprocess.Popen('Your Command Here',
                 stdout=subprocess.PIPE,
                 stderr=subprocess.STDOUT,
                 stdin=PIPE,
                 shell=True,
                 bufsize=0)
p.stdin.write('START\n')
out = p.stdout.readline()
while out:
  line = out
  line = line.rstrip("\n")

  if "WHATEVER1" in line:
      pr = 1
      p.stdin.write('DO 1\n')
      out = p.stdout.readline()
      continue

  if "WHATEVER2" in line:
      pr = 2
      p.stdin.write('DO 2\n')
      out = p.stdout.readline()
      continue
"""
..........
"""

out = p.stdout.readline()

p.wait()

5
因为 shell=True 经常被无端使用,而这是一个普遍的问题,所以让我指出,在很多情况下,Popen(['cmd', 'with', 'args']) 要比 Popen('cmd with args', shell=True) 更好,因为前者可以将命令和参数拆分为标记,但不会增加过多复杂性,也不会增加攻击面。而 shell=True 会将命令和参数拆分为标记,却没有提供任何有用信息,反而增加了显著的复杂性和攻击面。 - tripleee

7

在 Python 3.7+ 中,执行以下操作:

my_data = "whatever you want\nshould match this f"
subprocess.run(["grep", "f"], text=True, input=my_data)

很可能您需要添加capture_output=True以将运行命令的输出作为字符串获取。

在旧版本的Python中,用universal_newlines=True替换text=True

subprocess.run(["grep", "f"], universal_newlines=True, input=my_data)

6
请注意,如果s太大,则Popen.communicate(input=s)可能会出现问题,因为显然父进程在复制子进程之前会缓冲它,这意味着此时需要使用“一倍”的内存(至少根据“幕后”解释和链接文档here)。 在我特定的情况下,s是一个先完全展开,然后才写入stdin的生成器,因此在产生子进程之前,父进程非常庞大,没有剩余内存可供分叉:

File "/opt/local/stow/python-2.7.2/lib/python2.7/subprocess.py", line 1130, in _execute_child self.pid = os.fork() OSError: [Errno 12] Cannot allocate memory


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接