用Python将stdout重定向到文件?

439 投票
15 回答
769318 浏览
提问于 2025-04-16 09:52

我想知道如何在Python中把输出重定向到一个任意的文件。

当一个长时间运行的Python脚本(比如说一个网页应用)是在ssh会话中启动并放到后台运行的,而这个ssh会话又被关闭时,应用在尝试写入标准输出(stdout)时会出现IOError错误,导致失败。我需要找到一种方法,让应用和模块的输出直接写入文件,而不是写入标准输出,这样就可以避免因为IOError而失败。目前,我使用nohup命令来把输出重定向到一个文件,这样可以解决问题,但我很好奇有没有其他方法可以做到这一点,而不需要使用nohup。

我已经尝试过用sys.stdout = open('somefile', 'w')来重定向输出,但似乎并不能阻止一些外部模块仍然输出到终端(或者可能是sys.stdout = ...这一行根本没有执行)。我知道在我测试过的一些简单脚本中这个方法是有效的,但我还没有时间在网页应用上进行测试。

15 个回答

103

你可以试试这个,效果会好很多。

import sys

class Logger(object):
    def __init__(self, filename="Default.log"):
        self.terminal = sys.stdout
        self.log = open(filename, "a")

    def write(self, message):
        self.terminal.write(message)
        self.log.write(message)

sys.stdout = Logger("yourlogfilename.txt")
print "Hello world !" # this is should be saved in yourlogfilename.txt
278

在Python 3.4及以上版本中,有一个叫做contextlib.redirect_stdout()的函数

from contextlib import redirect_stdout

with open('help.txt', 'w') as f:
    with redirect_stdout(f):
        print('it now prints to `help.text`')

这个函数的用法和下面这个很相似:

import sys
from contextlib import contextmanager

@contextmanager
def redirect_stdout(new_target):
    old_target, sys.stdout = sys.stdout, new_target # replace sys.stdout
    try:
        yield new_target # run some code with the replaced stdout
    finally:
        sys.stdout = old_target # restore to the previous value

后者可以在早期的Python版本中使用。不过,后面的版本不能重复使用,如果需要的话,可以让它变得可重复使用。

这个函数并不是在文件描述符的层面上进行重定向,比如:

import os
from contextlib import redirect_stdout

stdout_fd = sys.stdout.fileno()
with open('output.txt', 'w') as f, redirect_stdout(f):
    print('redirected to a file')
    os.write(stdout_fd, b'not redirected')
    os.system('echo this also is not redirected')

b'not redirected''echo this also is not redirected'并不会被重定向到output.txt文件中。

如果想在文件描述符的层面上进行重定向,可以使用os.dup2()

import os
import sys
from contextlib import contextmanager

def fileno(file_or_fd):
    fd = getattr(file_or_fd, 'fileno', lambda: file_or_fd)()
    if not isinstance(fd, int):
        raise ValueError("Expected a file (`.fileno()`) or a file descriptor")
    return fd

@contextmanager
def stdout_redirected(to=os.devnull, stdout=None):
    if stdout is None:
       stdout = sys.stdout

    stdout_fd = fileno(stdout)
    # copy stdout_fd before it is overwritten
    #NOTE: `copied` is inheritable on Windows when duplicating a standard stream
    with os.fdopen(os.dup(stdout_fd), 'wb') as copied: 
        stdout.flush()  # flush library buffers that dup2 knows nothing about
        try:
            os.dup2(fileno(to), stdout_fd)  # $ exec >&to
        except ValueError:  # filename
            with open(to, 'wb') as to_file:
                os.dup2(to_file.fileno(), stdout_fd)  # $ exec > to
        try:
            yield stdout # allow code to be run with the redirected stdout
        finally:
            # restore stdout to its previous value
            #NOTE: dup2 makes stdout_fd inheritable unconditionally
            stdout.flush()
            os.dup2(copied.fileno(), stdout_fd)  # $ exec >&copied

如果用stdout_redirected()代替redirect_stdout(),同样的例子现在也可以正常工作:

import os
import sys

stdout_fd = sys.stdout.fileno()
with open('output.txt', 'w') as f, stdout_redirected(f):
    print('redirected to a file')
    os.write(stdout_fd, b'it is redirected now\n')
    os.system('echo this is also redirected')
print('this is goes back to stdout')

之前打印到标准输出的内容,现在只要stdout_redirected()这个上下文管理器处于活动状态,就会转到output.txt文件中。

注意:在Python 3中,stdout.flush()并不会刷新C标准输入输出缓冲区,因为I/O是直接在read()/write()系统调用上实现的。如果某个C扩展使用基于stdio的I/O,你可以显式调用libc.fflush(None)来刷新所有打开的C标准输出流:

try:
    import ctypes
    from ctypes.util import find_library
except ImportError:
    libc = None
else:
    try:
        libc = ctypes.cdll.msvcrt # Windows
    except OSError:
        libc = ctypes.cdll.LoadLibrary(find_library('c'))

def flush(stream):
    try:
        libc.fflush(None)
        stream.flush()
    except (AttributeError, ValueError, IOError):
        pass # unsupported

你还可以使用stdout参数来重定向其他流,不仅仅是sys.stdout,比如,可以将sys.stderrsys.stdout合并:

def merged_stderr_stdout():  # $ exec 2>&1
    return stdout_redirected(to=sys.stdout, stdout=sys.stderr)

示例:

from __future__ import print_function
import sys

with merged_stderr_stdout():
     print('this is printed on stdout')
     print('this is also printed on stdout', file=sys.stderr)

注意:stdout_redirected()会混合缓冲I/O(通常是sys.stdout)和无缓冲I/O(直接对文件描述符的操作)。要小心,可能会出现缓冲问题

至于你的编辑:你可以使用python-daemon来将你的脚本变成守护进程,并使用logging模块(正如@erikb85建议的)来替代print语句,仅仅重定向标准输出,适用于你现在使用nohup运行的长时间运行的Python脚本。

570

如果你想在Python脚本中进行重定向,可以把sys.stdout设置为一个文件对象,这样就可以实现了:

# for python3
import sys
with open('file', 'w') as sys.stdout:
    print('test')

另外一种更常见的方法是在执行时使用命令行重定向,这在Windows和Linux上都是一样的:

$ python3 foo.py > file

撰写回答