subprocess.Popen 读取 stdin 文件

14 投票
3 回答
11980 浏览
提问于 2025-04-17 22:23

我正在尝试在读取文件的一部分后调用一个进程。例如:

with open('in.txt', 'r') as a, open('out.txt', 'w') as b:
  header = a.readline()
  subprocess.call(['sort'], stdin=a, stdout=b)

如果我在调用subprocess.call之前什么都不读取,这样是没问题的,但如果我从文件中读取了任何内容,子进程就看不到任何东西。这是使用python 2.7.3的情况。我在文档中找不到解释这种行为的内容,快速看了一下subprocess的源代码也没有发现原因。

3 个回答

1

正如@jfs提到的,当你使用popen的时候,它会把文件描述符传递给进程。同时,Python会分块读取数据(比如每次读取4096字节)。这样一来,文件描述符的位置就和你预期的会有所不同。

我在Python 2.7中通过调整文件描述符的位置来解决了这个问题。

_file = open(some_path)
_file.read(codecs.BOM_UTF8)
os.lseek(_file.fileno(), _file.tell(), os.SEEK_SET)
truncate_null_cmd = ['tr','-d', '\\000']
subprocess.Popen(truncate_null_cmd, stdin=_file, stdout=subprocess.PIPE)
4

这个问题发生是因为 subprocess 模块从文件对象中提取了文件句柄。

http://hg.python.org/releasing/2.7.6/file/ba31940588b6/Lib/subprocess.py

在第1126行,来自701行。

文件对象使用了缓冲区,当 subprocess 提取文件句柄时,它已经从文件句柄中读取了很多内容。

14

如果你以无缓冲的方式打开文件,那么它就能正常工作:

import subprocess

with open('in.txt', 'rb', 0) as a, open('out.txt', 'w') as b:
    header = a.readline()
    rc = subprocess.call(['sort'], stdin=a, stdout=b)

subprocess模块是在文件描述符的层面上工作的(也就是操作系统的低级无缓冲输入输出)。它可以和os.pipe()socket.socket()pty.openpty()等一起使用,只要这些东西有有效的.fileno()方法,并且操作系统支持。

不建议在同一个文件上混合使用缓冲和无缓冲的输入输出。

在Python 2中,file.flush()会让输出立刻显示出来,例如:

import subprocess
# 2nd
with open(__file__) as file:
    header = file.readline()
    file.seek(file.tell()) # synchronize (for io.open and Python 3)
    file.flush()           # synchronize (for C stdio-based file on Python 2)
    rc = subprocess.call(['cat'], stdin=file)

这个问题在没有使用subprocess模块的情况下也可以通过os.read()重现:

#!/usr/bin/env python
# 2nd
import os

with open(__file__) as file: #XXX fully buffered text file EATS INPUT
    file.readline() # ignore header line
    os.write(1, os.read(file.fileno(), 1<<20))

如果缓冲区的大小很小,那么文件的其余部分会被打印出来:

#!/usr/bin/env python
# 2nd
import os

bufsize = 2 #XXX MAY EAT INPUT
with open(__file__, 'rb', bufsize) as file:
    file.readline() # ignore header line
    os.write(2, os.read(file.fileno(), 1<<20))

如果第一行的大小不能被bufsize整除,那么会消耗更多的输入。

在我的机器上,默认的bufsizebufsize=1(行缓冲)表现得很相似:文件的开头部分会消失,大约是4KB。

file.tell()在所有缓冲区大小下都报告第二行的开头位置。使用next(file)代替file.readline()会导致在我的机器上Python 2中file.tell()的值大约在5K,这个是因为一个预读缓冲区的错误io.open()会给出预期的第二行位置)。

在调用子进程之前尝试file.seek(file.tell())在Python 2的默认基于stdio的文件对象上并没有帮助。使用Python 2中的io_pyio模块的open()函数,以及Python 3中的默认open(也是基于io的)则可以正常工作。

在Python 2和Python 3中尝试io_pyio模块,无论是否使用file.flush(),都会产生不同的结果。这进一步证实了在同一个文件描述符上混合使用缓冲和无缓冲的输入输出并不是个好主意

撰写回答