เปลี่ยนเส้นทาง stdout เป็นไฟล์ใน Python หรือไม่?


314

ฉันจะเปลี่ยนเส้นทาง stdout ไปยังไฟล์ที่กำหนดเองใน Python ได้อย่างไร

เมื่อสคริปต์ Python ที่รันเป็นเวลานาน (เช่นเว็บแอปพลิเคชัน) เริ่มต้นจากภายในเซสชัน ssh และ backgounded และปิดเซสชัน ssh แอปพลิเคชันจะเพิ่ม IOError และล้มเหลวทันทีที่พยายามเขียนไปยัง stdout ฉันต้องการค้นหาวิธีที่จะทำให้แอปพลิเคชันและโมดูลส่งออกไปยังไฟล์แทนที่จะเป็น stdout เพื่อป้องกันความล้มเหลวเนื่องจาก IOError ขณะนี้ฉันใช้ nohup เพื่อเปลี่ยนเส้นทางการส่งออกไปยังไฟล์และนั่นทำให้งานเสร็จ แต่ฉันสงสัยว่ามีวิธีที่จะทำหรือไม่โดยไม่ต้องใช้ nohup จากความอยากรู้

ฉันได้ลองไปแล้วsys.stdout = open('somefile', 'w')แต่นี่ดูเหมือนจะไม่ป้องกันบางโมดูลภายนอกที่ยังคงส่งสัญญาณออกไปยังเทอร์มินัล (หรืออาจเป็นเพราะsys.stdout = ...สายไม่ได้ทำงานเลย) ฉันรู้ว่ามันควรจะทำงานได้จากสคริปต์ที่เรียบง่ายกว่าที่ฉันเคยทดสอบ แต่ฉันยังไม่มีเวลาที่จะทำการทดสอบกับเว็บแอปพลิเคชัน


8
นั่นไม่ใช่สิ่งที่หลามจริงๆมันคือฟังก์ชันเชลล์ เพียงเรียกใช้สคริปต์ของคุณเช่นscript.p > file
Falmarri

ขณะนี้ฉันแก้ปัญหาโดยใช้ nohup แต่ฉันคิดว่าอาจมีบางสิ่งที่ฉลาดกว่า ...

1
@foxbunny: nohup? ทำไมเพียงsomeprocess | python script.py? ทำไมถึงเกี่ยวข้องกับnohup?
S.Lott

3
เขียนprintคำสั่งใหม่เพื่อใช้loggingโมดูลจาก stdlib จากนั้นคุณสามารถเปลี่ยนเส้นทางออกทุกที่มีวิธีการควบคุมการส่งออกที่คุณต้องการ ฯลฯ ในที่สุดรหัสการผลิตกรณีไม่ควรแต่print log
erikbwork

2
บางทีทางออกที่ดีกว่าสำหรับปัญหานี้คือคำสั่งหน้าจอซึ่งจะบันทึกเซสชัน bash ของคุณและอนุญาตให้คุณเข้าถึงจากการทำงานที่แตกต่างกัน
Ryan Amos

คำตอบ:


403

หากคุณต้องการเปลี่ยนเส้นทางภายในสคริปต์ Python การตั้งค่าsys.stdoutเป็นวัตถุไฟล์จะทำตามขั้นตอนดังนี้:

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

วิธีการทั่วไปที่ใช้กันมากคือการใช้การเปลี่ยนเส้นทางของเชลล์เมื่อดำเนินการ (เหมือนกันใน Windows และ Linux):

$ python foo.py > file


7
มันใช้งานไม่ได้from sys import stdoutอาจเป็นเพราะสร้างสำเนาในเครื่อง นอกจากนี้คุณสามารถใช้มันด้วยเช่นwith with open('file', 'w') as sys.stdout: functionThatPrints()ตอนนี้คุณสามารถใช้งานได้functionThatPrints()โดยใช้printคำสั่งปกติ
mgold

41
มันเป็นเรื่องที่ดีที่สุดเพื่อเก็บสำเนาท้องถิ่นเพื่อให้คุณสามารถนำมันกลับมาเมื่อคุณทำเสร็จแล้วstdout = sys.stdout sys.stdout = stdoutด้วยวิธีนี้ถ้าคุณถูกเรียกจากฟังก์ชั่นที่ใช้printคุณจะไม่พลาด
mgold

4
@Jan: buffering=0ปิดใช้งานการบัฟเฟอร์ (มันอาจส่งผลเสียต่อประสิทธิภาพ (10-100 ครั้ง)) buffering=1เปิดใช้งานการบัฟเฟอร์บรรทัดเพื่อให้คุณสามารถใช้tail -fสำหรับเอาต์พุตที่มุ่งเน้นบรรทัด
jfs

41
@ mgold หรือคุณสามารถใช้sys.stdout = sys.__stdout__เพื่อรับคืน
clemtoy

176

