PySide espera la señal del hilo principal en un hilo trabajador

Decidí agregar una GUI a uno de mis scripts. El script es un raspador web simple. Decidí usar un subproceso de trabajo, ya que la descarga y el análisis de los datos pueden llevar un tiempo. Decidí usar PySide, pero mi conocimiento de Qt en general es bastante limitado.

Como se supone que el script espera la entrada del usuario al encontrar un captcha, decidí que debería esperar hasta que QLineEdit dispare returnPressed y luego envíe su contenido al subproceso de trabajo para que pueda enviarlo para su validación. Eso debería ser mejor que estar ocupado, esperando que se presione la tecla de retorno.

Parece que esperar una señal no es tan sencillo como pensé y después de buscarlo encontré varias soluciones similares a esta . Sin embargo, la señalización en subprocesos y un bucle de evento local en el subproceso de trabajo hacen que mi solución sea un poco más complicada.

Después de jugar con él durante varias horas, todavía no funcionará.

Lo que se supone que sucede:

  • Descargue los datos hasta que sean referidos a captcha e ingrese un ciclo
  • Descargue captcha y muéstrelo al usuario; inicie QEventLoop llamando a self.loop.exec_()
  • Salga de QEventLoop llamando a loop.quit() en una ranura de subprocesos de trabajo que se conecta a través de self.line_edit.returnPressed.connect(self.worker.stop_waiting) en la clase main_window
  • Valide el captcha y el bucle si la validación falla, de lo contrario, vuelva a intentar la última url que debería ser descargable ahora, luego continúe con la siguiente url

Lo que pasa:

  • …véase más arriba…

  • Salir de QEventLoop no funciona. self.loop.isRunning() devuelve False después de llamar a exit() . self.isRunning devuelve True , como tal, el hilo no parece morir en extrañas circunstancias. Aún así, el hilo se detiene en la línea self.loop.exec_() . Como tal, el hilo está atascado ejecutando el bucle de eventos, aunque el bucle de eventos me dice que ya no se está ejecutando.

  • La GUI responde como lo hacen las ranuras de la clase de subproceso de trabajo. Puedo ver el texto enviado al subproceso de trabajo, el estado del bucle de eventos y el propio subproceso, pero no se ejecuta nada después de la línea mencionada anteriormente.

El código es un poco complicado, como tal, agrego un poco de pseudo-código-python-mix dejando de lado lo que no es importante:

 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_() 

La ranura se ejecuta dentro del hilo que creó el QThread , y no en el hilo que controla el QThread .

QObject mover un QObject al hilo y conectar su ranura a la señal, y esa ranura se ejecutará dentro del hilo:

 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() 

Lo que estás describiendo parece ideal para QWaitCondition .

Ejemplo simple:

 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_())