sys.stdoutをログファイルに複製する方法


149

編集:解決策がないか、誰も知らないような非標準的なことをしているようです-私は質問を修正して次のことも尋ねます:pythonアプリが多くのシステムコール?

私のアプリには2つのモードがあります。インタラクティブモードでは、すべての出力を画面とログファイル(システムコールからの出力を含む)に出力します。デーモンモードでは、すべての出力がログに送られます。デーモンモードは、を使用するとうまく機能しos.dup2()ます。すべてのシステムコールを変更せずに、インタラクティブモードでログへのすべての出力を「ティー」する方法を見つけることができません。


言い換えれば、システムコールの出力を含め、Pythonアプリによって生成されたすべての出力に対して、コマンドライン 'Tee'の機能が必要です。

明確にするために:

すべての出力をリダイレクトするには、次のようにします。

# open our log file
so = se = open("%s.log" % self.name, 'w', 0)

# re-open stdout without buffering
sys.stdout = os.fdopen(sys.stdout.fileno(), 'w', 0)

# redirect stdout and stderr to the log file opened above
os.dup2(so.fileno(), sys.stdout.fileno())
os.dup2(se.fileno(), sys.stderr.fileno())

これの良い点は、残りのコードからの特別な印刷呼び出しを必要としないことです。このコードはいくつかのシェルコマンドも実行するため、それぞれの出力を個別に処理する必要がないのも便利です。

単純に、リダイレクトではなく複製を除いて、同じことをしたいのです。

最初は、単にを逆にするだけdup2でうまくいくと思いました。なぜそうではないのですか?これが私のテストです:

import os, sys

### my broken solution:
so = se = open("a.log", 'w', 0)
sys.stdout = os.fdopen(sys.stdout.fileno(), 'w', 0)

os.dup2(sys.stdout.fileno(), so.fileno())
os.dup2(sys.stderr.fileno(), se.fileno())
###

print("foo bar")

os.spawnve("P_WAIT", "/bin/ls", ["/bin/ls"], {})
os.execve("/bin/ls", ["/bin/ls"], os.environ)

ファイル「a.log」は、画面に表示されたものと同一である必要があります。


manページ(manpagez.com/man/2/dup2)を見ると、dup2の2番目の引数は常に閉じています(すでに開いている場合)。そのため、「壊れたソリューション」では、soとseを閉じてから、ファイル番号をsys.stdoutに再割り当てします。
Jacob Gabrielson、

1
再:あなたの編集:これは珍しいことではありません、私は数回(他の言語で)同様のことをしました。Unixは同じファイルハンドルに対して複数の「エイリアス」を許可しますが、ファイルハンドルを「分割」しません(他の複数にコピーします)。したがって、自分で「Tee」を実装する必要があります(または単に「Tee」を使用してください。私の大まかな答えを参照してください)。
Jacob Gabrielson

JohnTの回答は、実際に受け入れられている回答よりも優れていると思います。受け入れられた回答を変更することができます。
フォン

「私はとても非標準的なことをしています」-あなたは本当に、人々はログをstderrに送信し、コマンドラインから処理します。
khachik

回答:


55

コードから外部プロセスを生成することに慣れているので、teeそれ自体を使用できます。私は正確に何をするかUnixシステムコールを知りませんtee

# Note this version was written circa Python 2.6, see below for
# an updated 3.3+-compatible version.
import subprocess, os, sys

# Unbuffer output (this ensures the output is in the correct order)
sys.stdout = os.fdopen(sys.stdout.fileno(), 'w', 0)

tee = subprocess.Popen(["tee", "log.txt"], stdin=subprocess.PIPE)
os.dup2(tee.stdin.fileno(), sys.stdout.fileno())
os.dup2(tee.stdin.fileno(), sys.stderr.fileno())

print "\nstdout"
print >>sys.stderr, "stderr"
os.spawnve("P_WAIT", "/bin/ls", ["/bin/ls"], {})
os.execve("/bin/ls", ["/bin/ls"], os.environ)

また、エミュレート可能性がtee使用してマルチプロセッシング(または使用パッケージ処理を、あなたは、Python 2.5またはそれ以前のバージョンを使用している場合)。

更新

Python 3.3+互換バージョンは次のとおりです。

import subprocess, os, sys

tee = subprocess.Popen(["tee", "log.txt"], stdin=subprocess.PIPE)
# Cause tee's stdin to get a copy of our stdin/stdout (as well as that
# of any child processes we spawn)
os.dup2(tee.stdin.fileno(), sys.stdout.fileno())
os.dup2(tee.stdin.fileno(), sys.stderr.fileno())

# The flush flag is needed to guarantee these lines are written before
# the two spawned /bin/ls processes emit any output
print("\nstdout", flush=True)
print("stderr", file=sys.stderr, flush=True)

# These child processes' stdin/stdout are 
os.spawnve("P_WAIT", "/bin/ls", ["/bin/ls"], {})
os.execve("/bin/ls", ["/bin/ls"], os.environ)

28
まあ、この答えはうまくいくので、受け入れます。それでも、それは私が作る感じて汚いです。
2009年

2
私はtee(py2 / 3互換)の純粋なpython実装を投稿しました。これは、任意のプラットフォームで実行でき、さまざまなロギング構成でも使用できます。stackoverflow.com/questions/616645/...
ソリン

8
Pythonが私のマシンの1つで実行され、ソリューションが実行されない場合、それはPythonicソリューションではありません。そのため反対投票。
anatly techtonik

2
この投稿によると、このsys.stdout = os.fdopen(sys.stdout.fileno(), 'w', 0)はpython 3.3(PEP 3116を参照)以降機能しません
Ken Myers

1
"sys:1:ResourceWarning:unclosed file <_io.BufferedWriter name = 5>"というエラーが発生したためtee.stdin.close()、プログラムの最後に追加する必要がありました。また、「ResourceWarning:subprocess 1842 is running」と表示さsys.stdout.close(); sys.stderr.close()れ、プログラムの最後に追加すると修正されます。
matthieu

136

以前に同じ問題があり、このスニペットが非常に便利であることがわかりました:

class Tee(object):
    def __init__(self, name, mode):
        self.file = open(name, mode)
        self.stdout = sys.stdout
        sys.stdout = self
    def __del__(self):
        sys.stdout = self.stdout
        self.file.close()
    def write(self, data):
        self.file.write(data)
        self.stdout.write(data)
    def flush(self):
        self.file.flush()