มีcontextlib.redirect_stdout()ฟังก์ชั่นใน Python 3.4:

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 ที่ระดับ file descriptors เช่น:

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 จะไปoutput.txtที่ตราบใดที่stdout_redirected()ตัวจัดการบริบททำงานอยู่

หมายเหตุ: stdout.flush()ไม่ต้องล้างข้อมูลบัฟเฟอร์ C stdio บน Python 3 โดยที่ I / O จะถูกนำไปใช้กับการโทรread()/ write()ระบบโดยตรง ในการล้างข้อมูลทั้งหมดของสตรีมเอาท์พุท C stdio คุณสามารถโทรได้libc.fflush(None)อย่างชัดเจนหากส่วนขยาย C บางตัวใช้ I / O แบบอิง 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.stderrและsys.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-daemondaemonize สคริปต์ของคุณและใช้loggingโมดูล (ตามที่แนะนำ @ erikb85 ) แทนprintข้อความสั่งและเพียงแค่เปลี่ยนเส้นทาง stdout สำหรับสคริปต์ Python ที่รันมานานซึ่งคุณใช้งานnohupอยู่ตอนนี้


3
stdout_redirectedมีประโยชน์ ระวังสิ่งนี้ไม่ทำงานภายใน doctests เนื่องจากSpoofOutdoctest ตัวจัดการพิเศษที่ใช้ในการแทนที่sys.stdoutไม่มีfilenoแอตทริบิวต์
Chris Johnson

@ChrisJohnson: ถ้ามันไม่ยกValueError("Expected a file (`.fileno()`) or a file descriptor")มันก็เป็นข้อผิดพลาด คุณแน่ใจหรือว่าไม่ได้เพิ่ม
jfs

มันทำให้เกิดข้อผิดพลาดขึ้นซึ่งเป็นสิ่งที่ทำให้มันไม่สามารถใช้งานได้ภายในหลักคำสอน ในการใช้ฟังก์ชั่นของคุณภายใน doctest มันจำเป็นต้องระบุว่าdoctest.sys.__stdout__ปกติแล้วเราจะใช้ที่sys.stdoutใด นี่ไม่ใช่ปัญหาของฟังก์ชั่นของคุณเพียงแค่ต้องการที่พักสำหรับ doctest เนื่องจากมันจะแทนที่ stdout ด้วยวัตถุที่ไม่มีคุณสมบัติทั้งหมดที่ไฟล์จริงต้องการ
Chris Johnson

stdout_redirected()มีstdoutพารามิเตอร์คุณสามารถตั้งค่าเป็นsys.__stdout__หากคุณต้องการเปลี่ยนเส้นทาง python stdout ดั้งเดิม (ซึ่งควรมีค่าที่ถูกต้อง.fileno()ในกรณีส่วนใหญ่) มันไม่ได้ทำอะไรเลยในปัจจุบันsys.stdoutหากพวกเขาแตกต่างกัน อย่าใช้doctest.sys; มันสามารถใช้ได้โดยไม่ได้ตั้งใจ
jfs

วิธีนี้ใช้งานได้ดีจริง ๆ เช่นเปลี่ยนเส้นทาง stdout และ stderr ไปยัง fd: with stdout_redirected(to=fd): with merged_stderr_stdout(): print('...'); print('...', file=sys.stderr)
neok

91

คุณสามารถลองสิ่งนี้ดีกว่ามากเกินไป

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

ข้อเสนอแนะใด ๆ สำหรับการไพพ์ถึงloggerหรือsyslog?
dsummersl

หากคุณต้องการแก้ไขไฟล์สิ่งนี้ไม่มีประโยชน์มาก อย่างไรก็ตาม +1 สำหรับเคล็ดลับที่ดี
aikid

10
สิ่งนี้จะมีผลตามมาสำหรับรหัสที่ถือว่า sys.stdout เป็นวัตถุไฟล์แบบเต็มด้วยวิธีการเช่น fileno () (ซึ่งรวมถึงรหัสในไลบรารีมาตรฐานของไพ ธ อน) ฉันจะเพิ่มเมธอด __getattr __ (self, attr) ให้กับสิ่งที่ทำให้การค้นหาแอททริบิวต์ลงใน self.terminal def __getattr__(self, attr): return getattr(self.terminal, attr)
พีบอดี

4
คุณจะต้องเพิ่มวิธีการเช่นเดียวกับการเรียนdef flush(self): Logger
loretoparisi

1
@loretoparisi แต่สิ่งที่จริงไปในวิธีการที่คุณสร้าง?
elkshadow5

28

คำตอบอื่น ๆ ไม่ได้ครอบคลุมกรณีที่คุณต้องการให้กระบวนการที่แยกกันเพื่อแบ่งปัน 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
แทรกคำสั่งsys.stdout.flush()ก่อนหน้าclose(1)เพื่อให้แน่ใจว่า'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

28

ยกมาจากPEP 343 - คำสั่ง "กับ" (คำสั่งนำเข้าเพิ่ม):

