在Python中将标准输出重定向到文件?

430

如何在Python中将标准输出重定向到任意文件?

当一个长时间运行的Python脚本(例如web应用程序)在ssh会话内启动并进入后台运行,然后关闭ssh会话时,一旦它尝试写入标准输出(stdout),应用程序就会引发IOError并失败。我需要找到一种方法,使应用程序和模块输出到文件而不是标准输出(stdout),以防止由于IOError导致的失败。目前,我使用nohup将输出重定向到文件,并且这样做可以完成任务,但我很好奇是否有一种方法可以不使用nohup来实现这一点。

我已经尝试过sys.stdout = open('somefile', 'w'),但似乎这并不能阻止某些外部模块仍然将输出发送到终端(或者也许sys.stdout = ...行根本没有触发)。我知道从我测试过的较简单的脚本中,它应该起作用,但我还没有时间测试一个web应用程序。


12
这并不是Python的一部分,它是一个shell函数。只需像这样运行您的脚本:script.p > file - Falmarri
1
@foxbunny:nohup?为什么不直接使用“someprocess | python script.py”?为什么要涉及到“nohup”? - S.Lott
@S.Lott:经过更多的测试,我纠正了代码中的一些错误,现在(与您所声称的相反)stdout已经根据下面两个答案中的解决方案普遍重定向到文件。如果您仍然认为它不应该工作,那么您应该自己尝试一下。但是,如果您仍然不理解问题本身,那就很遗憾了。再次看看提供的两个答案,您应该能够弄清楚。 - user234932
6
请将“print”语句改为使用标准库中的“logging”模块。这样你就可以随时重定向输出、控制输出量等。在大多数情况下,生产代码不应该使用“print”,而应该使用“log”。请注意保持原意并使翻译通俗易懂。 - erikbstack
5
这个问题的更好解决方案或许是使用screen命令,它可以保存你的bash会话并允许你在不同的运行中访问它。 - Ryan Amos
显示剩余6条评论
15个回答

564

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

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

更常见的方法是在执行时使用 shell 重定向(在 Windows 和 Linux 上都适用):

A far more common method is to use shell redirection when executing (same on Windows and Linux):

$ python3 foo.py > file

5
如果您使用Windows,请注意Windows中的一个错误 - 当我只使用脚本名称运行Python脚本时,无法重定向输出 - Piotr Dobrogost
9
也许是因为它创建了一个本地副本,所以无法与 from sys import stdout 一起使用。您还可以使用 with,例如 with open('file', 'w') as sys.stdout: functionThatPrints()。现在,您可以使用普通的 print 语句来实现 functionThatPrints() - mgold
61
最好保留一份本地副本,stdout = sys.stdout,这样在完成后可以放回去,sys.stdout = stdout。这样,如果您正在从使用print的函数中调用,就不会搞砸它们。 - mgold
4
@Jan: buffering=0 表示禁用缓冲(这可能会对性能产生负面影响(10-100倍))。buffering=1 启用行缓冲,这样您就可以使用 tail -f 进行逐行输出。 - jfs
67
@mgold 或者你可以使用 sys.stdout = sys.__stdout__ 将其恢复。 - clemtoy
显示剩余11条评论

272

在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版本上使用。后者版本不可重用,如果需要可以将其制作成可重用的。

它不会在文件描述符级别上重定向stdout,例如:

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()上下文管理器,之前在标准输出(stdout)打印的内容就会被重定向到output.txt文件中。

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

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脚本。