から:http : //mail.python.org/pipermail/python-list/2007-May/438106.html


7
sys.stdoutの再割り当てを内部的に処理するための+1。Teeオブジェクトを削除してロギングを終了できるようにする
Ben Blank

12
それにフラッシュを追加します。例: 'self.file.flush()'
Luke Stanley

4
ロギングモジュールについて同意しません。いじくりするのに最適です。ロギングはそのためには大きすぎます。
Kobor42 2012

4
回答のリンクされたディスカッションへのこのフォローアップの改訂バージョンに必ず注意してください。
martineau 2013年

4
それは動作しません。__del__実行が終了するまで呼び出されません。stackoverflow.com/questions/6104535/…を
Nux

77

このprintステートメントは、write()sys.stdoutに割り当てるオブジェクトのメソッドを呼び出します。

小さなクラスをスピンアップして、一度に2つの場所に書き込みます...

import sys

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

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

sys.stdout = Logger()

これで、printステートメントが画面にエコーされ、ログファイルに追加されます。

# prints "1 2" to <stdout> AND log.dat
print "%d %d" % (1,2)

これは明らかに簡単で汚いものです。いくつかのメモ:

  • おそらく、ログファイル名をパラメータ化する必要があります。
  • <stdout>プログラムの実行中にログを記録しない場合は、おそらくsys.stdoutを元に戻す必要があります。
  • 一度に複数のログファイルに書き込んだり、さまざまなログレベルを処理したりする機能が必要な場合があります。

これらはすべて単純明快であり、読者のための練習問題として残しておくのが快適です。ここでの重要な洞察は、にprint割り当てられている「ファイルのようなオブジェクト」を呼び出すだけsys.stdoutです。


まさに私が投稿しようとしたものです。自己引数を持たない書き込みの問題を修正すると、+ 1。。また、あなたが地獄に渡され、また、標準出力に渡されたのは、より良いデザインであるかもしれないへの書き込みを行っているというファイルを持っている優れたデザインになるだろう。
デヴィンJeanpierre

@Devin、そうですね、これは迅速で汚いものでした。初期の改善の可能性についていくつかメモしておきます。
トリプティク

7
私はこの回答を早すぎて選択しました。これは「印刷」には最適ですが、外部コマンド出力にはあまり役立ちません。
2009年

2
Loggerクラスでは、「def flush():self.terminal.flush(); self.log.flush()」などのflush()メソッドも定義する必要があります
blokeley

5
あなたは言うThe print statement will call the write() method of any object you assign to sys.stdout。また、を使用せずにstdoutにデータを送信する他の関数についてはどうでしょうかprint。たとえば、subprocess.callその出力を使用してプロセスを作成すると、コンソールには移動しlog.datますがファイルには移動しません...それを修正する方法はありますか?
jpo38 2015

64

あなたが本当に欲しいのはlogging標準ライブラリのモジュールです。ロガーを作成し、2つのハンドラーをアタッチします。1つはファイルに書き込み、もう1つはstdoutまたはstderrに書き込みます。

詳細については、複数の宛先へのロギングを参照 してください


9
Loggingモジュールは例外やその他の重要な出力をstdoutに記録しません。これは、ビルドサーバー(たとえば)でログを分析するときに役立ちます。
anatly techtonik 2012

2
loggingモジュールは、次のようなシステムコールからの出力をリダイレクトしませんos.write(1, b'stdout')
jfs

17

他のソリューションよりも一般的な別のソリューションを次に示します。これは、出力を(に書き込まれたsys.stdout)任意の数のファイルのようなオブジェクトに分割することをサポートします。__stdout__それ自体が含まれている必要はありません。

import sys

class multifile(object):
    def __init__(self, files):
        self._files = files
    def __getattr__(self, attr, *args):
        return self._wrap(attr, *args)
    def _wrap(self, attr, *args):
        def g(*a, **kw):
            for f in self._files:
                res = getattr(f, attr, *args)(*a, **kw)
            return res
        return g

# for a tee-like behavior, use like this:
sys.stdout = multifile([ sys.stdout, open('myfile.txt', 'w') ])

# all these forms work:
print 'abc'
print >>sys.stdout, 'line2'
sys.stdout.write('line3\n')

注:これは概念実証です。ここでの実装は完全ではありません。ファイルのようなオブジェクト(例:)のメソッドをラップwriteし、members / properties / setattrなどを除外するだけです。しかし、現在のところ、ほとんどの人にとっておそらく十分です。

私はそれについて好きで、その普遍性以外の、それはある意味で、それが直接の呼び出しに行わないクリーンであることでwriteflushos.dup2、など


3
私はinitにファイルではなく* filesを使用しますが、それ以外の場合は、これを使用します。他のソリューションはどれも、他の問題を解決しようとせずに「ティー」機能を分離するものではありません。出力するすべてのものに接頭辞を付ける場合は、このクラスを接頭辞ライタークラスでラップできます。(1つのストリームだけにプレフィックスを付けたい場合は、ストリームをラップしてこのクラスに渡します。)これには、multifile([])がすべてを無視するファイルを作成するという利点もあります(open( '/ dev /ヌル'))。
ベン

なぜ_wrapここにあるのですか?そこにあるコードをコピーして__getattr__も同じように機能しませんか?
timotree

@Benは実際に、そのメソッドの1つを呼び出すたびにをmultifile([])発生させるファイルを作成しますUnboundLocalError。(res割り当てられずに返されます)
timotree

13

他の場所で説明されているように、おそらく最良の解決策は、ロギングモジュールを直接使用することです。

import logging

logging.basicConfig(level=logging.DEBUG, filename='mylog.log')
logging.info('this should to write to the log file')

ただし、stdoutをリダイレクトしたい場合があります(まれな場合)。プリントを使用するdjangoのrunserverコマンドを拡張しているときに、このような状況が発生しました。djangoソースをハックしたくなかったのですが、ファイルに移動するためにprintステートメントが必要でした。

これは、ロギングモジュールを使用してシェルからstdoutおよびstderrをリダイレクトする方法です。

import logging, sys

class LogFile(object):
    """File-like object to log text using the `logging` module."""

    def __init__(self, name=None):
        self.logger = logging.getLogger(name)

    def write(self, msg, level=logging.INFO):
        self.logger.log(level, msg)

    def flush(self):
        for handler in self.logger.handlers:
            handler.flush()