เปลี่ยนเส้นทาง 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"

แน่นอนว่าไม่ปลอดภัยสำหรับเธรด แต่ไม่มีการเต้นแบบเดียวกันนี้ด้วยตนเอง ในโปรแกรมแบบเธรดเดียว (ตัวอย่างเช่นในสคริปต์) มันเป็นวิธีที่ได้รับความนิยมในการทำสิ่งต่าง ๆ


1
+1 หมายเหตุ: os.system('echo not redirected')มันไม่ทำงานสำหรับกระบวนการย่อยเช่น คำตอบของฉันแสดงวิธีการเปลี่ยนเส้นทางผลลัพธ์ดังกล่าว
jfs

เริ่มต้นจาก Python 3.4 มีredirect_stdoutในcontextlib
Walter Tross


3

นี่คือรูปแบบของคำตอบ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())

2

ตามคำตอบนี้: https://stackoverflow.com/a/5916874/1060344นี่เป็นอีกวิธีหนึ่งที่ฉันคิดว่าฉันใช้ในโครงการใดโครงการหนึ่งของฉัน สำหรับสิ่งที่คุณเปลี่ยน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)

2

คุณต้องมีเทอร์มินัลมัลติเพล็กเซอร์เช่นtmuxหรือหน้าจอ GNU

ฉันประหลาดใจที่ความคิดเห็นเล็ก ๆ ของ Ryan Amos 'สำหรับคำถามดั้งเดิมเป็นการกล่าวถึงเพียงวิธีเดียวที่ดีกว่าที่คนอื่น ๆ ทุกคนเสนอให้ นอกจากความเห็นของไรอันแล้ว tmux ยังเป็นทางเลือกที่ดีสำหรับหน้าจอ GNU

แต่หลักการก็เหมือนกัน: ถ้าคุณเคยคิดว่าตัวเองต้องการออกจากงานเทอร์มินัลขณะที่คุณออกจากระบบมุ่งหน้าไปที่คาเฟ่เพื่อทำแซนวิชป๊อปไปที่ห้องน้ำกลับบ้าน (ฯลฯ ) จากนั้นเชื่อมต่อกับ ขั้วเซสชั่นจากที่ใดก็ได้หรือคอมพิวเตอร์ใด ๆ ที่เป็นแม้ว่าคุณจะไม่เคยออกไป multiplexers ขั้วมีคำตอบ คิดว่ามันเป็น VNC หรือเดสก์ท็อประยะไกลสำหรับเซสชันเทอร์มินัล สิ่งอื่นใดคือวิธีแก้ปัญหา เป็นโบนัสเมื่อเจ้านายและ / หรือหุ้นส่วนเข้ามาและคุณตั้งใจ ctrl-w / cmd-w หน้าต่างเทอร์มินัลแทนหน้าต่างเบราว์เซอร์ของคุณที่มีเนื้อหาหลบคุณจะไม่สูญเสียการประมวลผล 18 ชั่วโมงที่ผ่านมา !


4
ในขณะที่มันเป็นคำตอบที่ดีสำหรับส่วนของคำถามที่ปรากฏหลังจากแก้ไข; ไม่ตอบคำถามในชื่อเรื่อง (คนส่วนใหญ่มาที่นี่จาก google สำหรับชื่อเรื่อง)
jfs

0

โปรแกรมที่เขียนในภาษาอื่น (เช่น C) ต้องทำเวทย์มนตร์พิเศษ (เรียกว่าการฟอร์กสองครั้ง) โดยชัดแจ้งเพื่อแยกออกจากเทอร์มินัล (และเพื่อป้องกันกระบวนการซอมบี้) ดังนั้นฉันคิดว่าทางออกที่ดีที่สุดคือเลียนแบบพวกเขา

ข้อดีของการรันโปรแกรมอีกครั้งคือคุณสามารถเลือกการเปลี่ยนเส้นทางในบรรทัดคำสั่งได้ /usr/bin/python mycoolscript.py 2>&1 1>/dev/null

ดูโพสต์นี้สำหรับข้อมูลเพิ่มเติม: อะไรคือสาเหตุของการแยกแยะสองครั้งเมื่อสร้างดีมอน


เอ๊ะ ... ไม่สามารถพูดได้ว่าฉันเป็นแฟนตัวยงของกระบวนการจัดการการตีสองครั้งของตัวเอง มันเป็นสำนวนที่ใช้กันทั่วไปและรหัสผิดง่ายถ้าคุณไม่ระวัง ดีกว่าที่จะเขียนกระบวนการของคุณให้ทำงานในเบื้องหน้าและใช้ตัวจัดการงานพื้นหลังของระบบ ( systemd, upstart) หรือยูทิลิตี้อื่น ๆ ( daemon(1)) เพื่อจัดการกับแผ่นเหล็กสำหรับตีส้อม
Lucretiel
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.