3
stdout_redirected 是有帮助的。请注意,在 doctest 中它不起作用,因为 doctest 使用特殊的 SpoofOut 处理程序替换了 sys.stdout,而该处理程序没有 fileno 属性。 - Chris Johnson
@ChrisJohnson:如果它没有引发 ValueError("Expected a file (\.fileno()`) or a file descriptor")`,那么这就是一个 bug。你确定它不会引发吗? - jfs
它确实引发了那个错误,这就是使它在doctest中不可用的原因。为了在doctest中使用您的函数,似乎需要指定“doctest.sys.__stdout__”,而我们通常会使用“sys.stdout”。这不是您的函数的问题,只是对于doctest需要一种适应,因为它用一个没有所有真正文件属性的对象替换了stdout。 - Chris Johnson
stdout_redirected()函数有一个stdout参数,如果要重定向原始的Python stdout(在大多数情况下应该具有有效的.fileno()),则可以将其设置为sys.__stdout__。如果当前的sys.stdout与之不同,则无法执行任何操作。请勿使用doctest.sys,它是一种偶然性可用。 - jfs
也许不相关,但我认为os.dup2()示例不会影响子进程,至少不会影响使用subprocess.Popen()创建的子进程(或者至少不会影响某些子进程,如果这很重要的话,我不确定我的子进程是使用spawn还是fork)。我有一个测试套件,指定stdout=subprocess.DEVNULLPopen(),但没有为stderr指定任何内容,因此在测试期间会打印垃圾信息。在这种情况下,我将只指定stderr,但未来防止垃圾信息混入也是好的。 - David Winiecki
显示剩余5条评论

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

13
这将对假定sys.stdout是具有方法(例如fileno())的完整文件对象的代码产生影响(包括Python标准库中的代码)。我建议在该对象中添加一个__getattr__(self, attr)方法,将属性查找延迟到self.terminal。def __getattr__(self, attr): return getattr(self.terminal, attr) - peabody
7
你需要在 Logger 类中也添加一个名为 flush(self) 的方法。 - loretoparisi
1
@loretoparisi flush方法会做什么? - elkshadow5
1
@elkshadow5 flush操作将会在缓冲区中累积字符,然后打印并清空缓冲区。 - loretoparisi
3
不过,你创建的方法中实际上放了什么? - elkshadow5
显示剩余4条评论

32

其他答案没有涵盖您想让分叉进程共享新stdout的情况。

要实现这一点:

from os import open, close, dup, O_WRONLY

old = dup(1)
close(1)
open("file", O_WRONLY) # should open on 1

..... do stuff and then restore

close(1)
dup(old) # should dup to 1
close(old) # get rid of left overs

3
需要将'w'属性替换为os.O_WRONLY|os.O_CREATE...不能将字符串传入"os"命令! - Ch'marr
3
close(1)语句之前插入sys.stdout.flush()以确保重定向的'file'文件获取输出。此外,您可以使用tempfile.mkstemp()文件来代替'file'。并且要小心,确保没有其他线程在os.close(1)之后但在打开'file'以使用句柄之前窃取操作系统的第一个文件句柄。 - Alex Robinson
2
它是 os.O_WRONLY | os.O_CREAT ... 上面没有 E。 - Jeff Sheffield
@Ch'marr,应该是O_CREAT,而不是O_CREATE。 - quant_dev
dup 函数使文件描述符不可继承。这会导致在重定向完成后运行写入 stdout 的子进程时出现故障。 - frantisek

31

引用自PEP 343 -- "with"语句(添加导入语句):

暂时重定向stdout:


import sys
from contextlib import contextmanager
@contextmanager
def stdout_redirected(new_stdout):
    save_stdout = sys.stdout
    sys.stdout = new_stdout
    try:
        yield None
    finally:
        sys.stdout = save_stdout

使用方法如下:

with open(filename, "w") as f:
    with stdout_redirected(f):
        print "Hello world"

当然,这并不是线程安全的,但手动执行相同的操作也是如此。在单线程程序中(例如脚本),这是一种流行的做法。


2
+1. 注意:它不适用于子进程,例如 os.system('echo not redirected')我的答案展示了如何重定向这样的输出 - jfs
1
从Python 3.4开始,contextlib中有redirect_stdout - Walter Tross

14
import sys
sys.stdout = open('stdout.txt', 'w')

7

以下是对Yuda Prawira答案的改进:

  • 实现flush()和所有文件属性
  • 将其编写为上下文管理器
  • 还要捕获stderr

.

import contextlib, sys

@contextlib.contextmanager
def log_print(file):
    # capture all outputs to a log file while still printing it
    class Logger:
        def __init__(self, file):
            self.terminal = sys.stdout
            self.log = file

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

        def __getattr__(self, attr):
            return getattr(self.terminal, attr)

    logger = Logger(file)

    _stdout = sys.stdout
    _stderr = sys.stderr
    sys.stdout = logger
    sys.stderr = logger
    try:
        yield logger.log
    finally:
        sys.stdout = _stdout
        sys.stderr = _stderr


with log_print(open('mylogfile.log', 'w')):
    print('hello world')
    print('hello world on stderr', file=sys.stderr)

# you can capture the output to a string with:
# with log_print(io.StringIO()) as log:
#   ....
#   print('[captured output]', log.getvalue())

5
你需要一个终端复用器,比如 tmux 或者 GNU screen
我惊讶于 Ryan Amos 在最初的问题中发表的一句小评论是唯一提到的一个远比所有其他方案都更好的解决方案,无论那些 Python 的技巧有多聪明,以及它们获得了多少赞。除了 Ryan 的评论之外,tmux 是 GNU screen 的一个不错的替代品。
但原则是相同的:如果你想在注销后让终端作业继续运行,并且离开时想去咖啡厅吃个三明治、上个厕所、回家(等等),然后稍后无论你身在何处或使用哪台计算机,都可以重新连接到你的终端会话,就好像你从未离开过一样,终端复用器就是答案。把它们当作终端会话的 VNC 或远程桌面。任何其他东西都是权宜之计。作为一个额外的奖励,当老板和/或伴侣进来时,你无意中用 ctrl-w/cmd-w 关闭了终端窗口而不是带有瑕疵内容的浏览器窗口时,你不会失去过去 18 小时的处理内容!

4
虽然它对编辑后出现的问题部分是一个很好的答案,但它并没有回答标题中的问题(大多数人通过标题来到这里)。 - jfs

3
根据这个答案: https://dev59.com/7m445IYBdhLWcg3w1tgS#5916874,我发现了另一种方法,并在我的一个项目中使用。无论你用什么替换sys.stderr或者sys.stdout,你必须确保替换符合file接口的规范,特别是如果你这样做是因为stderr/stdout被某个不在你控制下的库使用。那个库可能正在使用文件对象的其他方法。
看看这种方式,在这种方式中我仍然让所有东西都去执行stderr/stdout(或任何文件)并且使用Python的日志记录方式将消息发送到日志文件中(但你真的可以对此做任何事情):
class FileToLogInterface(file):
    '''
    Interface to make sure that everytime anything is written to stderr, it is
    also forwarded to a file.
    '''

    def __init__(self, *args, **kwargs):
        if 'cfg' not in kwargs:
            raise TypeError('argument cfg is required.')
        else:
            if not isinstance(kwargs['cfg'], config.Config):
                raise TypeError(
                    'argument cfg should be a valid '
                    'PostSegmentation configuration object i.e. '
                    'postsegmentation.config.Config')
        self._cfg = kwargs['cfg']
        kwargs.pop('cfg')

        self._logger = logging.getlogger('access_log')

        super(FileToLogInterface, self).__init__(*args, **kwargs)

    def write(self, msg):
        super(FileToLogInterface, self).write(msg)
        self._logger.info(msg)

1
其他编程语言(例如C)编写的程序需要进行特殊的操作(称为双重分叉)才能从终端断开连接(并且防止僵尸进程)。因此,我认为最好的解决方案是模拟它们。
重新执行您的程序的一个好处是,您可以在命令行上选择重定向,例如:/usr/bin/python mycoolscript.py 2>&1 1>/dev/null 请参见此帖子以获取更多信息:创建守护进程时执行双重fork的原因是什么?

嗯...我不太喜欢进程管理自己的双重分叉。这是一个非常常见的习语,如果你不小心编码,很容易出错。最好编写您的进程以在前台运行,并使用系统后台任务管理器(systemdupstart)或其他实用程序(daemon(1))来处理分叉样板。 - Lucretiel

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