logging.basicConfig(level=logging.DEBUG, filename='mylog.log')

# Redirect stdout and stderr
sys.stdout = LogFile('stdout')
sys.stderr = LogFile('stderr')

print 'this should to write to the log file'

ロギングモジュールを直接使用できない場合にのみ、このLogFile実装を使用してください。


11

tee()ほとんどの場合に機能するはずのPythonの実装を作成しましたが、Windowsでも機能します。

https://github.com/pycontribs/tendo

また、必要にlogging応じて、Pythonのモジュールと組み合わせて使用​​できます。


うーん-そのリンクは機能しません-他にどこにありますか?
ダニーステープル

1
うわー、特にWindowsコンソールカルチャがどれほど厄介であるかを知っているが、それを機能させることをあきらめなかった場合、パッケージは揺るぎません!
n611x007 2013

8

(ああ、あなたの質問をもう一度読んで、これがまったく当てはまらないことを確認してください。)

Pythonのロギングモジュールを使用するサンプルプログラムを次に示します。このロギングモジュールは、2.3以降のすべてのバージョンに含まれています。このサンプルでは、​​ロギングはコマンドラインオプションで設定できます。

完全モードではファイルにのみログを記録し、通常モードではファイルとコンソールの両方にログを記録します。

import os
import sys
import logging
from optparse import OptionParser

def initialize_logging(options):
    """ Log information based upon users options"""

    logger = logging.getLogger('project')
    formatter = logging.Formatter('%(asctime)s %(levelname)s\t%(message)s')
    level = logging.__dict__.get(options.loglevel.upper(),logging.DEBUG)
    logger.setLevel(level)

    # Output logging information to screen
    if not options.quiet:
        hdlr = logging.StreamHandler(sys.stderr)
        hdlr.setFormatter(formatter)
        logger.addHandler(hdlr)

    # Output logging information to file
    logfile = os.path.join(options.logdir, "project.log")
    if options.clean and os.path.isfile(logfile):
        os.remove(logfile)
    hdlr2 = logging.FileHandler(logfile)
    hdlr2.setFormatter(formatter)
    logger.addHandler(hdlr2)

    return logger

def main(argv=None):
    if argv is None:
        argv = sys.argv[1:]

    # Setup command line options
    parser = OptionParser("usage: %prog [options]")
    parser.add_option("-l", "--logdir", dest="logdir", default=".", help="log DIRECTORY (default ./)")
    parser.add_option("-v", "--loglevel", dest="loglevel", default="debug", help="logging level (debug, info, error)")
    parser.add_option("-q", "--quiet", action="store_true", dest="quiet", help="do not log to console")
    parser.add_option("-c", "--clean", dest="clean", action="store_true", default=False, help="remove old log file")

    # Process command line options
    (options, args) = parser.parse_args(argv)

    # Setup logger format and output locations
    logger = initialize_logging(options)

    # Examples
    logger.error("This is an error message.")
    logger.info("This is an info message.")
    logger.debug("This is a debug message.")

if __name__ == "__main__":
    sys.exit(main())

いい答えだ。ログをコンソールに複製するいくつかの本当に複雑な方法を見ましたが、stderrでStreamHandlerを作成することが私が探していた答えでした:)
meatvest

コードはいい質問ですが、質問には答えません。これはログをファイルとstderrに出力します。元の質問は、stderrをログファイルに複製することを求めていました。
emem

8

John Tの回答を完了するには:https : //stackoverflow.com/a/616686/395687

私は追加し__enter__、このコードを与えるキーワードを__exit__持つコンテキストマネージャーとしてそれを使用するメソッドwith

class Tee(object):
    def __init__(self, name, mode):
        self.file = open(name, mode)
        self.stdout = sys.stdout
        sys.stdout = self

    def __del__(self):
        sys.stdout = self.stdout
        self.file.close()

    def write(self, data):
        self.file.write(data)
        self.stdout.write(data)

    def __enter__(self):
        pass

    def __exit__(self, _type, _value, _traceback):
        pass

その後、次のように使用できます

with Tee('outfile.log', 'w'):
    print('I am written to both stdout and outfile.log')

1
__del__機能をに移動します__exit__
vontrapp 2017年

1
実際、使用すること__del__は悪い考えだと思います。で呼び出される「閉じる」関数に移動する必要があります__exit__
cladmi 2017年

7

私はこの質問に何度も回答したことを知っていますが、これについてはジョンTの回答から主な回答を取り、それを修正して、提案されたフラッシュが含まれ、リンクされた改訂版に従っているようにしました。withステートメントで使用するために、cladmiの回答で述べたように、EnterとExitも追加しました。さらに、ドキュメントでは、ファイルをフラッシュすることについて言及しているos.fsync()ので、それも追加しました。それが本当に必要かどうかはわかりませんが、そこにあります。

import sys, os

class Logger(object):
    "Lumberjack class - duplicates sys.stdout to a log file and it's okay"
    #source: https://stackoverflow.com/q/616645
    def __init__(self, filename="Red.Wood", mode="a", buff=0):
        self.stdout = sys.stdout
        self.file = open(filename, mode, buff)
        sys.stdout = self

    def __del__(self):
        self.close()

    def __enter__(self):
        pass

    def __exit__(self, *args):
        self.close()

    def write(self, message):
        self.stdout.write(message)
        self.file.write(message)

    def flush(self):
        self.stdout.flush()
        self.file.flush()
        os.fsync(self.file.fileno())

    def close(self):
        if self.stdout != None:
            sys.stdout = self.stdout
            self.stdout = None

        if self.file != None:
            self.file.close()
            self.file = None

その後、それを使用できます

with Logger('My_best_girlie_by_my.side'):
    print("we'd sing sing sing")

または

Log=Logger('Sleeps_all.night')
print('works all day')
Log.close()

多くのThnaks @Statusが私の質問を解決しました(stackoverflow.com/questions/39143417/…)。私はあなたの解決策へのリンクを置きます。
Mohammad ElNesr

1
@MohammadElNesr withステートメントで使用すると、コードに問題があることに気づきました。私はそれを修正し、withブロックの最後で正しく閉じるようになりました。
ステータス

1
これはのみに変更モードに必要な、私のために素晴らしい仕事をmode="ab"してにwrite機能self.file.write(message.encode("utf-8"))
ennetws

4

ロギングモジュールを使用した別のソリューション:

import logging
import sys

