将stdout重定向到python中的文件?

Redirect stdout to a file in Python?

如何将stdout重定向到python中的任意文件?

当一个长时间运行的python脚本(例如,web应用程序)从ssh会话中启动并返回时,ssh会话关闭,应用程序将在尝试写入stdout时引发ioerror并失败。我需要找到一种方法使应用程序和模块输出到一个文件,而不是stdout,以防止由于ioerror而导致的失败。目前,我使用nohup将输出重定向到一个文件,这就完成了任务,但是出于好奇,我想知道是否有一种方法可以不用nohup来完成输出。

我已经尝试过sys.stdout = open('somefile', 'w'),但这似乎并不能阻止一些外部模块仍然输出到终端(或者可能sys.stdout = ...线路根本没有点火)。我知道它应该从我测试过的更简单的脚本开始工作,但是我还没有时间在Web应用程序上测试。


如果要在python脚本中进行重定向,请将sys.stdout设置为文件对象,这样做:

1
2
3
import sys
sys.stdout = open('file', 'w')
print('test')

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

1
$ python foo.py > file


python 3.4中有contextlib.redirect_stdout()函数:

1
2
3
4
5
from contextlib import redirect_stdout

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

类似于:

1
2
3
4
5
6
7
8
9
10
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,例如:

1
2
3
4
5
6
7
8
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()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
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(),那么现在也可以使用相同的示例:

1
2
3
4
5
6
7
8
9
10
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
'
)
    os.system('echo this is also redirected')
print('this is goes back to stdout')

只要stdout_redirected()上下文管理器处于活动状态,以前在stdout上打印的输出现在就转到output.txt

注:stdout.flush()不冲洗c python 3上的stdio缓冲区,其中I/O直接在read()/write()系统调用上实现。要刷新所有打开的C stdio输出流,如果某些C扩展使用基于stdio的I/O,可以显式调用libc.fflush(None)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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

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

例子:

1
2
3
4
5
6
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脚本重定向stdout。


你可以试试这个太好了

1
2
3
4
5
6
7
8
9
10
11
12
13
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


其他的答案不包括希望分叉进程共享新stdout的情况。

这样做:

1
2
3
4
5
6
7
8
9
10
11
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


引自PEP 343——"with"声明(添加进口声明):

临时重定向stdout:

1
2
3
4
5
6
7
8
9
10
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

使用方法如下:

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

当然,这不是线程安全的,但也不是手动执行相同的舞蹈。在单线程程序中(例如在脚本中),这是一种常用的处理方法。


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


你需要一个终端多路复用器,比如TMUX或GNU屏幕

令我惊讶的是,Ryan Amos对最初问题的一个小小的评论是唯一一个比所有其他人都更可取的解决方案,不管python的诡计有多聪明,他们收到了多少赞成票。除了Ryan的评论,TMUX是GNU屏幕的一个不错的替代品。

但是原理是一样的:如果你想在注销时离开一个终端工作,去咖啡馆吃三明治,跳到浴室,回家(等等),然后从任何地方或任何计算机重新连接到终端会话,就像你从未离开过一样,终端多路复用器就是答案。把它们当作终端会话的VNC或远程桌面。任何其他事情都是解决办法。作为额外的好处,当老板和/或合作伙伴进来时,你不经意地在终端窗口中按ctrl-w/cmd-w,而不是在浏览器窗口中输入不可靠的内容,你就不会损失过去18小时的处理时间!


基于这个答案:https://stackoverflow.com/a/5916874/1060344,这里是我在一个项目中使用的另一种方法。无论您用什么替换sys.stderrsys.stdout,您都必须确保替换符合file接口,特别是当您这样做是因为stderr/stdout在您无法控制的其他库中使用时。该库可能正在使用文件对象的其他方法。

在这里,我仍然让所有的事情都执行stderr/stdout(或任何与此相关的文件),并使用python的日志工具将消息发送到日志文件(但您确实可以对此做任何操作):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
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)

用其他语言(如C)编写的程序必须执行特殊的魔法(称为双分叉),以明确地从终端分离(并防止僵尸进程)。所以,我认为最好的解决方案是模仿它们。

重新执行程序的另一个好处是,您可以在命令行上选择重定向,例如/usr/bin/python mycoolscript.py 2>&1 1>/dev/null

有关更多信息,请参阅本文:创建守护进程时执行双分叉的原因是什么?


马科格

第二个选项只有当脚本在go中执行时才是好的。或者脚本应该完全执行,然后输出进入该文件,并且不应该出现无限循环(最佳)。最好的解决方案,如果它是一个简单的脚本。


以下是Yuda Prawira答案的变体:

  • 实现flush()和所有文件属性
  • 作为ContextManager编写
  • 捕获stderr

.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
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())