Python中的StringIO在使用subprocess.call()时无法作为文件使用

10

我正在使用subprocess包从Python脚本中调用一些外部控制台命令,并且我需要传递文件处理程序以单独获取stdoutstderr。代码大致如下:

import subprocess

stdout_file = file(os.path.join(local_path, 'stdout.txt'), 'w+')
stderr_file = file(os.path.join(local_path, 'stderr.txt'), 'w+')

subprocess.call(["somecommand", "someparam"], stdout=stdout_file, stderr=stderr_file)

这样做很好,可以创建与相关输出对应的txt文件。但是,更好的方法是在内存中处理这些输出,避免创建文件。因此我使用StringIO包以以下方式处理:

import subprocess
import StringIO

stdout_file = StringIO.StringIO()
stderr_file = StringIO.StringIO()

subprocess.call(["somecommand", "someparam"], stdout=stdout_file, stderr=stderr_file)

但是这样不起作用。会失败并显示:

  File "./test.py", line 17, in <module>
    subprocess.call(["somecommand", "someparam"], stdout=stdout_file, stderr=stderr_file)
  File "/usr/lib/python2.7/subprocess.py", line 493, in call
    return Popen(*popenargs, **kwargs).wait()
  File "/usr/lib/python2.7/subprocess.py", line 672, in __init__
    errread, errwrite) = self._get_handles(stdin, stdout, stderr)
  File "/usr/lib/python2.7/subprocess.py", line 1063, in _get_handles
    c2pwrite = stdout.fileno()
AttributeError: StringIO instance has no attribute 'fileno'

我看到它缺少一些本地文件对象的部分,因此失败了。

因此,这个问题更多是教育性质而不是实用性质——为什么StringIO缺少文件接口的这些部分,是否有任何原因导致无法实现?


这有点类似于 subprocess.check_output 的作用。 - Blender
问题在于 subprocess.check_output 把所有输出作为一个字符串抛出,而我需要将 stdoutstderr 分开。 - alexykot
嗯,找到了一个解决方法,使用Popen代替subprocess。在这里解释了https://dev59.com/Tmkw5IYBdhLWcg3wJXMh - alexykot
selffix - Popen是subprocess的一部分,因此可以使用Popen代替subprocess.call()或subprocess.check_output()。 - alexykot
请参见https://dev59.com/Km025IYBdhLWcg3wnXWf。是的,这是标准库中的一个错误。 - Charles Merriam
原帖发布于2013年。解决此问题不再是我的首要任务,但还是感谢。 - alexykot
3个回答

10
正如您在评论中所说的那样,PopenPopen.communicate在这里是正确的解决方案。
一些背景知识:真实的文件对象拥有文件描述符,这是fileno属性,而StringIO对象则缺少该属性。它们只是普通整数:你可能熟悉文件描述符0、1和2,它们分别是标准输入(stdin)、标准输出(stdout)和标准错误(stderr)。如果一个进程打开更多的文件,它们将被分配到3、4、5等等。您可以使用lsof -p查看进程当前的文件描述符。
那么,为什么StringIO对象不能有文件描述符呢?为了获得一个,它需要打开一个文件或打开一个管道*。打开文件没有意义,因为使用StringIO的整个目的就是不打开文件。
即使像StringIO对象一样存储在内存中,打开管道也没有意义。它们是用于通信而不是存储的:seektruncatelen对于管道根本没有意义,readwrite的行为与文件大不相同。当您从管道中read数据时,返回的数据将从管道的缓冲区中删除,如果当您尝试write时该(相对较小的)缓冲区已满,则您的进程将挂起,直到有人read管道以释放缓冲区空间。
因此,如果您想将字符串用作子进程的stdinstdoutstderrStringIO就不够用了,但Popen.communicate非常适合。如上所述(并在subprocess文档中警告),正确读写管道是很复杂的。Popen为您处理了这种复杂性。
* 我想我可以理论上想象第三种文件描述符,对应于进程之间共享的内存区域?不太确定为什么它不存在。但嗯,我不是内核开发人员,所以我肯定有原因。

3
这份文档存在误导。它说stdout的值可以是“一个已存在的文件对象”,但未提到该文件对象必须具有fileno属性。 - Franklin Yu
回溯信息也过于晦涩,如果子进程对文件对象添加了特定要求,则应尽早检测到良好的错误消息并表达它,而不是接受任何发生的鸭子类型。 - Erik Carstensen

0
如果你想实时将 stdoutstderr 重定向到一个 StringIO,你需要同时进行。以下是使用 Python 3.11 中的 asyncio 的示例:
import asyncio
import io
from subprocess import SubprocessError

# Maximum number of bytes to read at once from the 'asyncio.subprocess.PIPE'
_MAX_BUFFER_CHUNK_SIZE = 1024

# Buffers for stdout and stderr
stdout_buffer = io.StringIO()
stderr_buffer = io.StringIO()

async def run_cmd_async(command, check=False):
    process = await asyncio.subprocess.create_subprocess_exec(
        *command,
        stdout=asyncio.subprocess.PIPE,
        stderr=asyncio.subprocess.PIPE)

    async def write_stdout() -> None:
        assert process.stdout is not None
        while chunk := await process.stdout.read(_MAX_BUFFER_CHUNK_SIZE):
            stdout_buffer.write(chunk.decode())

    async def write_stderr() -> None:
        assert process.stderr is not None
        while chunk := await process.stderr.read(_MAX_BUFFER_CHUNK_SIZE):
            stderr_buffer.write(chunk.decode())

    async with asyncio.TaskGroup() as task_group:
        task_group.create_task(write_stdout())
        task_group.create_task(write_stderr())

        exit_code = await process.wait()
        if check and exit_code != 0:
            raise SubprocessError(
                f"Command '{command}' returned non-zero exit status {exit_code}."
            )
    return exit_code


# Run your command and print output
asyncio.run(run_cmd_async(["somecommand", "someparam"], check=True))
print(stdout_buffer.getvalue())
print(stderr_buffer.getvalue())

然后,您可以添加一个单独的异步任务,以获取stdout和stderr缓冲区的当前值,并实时处理它们。

-3
我认为你期望其他进程能够从主进程中以流的形式读取内存。也许,如果你可以将流导入标准输入并将标准输出导入你的流中,你可能会成功。

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