log = logging.getLogger('stdxxx')

class StreamLogger(object):

    def __init__(self, stream, prefix=''):
        self.stream = stream
        self.prefix = prefix
        self.data = ''

    def write(self, data):
        self.stream.write(data)
        self.stream.flush()

        self.data += data
        tmp = str(self.data)
        if '\x0a' in tmp or '\x0d' in tmp:
            tmp = tmp.rstrip('\x0a\x0d')
            log.info('%s%s' % (self.prefix, tmp))
            self.data = ''


logging.basicConfig(level=logging.INFO,
                    filename='text.log',
                    filemode='a')

sys.stdout = StreamLogger(sys.stdout, '[stdout] ')

print 'test for stdout'

3

上記の答えはどれも、提起された問題に実際には答えていないようです。私はこれが古いスレッドであることを知っていますが、この問題は誰もが作っているよりもはるかに簡単だと思います

class tee_err(object):

 def __init__(self):
    self.errout = sys.stderr

    sys.stderr = self

    self.log = 'logfile.log'
    log = open(self.log,'w')
    log.close()

 def write(self, line):

    log = open(self.log,'a')
    log.write(line)
    log.close()   

    self.errout.write(line)

これで、通常のsys.stderrハンドラーとファイルに対してすべてが繰り返されます。の別のクラスtee_outを作成しますsys.stdout


2
同様のより良い答えがこの1年前の2年間にわたって投稿されました:stackoverflow.com/a/616686。このメソッドは非常に負荷が高くなりtee=tee_err();tee.write('');tee.write('');...ます。opens+を呼び出すたびに、それぞれのファイルが閉じられますwrite。この慣行に反対する議論については、stackoverflow.com / q / 4867468およびstackoverflow.com/q/164053を参照してください。
Rob W

3

@John Tの回答の下にあるコメントの@ user5359531によるリクエストに従って、以下はその回答のリンクされたディスカッションの改訂版への参照された投稿のコピーです。

Issue of redirecting the stdout to both file and screen
Gabriel Genellina gagsl-py2 at yahoo.com.ar
Mon May 28 12:45:51 CEST 2007

    Previous message: Issue of redirecting the stdout to both file and screen
    Next message: Formal interfaces with Python
    Messages sorted by: [ date ] [ thread ] [ subject ] [ author ]

En Mon, 28 May 2007 06:17:39 -0300, 人言落日是天涯,望极天涯不见家
<kelvin.you at gmail.com> escribió:

> I wanna print the log to both the screen and file, so I simulatered a
> 'tee'
>
> class Tee(file):
>
>     def __init__(self, name, mode):
>         file.__init__(self, name, mode)
>         self.stdout = sys.stdout
>         sys.stdout = self
>
>     def __del__(self):
>         sys.stdout = self.stdout
>         self.close()
>
>     def write(self, data):
>         file.write(self, data)
>         self.stdout.write(data)
>
> Tee('logfile', 'w')
> print >>sys.stdout, 'abcdefg'
>
> I found that it only output to the file, nothing to screen. Why?
> It seems the 'write' function was not called when I *print* something.

You create a Tee instance and it is immediately garbage collected. I'd
restore sys.stdout on Tee.close, not __del__ (you forgot to call the
inherited __del__ method, btw).
Mmm, doesn't work. I think there is an optimization somewhere: if it looks
like a real file object, it uses the original file write method, not yours.
The trick would be to use an object that does NOT inherit from file:

import sys
class TeeNoFile(object):
     def __init__(self, name, mode):
         self.file = open(name, mode)
         self.stdout = sys.stdout
         sys.stdout = self
     def close(self):
         if self.stdout is not None:
             sys.stdout = self.stdout
             self.stdout = None
         if self.file is not None:
             self.file.close()
             self.file = None
     def write(self, data):
         self.file.write(data)
         self.stdout.write(data)
     def flush(self):
         self.file.flush()
         self.stdout.flush()
     def __del__(self):
         self.close()

tee=TeeNoFile('logfile', 'w')
print 'abcdefg'
print 'another line'
tee.close()
print 'screen only'
del tee # should do nothing

--
Gabriel Genellina

1

コマンドラインスクリプトを実行するスクリプトを書いています。(場合によっては、rsyncの場合のように、Linuxコマンドに代わる実行可能なものがないだけです。)

私が本当に欲しかったのは、可能な場合はすべてデフォルトのpythonロギングメカニズムを使用することですが、予期しない問題が発生した場合でもエラーをキャプチャすることです。

このコードはトリックを行うようです。それは特にエレガントでも効率的でもないかもしれません(string + = stringを使用しないため、少なくとも特定の潜在的なボトルネックはありません)。他の誰かに役立つアイデアを提供できるように、私はそれを投稿しています。

import logging
import os, sys
import datetime

# Get name of module, use as application name
try:
  ME=os.path.split(__file__)[-1].split('.')[0]
except:
  ME='pyExec_'

LOG_IDENTIFIER="uuu___( o O )___uuu "
LOG_IDR_LENGTH=len(LOG_IDENTIFIER)

