如何从 Python 日志记录模块获得非阻塞/实时行为? (输出到 PyQt QTextBrowser)

Posted

技术标签:

【中文标题】如何从 Python 日志记录模块获得非阻塞/实时行为? (输出到 PyQt QTextBrowser)【英文标题】:How to get non-blocking/real-time behavior from Python logging module? (output to PyQt QTextBrowser) 【发布时间】:2012-12-30 05:59:39 【问题描述】:

描述:我编写了一个自定义日志处理程序,用于捕获日志事件并将它们写入 QTextBrowser 对象(工作示例代码如下所示)。

问题:按下按钮会调用someProcess()。这会将两个字符串写入logger 对象。但是,字符串仅在someProcess() 返回后出现。

问题:如何让记录的字符串立即/实时显示在 QTextBrowser 对象中? (即,只要调用 logger 输出方法)

from PyQt4 import QtCore, QtGui
import sys
import time
import logging
logger = logging.getLogger(__name__)

class ConsoleWindowLogHandler(logging.Handler):
    def __init__(self, textBox):
        super(ConsoleWindowLogHandler, self).__init__()
        self.textBox = textBox

    def emit(self, logRecord):
        self.textBox.append(str(logRecord.getMessage()))

def someProcess():
    logger.error("line1")
    time.sleep(5)
    logger.error("line2")

if __name__ == "__main__":
    app = QtGui.QApplication(sys.argv)
    window = QtGui.QWidget()
    textBox = QtGui.QTextBrowser()
    button = QtGui.QPushButton()
    button.clicked.connect(someProcess)
    vertLayout = QtGui.QVBoxLayout()
    vertLayout.addWidget(textBox)
    vertLayout.addWidget(button)
    window.setLayout(vertLayout)
    window.show()
    consoleHandler = ConsoleWindowLogHandler(textBox)
    logger.addHandler(consoleHandler)
    sys.exit(app.exec_())

编辑:感谢@abarnert 的回答,我设法使用QThread 编写了这段工作代码。我将QThread 子类化,以便在后台线程中运行一些函数someProcess。对于信号,我不得不求助于旧式 Signal and Slots(我不确定如何在新式中做到这一点)。我创建了一个虚拟 QObject,以便能够从日志处理程序发出信号。

from PyQt4 import QtCore, QtGui
import sys
import time
import logging
logger = logging.getLogger(__name__)

#------------------------------------------------------------------------------
class ConsoleWindowLogHandler(logging.Handler):
    def __init__(self, sigEmitter):
        super(ConsoleWindowLogHandler, self).__init__()
        self.sigEmitter = sigEmitter

    def emit(self, logRecord):
        message = str(logRecord.getMessage())
        self.sigEmitter.emit(QtCore.SIGNAL("logMsg(QString)"), message)

#------------------------------------------------------------------------------
class Window(QtGui.QWidget):
    def __init__(self):
        super(Window, self).__init__()

        # Layout
        textBox = QtGui.QTextBrowser()
        self.button = QtGui.QPushButton()
        vertLayout = QtGui.QVBoxLayout()
        vertLayout.addWidget(textBox)
        vertLayout.addWidget(self.button)
        self.setLayout(vertLayout)

        # Connect button
        self.button.clicked.connect(self.buttonPressed)

        # Thread
        self.bee = Worker(self.someProcess, ())
        self.bee.finished.connect(self.restoreUi)
        self.bee.terminated.connect(self.restoreUi)

        # Console handler
        dummyEmitter = QtCore.QObject()
        self.connect(dummyEmitter, QtCore.SIGNAL("logMsg(QString)"),
                     textBox.append)
        consoleHandler = ConsoleWindowLogHandler(dummyEmitter)
        logger.addHandler(consoleHandler)

    def buttonPressed(self):
        self.button.setEnabled(False)
        self.bee.start()

    def someProcess(self):
        logger.error("starting")
        for i in xrange(10):
            logger.error("line%d" % i)
            time.sleep(2)

    def restoreUi(self):
        self.button.setEnabled(True)

#------------------------------------------------------------------------------
class Worker(QtCore.QThread):
    def __init__(self, func, args):
        super(Worker, self).__init__()
        self.func = func
        self.args = args

    def run(self):
        self.func(*self.args)

#------------------------------------------------------------------------------
if __name__ == "__main__":
    app = QtGui.QApplication(sys.argv)
    window = Window()
    window.show()
    sys.exit(app.exec_())

【问题讨论】:

如果您的编辑是一个正确的答案,这将是解决在 PyQt PySide 小部件中显示日志消息的常见问题的规范解决方案。与所有类似问题的所有类似解决方案不同(例如,this、this),您的编辑利用插槽和信号,因此以非阻塞方式运行。 太棒了。 使用旧式插槽和信号以及超文本 QTextBrowser 而不是只读纯文本 QTextArea 不那么美妙。使用新型插槽和信号应该消除对dummyEmitter 中介的需要。同样,引用官方QTextBrowserdocumentation:“如果您想要一个没有超文本导航的文本浏览器,请使用QTextEdit 并使用QTextEdit::setReadOnly() 禁用编辑。” 【参考方案1】:

翻译 JoeXinfa 对 PyQt5 的回答:

import sys
import time
import logging
from PyQt5.QtCore import QObject, pyqtSignal, QThread
from PyQt5.QtWidgets import QWidget, QTextEdit, QPushButton, QVBoxLayout, QApplication

logger = logging.getLogger(__name__)


class ConsoleWindowLogHandler(logging.Handler, QObject):
    sigLog = pyqtSignal(str)
    def __init__(self):
        logging.Handler.__init__(self)
        QObject.__init__(self)

    def emit(self, logRecord):
        message = str(logRecord.getMessage())
        self.sigLog.emit(message)


class Window(QWidget):
    def __init__(self):
        super(Window, self).__init__()

        # Layout
        textBox = QTextEdit()
        textBox.setReadOnly(True)
        self.button = QPushButton('Click')
        vertLayout = QVBoxLayout()
        vertLayout.addWidget(textBox)
        vertLayout.addWidget(self.button)
        self.setLayout(vertLayout)

        # Connect button
        #self.button.clicked.connect(self.someProcess) # blocking
        self.button.clicked.connect(self.buttonPressed)

        # Thread
        self.bee = Worker(self.someProcess, ())
        self.bee.finished.connect(self.restoreUi)
        self.bee.terminate()

        # Console handler
        consoleHandler = ConsoleWindowLogHandler()
        consoleHandler.sigLog.connect(textBox.append)
        logger.addHandler(consoleHandler)

    def buttonPressed(self):
        self.button.setEnabled(False)
        self.bee.start()

    def someProcess(self):
        logger.error("starting")
        for i in range(10):
            logger.error("line%d" % i)
            time.sleep(2)

    def restoreUi(self):
        self.button.setEnabled(True)


class Worker(QThread):
    def __init__(self, func, args):
        super(Worker, self).__init__()
        self.func = func
        self.args = args

    def run(self):
        self.func(*self.args)


def main():
    app = QApplication(sys.argv)
    window = Window()
    window.show()
    sys.exit(app.exec_())


if __name__ == "__main__":
    main()

【讨论】:

【参考方案2】:

在@Gilead 的代码和@Cecil 的建议之上构建,我通过将旧样式更改为新样式信号/插槽并将QTextBrowser 更改为QTextEdit 来更新代码。

import sys
import time
import logging
from qtpy.QtCore import QObject, Signal, QThread
from qtpy.QtWidgets import QWidget, QTextEdit, QPushButton, QVBoxLayout

logger = logging.getLogger(__name__)


class ConsoleWindowLogHandler(logging.Handler, QObject):
    sigLog = Signal(str)
    def __init__(self):
        logging.Handler.__init__(self)
        QObject.__init__(self)

    def emit(self, logRecord):
        message = str(logRecord.getMessage())
        self.sigLog.emit(message)


class Window(QWidget):
    def __init__(self):
        super(Window, self).__init__()

        # Layout
        textBox = QTextEdit()
        textBox.setReadOnly(True)
        self.button = QPushButton('Click')
        vertLayout = QVBoxLayout()
        vertLayout.addWidget(textBox)
        vertLayout.addWidget(self.button)
        self.setLayout(vertLayout)

        # Connect button
        #self.button.clicked.connect(self.someProcess) # blocking
        self.button.clicked.connect(self.buttonPressed)

        # Thread
        self.bee = Worker(self.someProcess, ())
        self.bee.finished.connect(self.restoreUi)
        self.bee.terminated.connect(self.restoreUi)

        # Console handler
        consoleHandler = ConsoleWindowLogHandler()
        consoleHandler.sigLog.connect(textBox.append)
        logger.addHandler(consoleHandler)

    def buttonPressed(self):
        self.button.setEnabled(False)
        self.bee.start()

    def someProcess(self):
        logger.error("starting")
        for i in range(10):
            logger.error("line%d" % i)
            time.sleep(2)

    def restoreUi(self):
        self.button.setEnabled(True)


class Worker(QThread):
    def __init__(self, func, args):
        super(Worker, self).__init__()
        self.func = func
        self.args = args

    def run(self):
        self.func(*self.args)


def main():
    from qtpy.QtWidgets import QApplication
    app = QApplication(sys.argv)
    window = Window()
    window.show()
    sys.exit(app.exec_())


if __name__ == "__main__":
    main()

【讨论】:

【参考方案3】:

这是另一种方法。在这个例子中,我通过继承QObjectStringIOStreamHandler 添加到写入缓冲区的记录器中:当处理程序遇到非空字符串时,发出bufferMessage 信号并在on_bufferMessage 插槽。

#!/usr/bin/env python
#-*- coding:utf-8 -*-

import logging, StringIO, time

from PyQt4 import QtCore, QtGui

class logBuffer(QtCore.QObject, StringIO.StringIO):
    bufferMessage = QtCore.pyqtSignal(str)

    def __init__(self, *args, **kwargs):
        QtCore.QObject.__init__(self)
        StringIO.StringIO.__init__(self, *args, **kwargs)

    def write(self, message):
        if message:
            self.bufferMessage.emit(unicode(message))

        StringIO.StringIO.write(self, message)

class myThread(QtCore.QThread):
    def __init__(self, parent=None):
        super(myThread, self).__init__(parent)
        self.iteration = None

    def start(self):
        self.iteration = 3

        return super(myThread, self).start()

    def run(self):        
        while self.iteration:
            logging.info("Hello from thread 0! 1".format(0, self.iteration))
            self.iteration -= 1

            time.sleep(3)

class myThread1(QtCore.QThread):
    def __init__(self, parent=None):
        super(myThread1, self).__init__(parent)
        self.iteration = None
        self.logger = logging.getLogger(__name__)

    def start(self):
        self.iteration = 3

        return super(myThread1, self).start()

    def run(self):        
        time.sleep(1)
        while self.iteration:
            self.logger.info("Hello from thread 0! 1".format(1, self.iteration))
            self.iteration -= 1

            time.sleep(3)


class myWindow(QtGui.QWidget):
    def __init__(self, parent=None):
        super(myWindow, self).__init__(parent)

        self.pushButton = QtGui.QPushButton(self)
        self.pushButton.setText("Send Log Message")
        self.pushButton.clicked.connect(self.on_pushButton_clicked)

        self.pushButtonThread = QtGui.QPushButton(self)
        self.pushButtonThread.setText("Start Threading")
        self.pushButtonThread.clicked.connect(self.on_pushButtonThread_clicked)

        self.lineEdit = QtGui.QLineEdit(self)
        self.lineEdit.setText("Hello!")

        self.label = QtGui.QLabel(self)

        self.layout = QtGui.QVBoxLayout(self)
        self.layout.addWidget(self.lineEdit)
        self.layout.addWidget(self.pushButton)
        self.layout.addWidget(self.pushButtonThread)
        self.layout.addWidget(self.label)

        self.logBuffer = logBuffer()
        self.logBuffer.bufferMessage.connect(self.on_logBuffer_bufferMessage)

        logFormatter = logging.Formatter('%(levelname)s: %(message)s')

        logHandler = logging.StreamHandler(self.logBuffer)
        logHandler.setFormatter(logFormatter)

        self.logger = logging.getLogger()
        self.logger.setLevel(logging.INFO)
        self.logger.addHandler(logHandler)

        self.thread = myThread(self)
        self.thread1 = myThread1(self)

    @QtCore.pyqtSlot()
    def on_pushButtonThread_clicked(self):
        self.thread.start()
        self.thread1.start()

    @QtCore.pyqtSlot(str)
    def on_logBuffer_bufferMessage(self, message):
        self.label.setText(message)

    @QtCore.pyqtSlot()
    def on_pushButton_clicked(self):
        message = self.lineEdit.text()
        self.logger.info(message if message else "No new messages")

if __name__ == "__main__":
    import sys

    app = QtGui.QApplication(sys.argv)
    app.setApplicationName('myWindow')

    main = myWindow()
    main.show()

    sys.exit(app.exec_())

这种方法的最佳之处在于,您可以记录来自主应用程序的模块/线程的消息,而无需保留对记录器的任何引用,例如,通过调用 logging.log(logging.INFO, logging_message)logging.info(logging_message)

【讨论】:

哦,太好了!我可能会将它用于我拥有的另一段代码。我当前的代码依赖于几个日志处理程序,所以我确实需要引用logger。但是由于您的示例,我现在了解了如何编写新型信号和插槽。谢谢! @Gilead 我很高兴你能发现它有用,但我不想坚持! :) 使用此代码,您不必保留对记录器的任何类型的引用,只要它位于同一个 Python 解释器进程中即可。我的回答通过从myThread 调用logging.info 而不是logger.info 来证明这一点......您还可以通过调用logging.getLogger('someLogger') 将输出定向到特定的记录器 如何在没有日志实例的情况下添加自定义日志处理程序?我需要那个功能,而logging 没有addHandler 方法。我不确定我是否理解(但请注意,这是我第一次使用 logging 模块,所以我不明白其中的细微差别!) 我很乐意为您提供帮助,请将其作为问题发布并在此处的评论中提及我【参考方案4】:

这里真正的问题是,您在主线程中休眠会阻塞整个 GUI 5 秒钟。您不能这样做,否则不会显示更新,用户将无法与您的应用交互等。日志记录问题只是该主要问题的次要结果。

如果您的真实程序从第三方模块调用某些代码需要 5 秒或执行某些阻塞操作,则会出现完全相同的问题。

一般来说,有两种方法可以在不阻塞 GUI(或其他基于事件循环的)应用程序的情况下进行缓慢的阻塞操作:

    在后台线程中执行工作。根据您的 GUI 框架,您通常不能从后台线程直接调用 GUI 上的函数或修改其对象;相反,您必须使用某种机制将消息发布到事件循环。在 Qt 中,您通常通过信号槽机制来执行此操作。详情请见this question。

    将作业分解为快速返回的非阻塞或保证仅非常短期阻塞的作业,每个作业在返回前安排下一个权利。 (对于某些 GUI 框架,您可以通过调用 safeYield 之类的方法或递归调用事件循环来执行等效的内联操作,但您不能在 Qt 中这样做。)

鉴于someProcess 是一些您无法修改的外部代码,它要么需要几秒钟才能完成,要么会阻塞,您不能使用选项 2。因此,选项 1 是:在后台运行它线程。

幸运的是,这很容易。 Qt 有办法做到这一点,但 Python 的方法更简单:

t = threading.Thread(target=someProcess)
t.start()

现在,您需要更改ConsoleWindowLogHandler.emit,这样它就不会直接修改textBox,而是发送一个信号以在主线程中完成该操作。请参阅Threads and QObjects 了解所有详细信息以及一些很好的示例。

更具体地说:Mandelbrot 示例使用RenderThread,它实际上并没有绘制任何东西,而是发送renderedImage 信号;然后MandelbrotWidget 有一个updatePixmap 插槽,它连接到renderedImage 信号。同样,您的日志处理程序实际上不会更新文本框,而是发送gotLogMessage 信号;那么你会有一个LogTextWidget 和一个连接到该信号的updateLog 插槽。当然,对于您的简单情况,您可以将它们放在一个类中,只要您使用信号槽连接而不是直接方法调用将两侧连接起来。

您可能希望将t 保留在某处并在关机期间保留join,或者设置t.daemon = True

无论哪种方式,如果您想知道someProcess 何时完成,您需要在完成时使用其他方式与您的主线程进行通信——同样,对于 Qt,通常的答案是发送一个信号。这也可以让您从someProcess 获得结果。而且您无需修改​​someProcess 即可执行此操作;只需定义一个调用someProcess 并发出其结果的封装函数,然后从后台线程调用该封装函数。

【讨论】:

谢谢。在我的真实代码中,someProcess() 调用一个 Python 模块,该模块调用记录器来提供状态更新,我希望能够在 QTextBrowser 中实时显示这些状态更新。我知道subprocess 让我以非阻塞方式捕获stdout,但在这种情况下,我想以非阻塞方式捕获日志事件。 Python 模块不知道 GUI。 @Gilead:在后台线程中使用subprocess(或QProcess,如果您愿意的话)并不比在后台线程中执行任何其他操作更难。我在这里错过了什么吗?你读过Threads and QObjects吗?创建后台线程很容易,并且从它们发出信号会自动跨线程工作,这就是您在 Qt 中处理阻塞的方式。 感谢您的快速回复。 subProcess 易于使用,但它必须调用外部程序——我不确定如何将它与 Python 模块(我无法修改)一起使用,另外我不确定日志记录模块将如何通信如果子流程是外部的。 QProcess 好像很相似。 @Gilead:我只提出了subprocess,因为您这样做了,而且我认为您的解决方案可能与您在子进程中运行其他人的脚本有关。如果您不想使用它,那么您只需要一个线程。我已经更新了解释的答案。 感谢您的回答。我以前从未写过任何线程,令我惊讶的是,在几个小时内,我设法弄清楚了如何去做。你是对的——这并不难,尤其是使用 Python。我在上面附上了一些新代码。它似乎做我想做的事。新旧风格的信号和插槽的混合虽然有点不雅,但我暂时不考虑它。再次感谢!

以上是关于如何从 Python 日志记录模块获得非阻塞/实时行为? (输出到 PyQt QTextBrowser)的主要内容,如果未能解决你的问题,请参考以下文章

如何从 Python 中的请求模块中完全删除任何日志记录

Linux 非阻塞 fifo(按需日志记录)

Python学习_日志模块:logging

在 python 项目中如何记录日志

Celery Python 日志记录配置仅从指定模块记录 DEBUG

如何使用日志记录 Python 模块写入文件?