PySide ожидает сигнала от основного потока в рабочем потоке - PullRequest
7 голосов
/ 15 марта 2012

Я решил добавить графический интерфейс к одному из моих скриптов.Скрипт представляет собой простой веб-скребок.Я решил использовать рабочий поток, так как загрузка и анализ данных могут занять некоторое время.Я решил использовать PySide, но мои знания Qt в целом весьма ограничены.

Поскольку сценарий должен ожидать ввода данных от пользователя при обнаружении капчи, я решил, что он должен подождать, пока QLineEdit не сработает returnPressed и затем отправить его содержимое в рабочий поток, чтобы он мог отправить его на проверку.Это должно быть лучше, чем занято - ожидание нажатия клавиши возврата.

Кажется, что ожидание сигнала не так просто, как я думал, и после поиска какое-то время я наткнулсянесколько решений, похожих на this .Сигнализация между потоками и локальный цикл обработки событий в рабочем потоке усложняют моё решение.

После нескольких часов работы оно все равно не будет работать.

Что предполагаетсячтобы это произошло:

  • Загрузите данные до обращения к капче и введите цикл
  • Скачайте капчу и отобразите ее пользователю, запустите QEventLoop, вызвав self.loop.exec_()
  • Выйдите из QEventLoop, вызвав loop.quit() в слоте рабочих потоков, который подключен через self.line_edit.returnPressed.connect(self.worker.stop_waiting) в main_window классе
  • Проверка капчи и цикла в случае сбоя проверки, в противном случае повторите последний URL, который долженбыть загружаемым сейчас, а затем перейти к следующему URL

Что происходит:

  • ... см. выше ...

  • Выход QEventLoop не работает.self.loop.isRunning() возвращает False после вызова его exit().self.isRunning возвращает True, так как нить, кажется, не умирает при странных обстоятельствах.Тем не менее поток останавливается на линии self.loop.exec_().Таким образом, поток застрял, выполняя цикл событий, хотя цикл событий говорит мне, что он больше не работает.

  • GUI отвечает так же, как и слоты класса рабочего потока.Я вижу текст, отправляемый в рабочий поток, состояние цикла событий и самого потока, но ничего после того, как вышеупомянутая строка не выполняется,

Код немногоСвернутый, как таковой, я добавляю немного псевдокод-python-mix, оставляя неважным:

class MainWindow(...):
    # couldn't find a way to send the text with the returnPressed signal, so I
    # added a helper signal, seems to work though. Doesn't work in the
    # constructor, might be a PySide bug?
    helper_signal = PySide.QtCore.Signal(str)
    def __init__(self):
        # ...setup...
        self.worker = WorkerThread()
        self.line_edit.returnPressed.connect(self.helper_slot)
        self.helper_signal.connect(self.worker.stop_waiting)

    @PySide.QtCore.Slot()
    def helper_slot(self):
        self.helper_signal.emit(self.line_edit.text())

class WorkerThread(PySide.QtCore.QThread):
    wait_for_input = PySide.QtCore.QEventLoop()

    def run(self):
        # ...download stuff...
        for url in list_of_stuff:
            self.results.append(get(url))

    @PySide.QtCore.Slot(str)
    def stop_waiting(self, text):
        self.solution = text
        # this definitely gets executed upon pressing return
        self.wait_for_input.exit()

    # a wrapper for requests.get to handle captcha
    def get(self, *args, **kwargs):
        result = requests.get(*args, **kwargs)
        while result.history: # redirect means captcha
            # ...parse and extract captcha...
            # ...display captcha to user via not shown signals to main thread...

            # wait until stop_waiting stops this event loop and as such the user
            # has entered something as a solution
            self.wait_for_input.exec_()

            # ...this part never get's executed, unless I remove the event
            # loop...

            post = { # ...whatever data necessary plus solution... }
            # send the solution
            result = requests.post('http://foo.foo/captcha_url'), data=post)
        # no captcha was there, return result
        return result

frame = MainWindow()
frame.show()
frame.worker.start()
app.exec_()

Ответы [ 2 ]

5 голосов
/ 15 марта 2012

То, что вы описываете, выглядит идеально для QWaitCondition.

Простой пример:

import sys
from PySide import QtCore, QtGui

waitCondition = QtCore.QWaitCondition()
mutex = QtCore.QMutex()

class Main(QtGui.QMainWindow):
    def __init__(self, parent=None):
        super(Main, self).__init__()

        self.text = QtGui.QLineEdit()
        self.text.returnPressed.connect(self.wakeup)

        self.worker = Worker(self)
        self.worker.start()

        self.setCentralWidget(self.text)

    def wakeup(self):
        waitCondition.wakeAll()

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

    def run(self):
        print "initial stuff"

        mutex.lock()
        waitCondition.wait(mutex)
        mutex.unlock()

        print "after returnPressed"

if __name__=="__main__":      
    app = QtGui.QApplication(sys.argv)
    m = Main()
    m.show()
    sys.exit(app.exec_())
3 голосов
/ 15 марта 2012

Слот выполняется внутри потока, который создал QThread, а не в потоке, которым управляет QThread.

Вам необходимо переместить QObject в поток и подключить его слотк сигналу, и этот слот будет выполняться внутри потока:

class SignalReceiver(QtCore.QObject):
    def __init__(self):
        self.eventLoop = QEventLoop(self)             

    @PySide.QtCore.Slot(str)
    def stop_waiting(self, text):                   
        self.text = text
        eventLoop.exit()

    def wait_for_input(self):
        eventLoop.exec()
        return self.text

class MainWindow(...):
     ...
     def __init__(self):
        ...
        self.helper_signal.connect(self.worker.signalReceiver.stop_waiting)

class WorkerThread(PySide.QtCore.QThread): 
    def __init__(self):
        self.signalReceiver = SignalReceiver() 
        # After the following call the slots will be executed in the thread             
        self.signalReceiver.moveToThread(self)    

    def get(self,  *args, **kwargs):
        result = requests.get(*args, **kwargs)
        while result.history:
            ...
            self.result = self.signalReceiver.wait_for_input()   
...