class PyExec(object):

  # Use this to capture all possible error / output to log
  class SuperTee(object):
      # Original reference: http://mail.python.org/pipermail/python-list/2007-May/442737.html
      def __init__(self, name, mode):
          self.fl = open(name, mode)
          self.fl.write('\n')
          self.stdout = sys.stdout
          self.stdout.write('\n')
          self.stderr = sys.stderr

          sys.stdout = self
          sys.stderr = self

      def __del__(self):
          self.fl.write('\n')
          self.fl.flush()
          sys.stderr = self.stderr
          sys.stdout = self.stdout
          self.fl.close()

      def write(self, data):
          # If the data to write includes the log identifier prefix, then it is already formatted
          if data[0:LOG_IDR_LENGTH]==LOG_IDENTIFIER:
            self.fl.write("%s\n" % data[LOG_IDR_LENGTH:])
            self.stdout.write(data[LOG_IDR_LENGTH:])

          # Otherwise, we can give it a timestamp
          else:

            timestamp=str(datetime.datetime.now())
            if 'Traceback' == data[0:9]:
              data='%s: %s' % (timestamp, data)
              self.fl.write(data)
            else:
              self.fl.write(data)

            self.stdout.write(data)


  def __init__(self, aName, aCmd, logFileName='', outFileName=''):

    # Using name for 'logger' (context?), which is separate from the module or the function
    baseFormatter=logging.Formatter("%(asctime)s \t %(levelname)s \t %(name)s:%(module)s:%(lineno)d \t %(message)s")
    errorFormatter=logging.Formatter(LOG_IDENTIFIER + "%(asctime)s \t %(levelname)s \t %(name)s:%(module)s:%(lineno)d \t %(message)s")

    if logFileName:
      # open passed filename as append
      fl=logging.FileHandler("%s.log" % aName)
    else:
      # otherwise, use log filename as a one-time use file
      fl=logging.FileHandler("%s.log" % aName, 'w')

    fl.setLevel(logging.DEBUG)
    fl.setFormatter(baseFormatter)

    # This will capture stdout and CRITICAL and beyond errors

    if outFileName:
      teeFile=PyExec.SuperTee("%s_out.log" % aName)
    else:
      teeFile=PyExec.SuperTee("%s_out.log" % aName, 'w')

    fl_out=logging.StreamHandler( teeFile )
    fl_out.setLevel(logging.CRITICAL)
    fl_out.setFormatter(errorFormatter)

    # Set up logging
    self.log=logging.getLogger('pyExec_main')
    log=self.log

    log.addHandler(fl)
    log.addHandler(fl_out)

    print "Test print statement."

    log.setLevel(logging.DEBUG)

    log.info("Starting %s", ME)
    log.critical("Critical.")

    # Caught exception
    try:
      raise Exception('Exception test.')
    except Exception,e:
      log.exception(str(e))

    # Uncaught exception
    a=2/0


PyExec('test_pyExec',None)

明らかに、私ほど気まぐれではない場合は、LOG_IDENTIFIERを、誰かがログに書き込むのを見たくない別の文字列に置き換えます。


0

すべての出力をファイルに記録し、それをテキストファイルに出力する場合は、次の操作を実行できます。少しハッキーですが、動作します:

import logging
debug = input("Debug or not")
if debug == "1":
    logging.basicConfig(level=logging.DEBUG, filename='./OUT.txt')
    old_print = print
    def print(string):
        old_print(string)
        logging.info(string)
print("OMG it works!")

編集:sys.stderrをsys.stdoutにリダイレクトしない限り、これはエラーを記録しないことに注意してください

EDIT2:2番目の問題は、組み込み関数とは異なり、1つの引数を渡す必要があることです。

EDIT3:stdinとstdoutをコンソールとファイルに書き込むには、前のコードを参照してください。

import logging, sys
debug = input("Debug or not")
if debug == "1":
    old_input = input
    sys.stderr.write = logging.info
    def input(string=""):
        string_in = old_input(string)
        logging.info("STRING IN " + string_in)
        return string_in
    logging.basicConfig(level=logging.DEBUG, filename='./OUT.txt')
    old_print = print
    def print(string="", string2=""):
        old_print(string, string2)
        logging.info(string)
        logging.info(string2)
print("OMG")
b = input()
print(a) ## Deliberate error for testing

-1

の完全な置き換えをsys.stderr記述し、コードの名前stderrを変更しstdoutて、置き換え用にも使用できるようにしましたsys.stdout

これを行うには、私は現在と同じオブジェクト型を作成stderrしてstdout、元のシステムにすべてのメソッドを転送するstderrstdout

import os
import sys
import logging

class StdErrReplament(object):
    """
        How to redirect stdout and stderr to logger in Python
        /programming/19425736/how-to-redirect-stdout-and-stderr-to-logger-in-python

        Set a Read-Only Attribute in Python?
        /programming/24497316/set-a-read-only-attribute-in-python
    """
    is_active = False

    @classmethod
    def lock(cls, logger):
        """
            Attach this singleton logger to the `sys.stderr` permanently.
        """
        global _stderr_singleton
        global _stderr_default
        global _stderr_default_class_type

        # On Sublime Text, `sys.__stderr__` is set to None, because they already replaced `sys.stderr`
        # by some `_LogWriter()` class, then just save the current one over there.
        if not sys.__stderr__:
            sys.__stderr__ = sys.stderr

        try:
            _stderr_default
            _stderr_default_class_type

        except NameError:
            _stderr_default = sys.stderr
            _stderr_default_class_type = type( _stderr_default )

        # Recreate the sys.stderr logger when it was reset by `unlock()`
        if not cls.is_active:
            cls.is_active = True
            _stderr_write = _stderr_default.write

            logger_call = logger.debug
            clean_formatter = logger.clean_formatter

            global _sys_stderr_write
            global _sys_stderr_write_hidden

            if sys.version_info <= (3,2):
                logger.file_handler.terminator = '\n'

            # Always recreate/override the internal write function used by `_sys_stderr_write`
            def _sys_stderr_write_hidden(*args, **kwargs):
                """
                    Suppress newline in Python logging module
                    /programming/7168790/suppress-newline-in-python-logging-module
                """

                try:
                    _stderr_write( *args, **kwargs )
                    file_handler = logger.file_handler

                    formatter = file_handler.formatter
                    terminator = file_handler.terminator

                    file_handler.formatter = clean_formatter
                    file_handler.terminator = ""

                    kwargs['extra'] = {'_duplicated_from_file': True}
                    logger_call( *args, **kwargs )

                    file_handler.formatter = formatter
                    file_handler.terminator = terminator

                except Exception:
                    logger.exception( "Could not write to the file_handler: %s(%s)", file_handler, logger )
                    cls.unlock()

            # Only create one `_sys_stderr_write` function pointer ever
            try:
                _sys_stderr_write

            except NameError:

                def _sys_stderr_write(*args, **kwargs):
                    """
                        Hides the actual function pointer. This allow the external function pointer to
                        be cached while the internal written can be exchanged between the standard
                        `sys.stderr.write` and our custom wrapper around it.
                    """
                    _sys_stderr_write_hidden( *args, **kwargs )

        try:
            # Only create one singleton instance ever
            _stderr_singleton

        except NameError:

            class StdErrReplamentHidden(_stderr_default_class_type):
                """
                    Which special methods bypasses __getattribute__ in Python?
                    /programming/12872695/which-special-methods-bypasses-getattribute-in-python
                """

                if hasattr( _stderr_default, "__abstractmethods__" ):
                    __abstractmethods__ = _stderr_default.__abstractmethods__

                if hasattr( _stderr_default, "__base__" ):
                    __base__ = _stderr_default.__base__

                if hasattr( _stderr_default, "__bases__" ):
                    __bases__ = _stderr_default.__bases__

                if hasattr( _stderr_default, "__basicsize__" ):
                    __basicsize__ = _stderr_default.__basicsize__

                if hasattr( _stderr_default, "__call__" ):
                    __call__ = _stderr_default.__call__

                if hasattr( _stderr_default, "__class__" ):
                    __class__ = _stderr_default.__class__

                if hasattr( _stderr_default, "__delattr__" ):
                    __delattr__ = _stderr_default.__delattr__

                if hasattr( _stderr_default, "__dict__" ):
                    __dict__ = _stderr_default.__dict__

                if hasattr( _stderr_default, "__dictoffset__" ):
                    __dictoffset__ = _stderr_default.__dictoffset__

                if hasattr( _stderr_default, "__dir__" ):
                    __dir__ = _stderr_default.__dir__

                if hasattr( _stderr_default, "__doc__" ):
                    __doc__ = _stderr_default.__doc__

                if hasattr( _stderr_default, "__eq__" ):
                    __eq__ = _stderr_default.__eq__

                if hasattr( _stderr_default, "__flags__" ):
                    __flags__ = _stderr_default.__flags__

                if hasattr( _stderr_default, "__format__" ):
                    __format__ = _stderr_default.__format__

                if hasattr( _stderr_default, "__ge__" ):
                    __ge__ = _stderr_default.__ge__

                if hasattr( _stderr_default, "__getattribute__" ):
                    __getattribute__ = _stderr_default.__getattribute__

                if hasattr( _stderr_default, "__gt__" ):
                    __gt__ = _stderr_default.__gt__

                if hasattr( _stderr_default, "__hash__" ):
                    __hash__ = _stderr_default.__hash__

                if hasattr( _stderr_default, "__init__" ):
                    __init__ = _stderr_default.__init__

                if hasattr( _stderr_default, "__init_subclass__" ):
                    __init_subclass__ = _stderr_default.__init_subclass__

                if hasattr( _stderr_default, "__instancecheck__" ):
                    __instancecheck__ = _stderr_default.__instancecheck__

                if hasattr( _stderr_default, "__itemsize__" ):
                    __itemsize__ = _stderr_default.__itemsize__

                if hasattr( _stderr_default, "__le__" ):
                    __le__ = _stderr_default.__le__

                if hasattr( _stderr_default, "__lt__" ):
                    __lt__ = _stderr_default.__lt__

                if hasattr( _stderr_default, "__module__" ):
                    __module__ = _stderr_default.__module__

                if hasattr( _stderr_default, "__mro__" ):
                    __mro__ = _stderr_default.__mro__

                if hasattr( _stderr_default, "__name__" ):
                    __name__ = _stderr_default.__name__

                if hasattr( _stderr_default, "__ne__" ):
                    __ne__ = _stderr_default.__ne__

                if hasattr( _stderr_default, "__new__" ):
                    __new__ = _stderr_default.__new__

                if hasattr( _stderr_default, "__prepare__" ):
                    __prepare__ = _stderr_default.__prepare__

                if hasattr( _stderr_default, "__qualname__" ):
                    __qualname__ = _stderr_default.__qualname__

                if hasattr( _stderr_default, "__reduce__" ):
                    __reduce__ = _stderr_default.__reduce__

                if hasattr( _stderr_default, "__reduce_ex__" ):
                    __reduce_ex__ = _stderr_default.__reduce_ex__

                if hasattr( _stderr_default, "__repr__" ):
                    __repr__ = _stderr_default.__repr__

                if hasattr( _stderr_default, "__setattr__" ):
                    __setattr__ = _stderr_default.__setattr__

                if hasattr( _stderr_default, "__sizeof__" ):
                    __sizeof__ = _stderr_default.__sizeof__

                if hasattr( _stderr_default, "__str__" ):
                    __str__ = _stderr_default.__str__

                if hasattr( _stderr_default, "__subclasscheck__" ):
                    __subclasscheck__ = _stderr_default.__subclasscheck__

                if hasattr( _stderr_default, "__subclasses__" ):
                    __subclasses__ = _stderr_default.__subclasses__

                if hasattr( _stderr_default, "__subclasshook__" ):
                    __subclasshook__ = _stderr_default.__subclasshook__

                if hasattr( _stderr_default, "__text_signature__" ):
                    __text_signature__ = _stderr_default.__text_signature__

                if hasattr( _stderr_default, "__weakrefoffset__" ):
                    __weakrefoffset__ = _stderr_default.__weakrefoffset__

                if hasattr( _stderr_default, "mro" ):
                    mro = _stderr_default.mro

                def __init__(self):
                    """
                        Override any super class `type( _stderr_default )` constructor, so we can 
                        instantiate any kind of `sys.stderr` replacement object, in case it was already 
                        replaced by something else like on Sublime Text with `_LogWriter()`.

                        Assures all attributes were statically replaced just above. This should happen in case
                        some new attribute is added to the python language.

                        This also ignores the only two methods which are not equal, `__init__()` and `__getattribute__()`.
                    """
                    different_methods = ("__init__", "__getattribute__")
                    attributes_to_check = set( dir( object ) + dir( type ) )

                    for attribute in attributes_to_check:

                        if attribute not in different_methods \
                                and hasattr( _stderr_default, attribute ):

                            base_class_attribute = super( _stderr_default_class_type, self ).__getattribute__( attribute )
                            target_class_attribute = _stderr_default.__getattribute__( attribute )

                            if base_class_attribute != target_class_attribute:
                                sys.stderr.write( "    The base class attribute `%s` is different from the target class:\n%s\n%s\n\n" % (
                                        attribute, base_class_attribute, target_class_attribute ) )

                def __getattribute__(self, item):

                    if item == 'write':
                        return _sys_stderr_write

                    try:
                        return _stderr_default.__getattribute__( item )

                    except AttributeError:
                        return super( _stderr_default_class_type, _stderr_default ).__getattribute__( item )

            _stderr_singleton = StdErrReplamentHidden()
            sys.stderr = _stderr_singleton

        return cls

    @classmethod
    def unlock(cls):
        """
            Detach this `stderr` writer from `sys.stderr` and allow the next call to `lock()` create
            a new writer for the stderr.
        """

        if cls.is_active:
            global _sys_stderr_write_hidden

            cls.is_active = False
            _sys_stderr_write_hidden = _stderr_default.write



class StdOutReplament(object):
    """
        How to redirect stdout and stderr to logger in Python
        /programming/19425736/how-to-redirect-stdout-and-stderr-to-logger-in-python

        Set a Read-Only Attribute in Python?
        /programming/24497316/set-a-read-only-attribute-in-python
    """
    is_active = False

    @classmethod
    def lock(cls, logger):
        """
            Attach this singleton logger to the `sys.stdout` permanently.
        """
        global _stdout_singleton
        global _stdout_default
        global _stdout_default_class_type

        # On Sublime Text, `sys.__stdout__` is set to None, because they already replaced `sys.stdout`
        # by some `_LogWriter()` class, then just save the current one over there.
        if not sys.__stdout__:
            sys.__stdout__ = sys.stdout

        try:
            _stdout_default
            _stdout_default_class_type

        except NameError:
            _stdout_default = sys.stdout
            _stdout_default_class_type = type( _stdout_default )

        # Recreate the sys.stdout logger when it was reset by `unlock()`
        if not cls.is_active:
            cls.is_active = True
            _stdout_write = _stdout_default.write

            logger_call = logger.debug
            clean_formatter = logger.clean_formatter

            global _sys_stdout_write
            global _sys_stdout_write_hidden

            if sys.version_info <= (3,2):
                logger.file_handler.terminator = '\n'

            # Always recreate/override the internal write function used by `_sys_stdout_write`
            def _sys_stdout_write_hidden(*args, **kwargs):
                """
                    Suppress newline in Python logging module
                    /programming/7168790/suppress-newline-in-python-logging-module
                """

                try:
                    _stdout_write( *args, **kwargs )
                    file_handler = logger.file_handler

                    formatter = file_handler.formatter
                    terminator = file_handler.terminator

                    file_handler.formatter = clean_formatter
                    file_handler.terminator = ""

                    kwargs['extra'] = {'_duplicated_from_file': True}
                    logger_call( *args, **kwargs )

                    file_handler.formatter = formatter
                    file_handler.terminator = terminator

                except Exception:
                    logger.exception( "Could not write to the file_handler: %s(%s)", file_handler, logger )
                    cls.unlock()

            # Only create one `_sys_stdout_write` function pointer ever
            try:
                _sys_stdout_write

            except NameError:

                def _sys_stdout_write(*args, **kwargs):
                    """
                        Hides the actual function pointer. This allow the external function pointer to
                        be cached while the internal written can be exchanged between the standard
                        `sys.stdout.write` and our custom wrapper around it.
                    """
                    _sys_stdout_write_hidden( *args, **kwargs )

        try:
            # Only create one singleton instance ever
            _stdout_singleton

        except NameError:

            class StdOutReplamentHidden(_stdout_default_class_type):
                """
                    Which special methods bypasses __getattribute__ in Python?
                    /programming/12872695/which-special-methods-bypasses-getattribute-in-python
                """

                if hasattr( _stdout_default, "__abstractmethods__" ):
                    __abstractmethods__ = _stdout_default.__abstractmethods__

                if hasattr( _stdout_default, "__base__" ):
                    __base__ = _stdout_default.__base__

                if hasattr( _stdout_default, "__bases__" ):
                    __bases__ = _stdout_default.__bases__

                if hasattr( _stdout_default, "__basicsize__" ):
                    __basicsize__ = _stdout_default.__basicsize__

                if hasattr( _stdout_default, "__call__" ):
                    __call__ = _stdout_default.__call__

                if hasattr( _stdout_default, "__class__" ):
                    __class__ = _stdout_default.__class__

                if hasattr( _stdout_default, "__delattr__" ):
                    __delattr__ = _stdout_default.__delattr__

                if hasattr( _stdout_default, "__dict__" ):
                    __dict__ = _stdout_default.__dict__

                if hasattr( _stdout_default, "__dictoffset__" ):
                    __dictoffset__ = _stdout_default.__dictoffset__

                if hasattr( _stdout_default, "__dir__" ):
                    __dir__ = _stdout_default.__dir__

                if hasattr( _stdout_default, "__doc__" ):
                    __doc__ = _stdout_default.__doc__

                if hasattr( _stdout_default, "__eq__" ):
                    __eq__ = _stdout_default.__eq__

                if hasattr( _stdout_default, "__flags__" ):
                    __flags__ = _stdout_default.__flags__

                if hasattr( _stdout_default, "__format__" ):
                    __format__ = _stdout_default.__format__

                if hasattr( _stdout_default, "__ge__" ):
                    __ge__ = _stdout_default.__ge__

                if hasattr( _stdout_default, "__getattribute__" ):
                    __getattribute__ = _stdout_default.__getattribute__

                if hasattr( _stdout_default, "__gt__" ):
                    __gt__ = _stdout_default.__gt__

                if hasattr( _stdout_default, "__hash__" ):
                    __hash__ = _stdout_default.__hash__

                if hasattr( _stdout_default, "__init__" ):
                    __init__ = _stdout_default.__init__

                if hasattr( _stdout_default, "__init_subclass__" ):
                    __init_subclass__ = _stdout_default.__init_subclass__

                if hasattr( _stdout_default, "__instancecheck__" ):
                    __instancecheck__ = _stdout_default.__instancecheck__

                if hasattr( _stdout_default, "__itemsize__" ):
                    __itemsize__ = _stdout_default.__itemsize__

                if hasattr( _stdout_default, "__le__" ):
                    __le__ = _stdout_default.__le__

                if hasattr( _stdout_default, "__lt__" ):
                    __lt__ = _stdout_default.__lt__

                if hasattr( _stdout_default, "__module__" ):
                    __module__ = _stdout_default.__module__

                if hasattr( _stdout_default, "__mro__" ):
                    __mro__ = _stdout_default.__mro__

                if hasattr( _stdout_default, "__name__" ):
                    __name__ = _stdout_default.__name__

                if hasattr( _stdout_default, "__ne__" ):
                    __ne__ = _stdout_default.__ne__

                if hasattr( _stdout_default, "__new__" ):
                    __new__ = _stdout_default.__new__

                if hasattr( _stdout_default, "__prepare__" ):
                    __prepare__ = _stdout_default.__prepare__

                if hasattr( _stdout_default, "__qualname__" ):
                    __qualname__ = _stdout_default.__qualname__

                if hasattr( _stdout_default, "__reduce__" ):
                    __reduce__ = _stdout_default.__reduce__

                if hasattr( _stdout_default, "__reduce_ex__" ):
                    __reduce_ex__ = _stdout_default.__reduce_ex__

                if hasattr( _stdout_default, "__repr__" ):
                    __repr__ = _stdout_default.__repr__

                if hasattr( _stdout_default, "__setattr__" ):
                    __setattr__ = _stdout_default.__setattr__

                if hasattr( _stdout_default, "__sizeof__" ):
                    __sizeof__ = _stdout_default.__sizeof__

                if hasattr( _stdout_default, "__str__" ):
                    __str__ = _stdout_default.__str__

                if hasattr( _stdout_default, "__subclasscheck__" ):
                    __subclasscheck__ = _stdout_default.__subclasscheck__

                if hasattr( _stdout_default, "__subclasses__" ):
                    __subclasses__ = _stdout_default.__subclasses__

                if hasattr( _stdout_default, "__subclasshook__" ):
                    __subclasshook__ = _stdout_default.__subclasshook__

                if hasattr( _stdout_default, "__text_signature__" ):
                    __text_signature__ = _stdout_default.__text_signature__

                if hasattr( _stdout_default, "__weakrefoffset__" ):
                    __weakrefoffset__ = _stdout_default.__weakrefoffset__

                if hasattr( _stdout_default, "mro" ):
                    mro = _stdout_default.mro

                def __init__(self):
                    """
                        Override any super class `type( _stdout_default )` constructor, so we can 
                        instantiate any kind of `sys.stdout` replacement object, in case it was already 
                        replaced by something else like on Sublime Text with `_LogWriter()`.

                        Assures all attributes were statically replaced just above. This should happen in case
                        some new attribute is added to the python language.

                        This also ignores the only two methods which are not equal, `__init__()` and `__getattribute__()`.
                    """
                    different_methods = ("__init__", "__getattribute__")
                    attributes_to_check = set( dir( object ) + dir( type ) )

                    for attribute in attributes_to_check:

                        if attribute not in different_methods \
                                and hasattr( _stdout_default, attribute ):

                            base_class_attribute = super( _stdout_default_class_type, self ).__getattribute__( attribute )
                            target_class_attribute = _stdout_default.__getattribute__( attribute )

                            if base_class_attribute != target_class_attribute:
                                sys.stdout.write( "    The base class attribute `%s` is different from the target class:\n%s\n%s\n\n" % (
                                        attribute, base_class_attribute, target_class_attribute ) )

                def __getattribute__(self, item):

                    if item == 'write':
                        return _sys_stdout_write

                    try:
                        return _stdout_default.__getattribute__( item )

                    except AttributeError:
                        return super( _stdout_default_class_type, _stdout_default ).__getattribute__( item )

            _stdout_singleton = StdOutReplamentHidden()
            sys.stdout = _stdout_singleton

        return cls

    @classmethod
    def unlock(cls):
        """
            Detach this `stdout` writer from `sys.stdout` and allow the next call to `lock()` create
            a new writer for the stdout.
        """

        if cls.is_active:
            global _sys_stdout_write_hidden

            cls.is_active = False
            _sys_stdout_write_hidden = _stdout_default.write

これ を使用するには、出力テキストの送信に使用するロガーを呼び出しStdErrReplament::lock(logger)StdOutReplament::lock(logger)渡すだけです。例えば:

import os
import sys
import logging

current_folder = os.path.dirname( os.path.realpath( __file__ ) )
log_file_path = os.path.join( current_folder, "my_log_file.txt" )

file_handler = logging.FileHandler( log_file_path, 'a' )
file_handler.formatter = logging.Formatter( "%(asctime)s %(name)s %(levelname)s - %(message)s", "%Y-%m-%d %H:%M:%S" )

log = logging.getLogger( __name__ )
log.setLevel( "DEBUG" )
log.addHandler( file_handler )

log.file_handler = file_handler
log.clean_formatter = logging.Formatter( "", "" )

StdOutReplament.lock( log )
StdErrReplament.lock( log )

log.debug( "I am doing usual logging debug..." )
sys.stderr.write( "Tests 1...\n" )
sys.stdout.write( "Tests 2...\n" )

このコードを実行すると、画面に次のように表示されます。

ここに画像の説明を入力してください

そしてファイルの内容について:

ここに画像の説明を入力してください

log.debug画面に呼び出しの内容も表示したい場合は、ロガーにストリームハンドラーを追加する必要があります。この場合、次のようになります。

import os
import sys
import logging

class ContextFilter(logging.Filter):
    """ This filter avoids duplicated information to be displayed to the StreamHandler log. """
    def filter(self, record):
        return not "_duplicated_from_file" in record.__dict__

current_folder = os.path.dirname( os.path.realpath( __file__ ) )
log_file_path = os.path.join( current_folder, "my_log_file.txt" )

stream_handler = logging.StreamHandler()
file_handler = logging.FileHandler( log_file_path, 'a' )

formatter = logging.Formatter( "%(asctime)s %(name)s %(levelname)s - %(message)s", "%Y-%m-%d %H:%M:%S" )
file_handler.formatter = formatter
stream_handler.formatter = formatter
stream_handler.addFilter( ContextFilter() )

log = logging.getLogger( __name__ )
log.setLevel( "DEBUG" )
log.addHandler( file_handler )
log.addHandler( stream_handler )

log.file_handler = file_handler
log.stream_handler = stream_handler
log.clean_formatter = logging.Formatter( "", "" )

StdOutReplament.lock( log )
StdErrReplament.lock( log )

log.debug( "I am doing usual logging debug..." )
sys.stderr.write( "Tests 1...\n" )
sys.stdout.write( "Tests 2...\n" )

実行すると次のように出力されます:

ここに画像の説明を入力してください

それはまだこれをファイルに保存しますがmy_log_file.txt

ここに画像の説明を入力してください

でこれを無効にするとStdErrReplament:unlock()stderrストリームの標準動作のみが復元されます。他の誰かが古いバージョンへの参照を持っている可能性があるため、接続されたロガーを切り離すことはできないからです。これが、決して死なないグローバルシングルトンである理由です。したがって、このモジュールをimp何か他のものでリロードする場合、sys.stderr既に注入されていて内部に保存されているため、電流を再キャプチャすることはありません。


5
ストリームを複製するための驚くべきレベルの偶発的な複雑さ。
Attila Lendvai
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.