【问题标题】:subprocess.Popen stdin read filesubprocess.Popen 标准输入读取文件
【发布时间】:2014-03-14 22:37:05
【问题描述】:

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

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 之前没有从 a 中读取任何内容,这可以正常工作,但如果我从中读取任何内容,则子进程看不到任何内容。这是使用 python 2.7.3。我在文档中找不到任何解释此行为的内容,并且(非常)简短地查看子流程源并没有发现原因。

【问题讨论】:

  • 我猜“header = a.readline()”会导致 Python 解释器的 stdio 通过消除对某些系统调用的需要来缓冲 in.txt 的一部分以提高性能。如果 in.txts 不是很大,你最好使用 Python 的 list_.sort()。

标签: python subprocess


【解决方案1】:

如果你打开没有缓冲的文件,那么它可以工作:

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 模块在文件描述符级别(操作系统的低级别无缓冲 I/O)工作。如果操作系统支持,它可以与os.pipe()socket.socket()pty.openpty() 以及任何具有有效.fileno() 方法的东西一起使用。

不建议在同一个文件中混合使用缓冲和非缓冲 I/O。

在 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() 报告所有缓冲区大小的第 2 行开头的位置。由于read-ahead buffer bug,使用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(),都会产生各种结果。它确认在同一个文件描述符上混合缓冲和非缓冲 I/O 不是一个好主意

【讨论】:

    【解决方案2】:

    这是因为子进程模块从文件对象中提取了文件句柄。

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

    在第 1126 行,来自 701。

    文件对象使用缓冲区并且在子进程提取文件时已经从文件句柄中读取了很多内容。

    【讨论】:

    • 感谢您对此行为的解释。会接受,但 J.F. 打败了你。
    【解决方案3】:

    正如@jfs 所说 使用 popen 时,它会将文件描述符传递给进程, 同时python读取块(例如4096字节), 结果是 fd 级别的位置与您预期的不同。

    我在 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)
    

    【讨论】: