Python 刮痧+;pyqt5:信号仅在主线程错误时工作

Python 刮痧+;pyqt5:信号仅在主线程错误时工作,python,multithreading,scrapy,pyqt5,twisted,Python,Multithreading,Scrapy,Pyqt5,Twisted,我用scrapy编写了一个蜘蛛,它使用CrawlerProcess,运行良好 下一步,我需要一个GUI对话框,用于使用pyqt5生成的一些输入数据,并在scrapy工作时设置一个微调器。这项任务是使用一个虚拟过程完成的,并且运行良好 我知道多线程与CrawlerProcess不兼容,所以我将其更改为CrawlerRunner。无论如何,它失败了: signal.signal(signal.SIGINT, self.sigInt) File "C:\Program Files\Python36

我用scrapy编写了一个蜘蛛,它使用CrawlerProcess,运行良好

下一步,我需要一个GUI对话框,用于使用pyqt5生成的一些输入数据,并在scrapy工作时设置一个微调器。这项任务是使用一个虚拟过程完成的,并且运行良好

我知道多线程与CrawlerProcess不兼容,所以我将其更改为CrawlerRunner。无论如何,它失败了:

signal.signal(signal.SIGINT, self.sigInt)
  File "C:\Program Files\Python36\lib\signal.py", line 47, in signal
    handler = _signal.signal(_enum_to_int(signalnum), _enum_to_int(handler))
builtins.ValueError: signal only works in main thread
我创建了一个最小的示例来重现错误:

from PyQt5.QtWidgets import QApplication, QDialog, QTabWidget, QWidget, QGroupBox, QPushButton, QVBoxLayout
from PyQt5.QtCore import Qt, QRunnable, QMetaObject, pyqtSlot, QThreadPool
from PyQt5.QtGui import QIcon
from scrapy.crawler import CrawlerRunner
from scrapy import Request, Spider, Item, Field
from twisted.internet import reactor
from waitingspinnerwidget import QtWaitingSpinner

class RequestRunnable(QRunnable):
    def __init__(self, dialog):
        super(RequestRunnable, self).__init__()
        self.w = dialog
        print('runnable __init__')

    def run(self):
        print('run')
        crawler = CrawlerRunner({
            'USER_AGENT': 'Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 5.1)',
            'FEED_FORMAT': 'json',
            'FEED_URI': 'output.json',
            'DOWNLOAD_DELAY': 3,
            'LOG_STDOUT': True,
            'LOG_FILE': 'scrapy_output.txt',
            'ROBOTSTXT_OBEY': False,
            'RETRY_ENABLED': True,
            'RETRY_HTTP_CODES': [500, 503, 504, 400, 404, 408],
            'RETRY_TIMES': 5
        })

        # instantiate a spider
        spider = CustomSpider()
        d = crawler.crawl(spider)
        d.addBoth(lambda _: reactor.stop()) # I think this is for manually stopping the reactor when we are done
        reactor.run() #TODO gives error
        QMetaObject.invokeMethod(self.w, "FinishedDownload", Qt.QueuedConnection)


class CustomSpider(Spider):
    name = 'quotes_spider'
    allowed_domains = ['quotes.toscrape.com']
    start_urls = ['http://quotes.toscrape.com/']

    def parse(self, response):
        quotes = response.xpath("//div[@class='quote']//span[@class='text']/text()").extract()
        yield {'quotes': quotes}


class DownloadDataDialog(QDialog):
    def __init__(self, parent=None):
        super(DownloadDataDialog, self).__init__(parent)

        self.spinner = QtWaitingSpinner(self, True, True, Qt.ApplicationModal)

        tabWidget = QTabWidget(self)
        tabWidget.addTab(MyTab(tabWidget), "MyTab")

        mainLayout = QVBoxLayout()
        mainLayout.addWidget(tabWidget)
        self.setLayout(mainLayout)

        self.setWindowTitle("Download option chain data from web")


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

        dataGroup = QGroupBox('Data')

        getButton = QPushButton('Download')
        getButton.clicked.connect(self.download_data)

        dataLayout = QVBoxLayout()
        dataLayout.addWidget(getButton)
        dataGroup.setLayout(dataLayout)

        mainLayout = QVBoxLayout()
        mainLayout.addWidget(dataGroup)
        mainLayout.addStretch(1)
        self.setLayout(mainLayout)

    def download_data(self):
        self.parentWidget().window().spinner.start()
        runnable = RequestRunnable(self)
        QThreadPool.globalInstance().start(runnable)

    @pyqtSlot()
    def FinishedDownload(self):
        self.parentWidget().window().spinner.stop()

if __name__ == '__main__':

    import sys

    app = QApplication(sys.argv)

    tabdialog = DownloadDataDialog()
    tabdialog.show()
    sys.exit(app.exec_())
为了执行它,您需要waitingspinnerwidget.py:

# -*- coding: utf-8 -*-
"""
The MIT License (MIT)

Copyright (c) 2012-2014 Alexander Turkin
Copyright (c) 2014 William Hallatt
Copyright (c) 2015 Jacob Dawid
Copyright (c) 2016 Luca Weiss

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
"""

import math

from PyQt5.QtCore import *
from PyQt5.QtGui import *
from PyQt5.QtWidgets import *


class QtWaitingSpinner(QWidget):
    def __init__(self, parent, centerOnParent=True, disableParentWhenSpinning=False, modality=Qt.NonModal):
        super().__init__(parent, Qt.Dialog | Qt.FramelessWindowHint)

        self._centerOnParent = centerOnParent
        self._disableParentWhenSpinning = disableParentWhenSpinning

        # WAS IN initialize()
        self._color = QColor(Qt.black)
        self._roundness = 100.0
        self._minimumTrailOpacity = 3.14159265358979323846
        self._trailFadePercentage = 80.0
        self._revolutionsPerSecond = 1.57079632679489661923
        self._numberOfLines = 20
        self._lineLength = 10
        self._lineWidth = 2
        self._innerRadius = 10
        self._currentCounter = 0
        self._isSpinning = False

        self._timer = QTimer(self)
        self._timer.timeout.connect(self.rotate)
        self.updateSize()
        self.updateTimer()
        self.hide()
        # END initialize()

        self.setWindowModality(modality)
        self.setAttribute(Qt.WA_TranslucentBackground)

    def paintEvent(self, QPaintEvent):
        self.updatePosition()
        painter = QPainter(self)
        painter.fillRect(self.rect(), Qt.transparent)
        painter.setRenderHint(QPainter.Antialiasing, True)

        if self._currentCounter >= self._numberOfLines:
            self._currentCounter = 0

        painter.setPen(Qt.NoPen)
        for i in range(0, self._numberOfLines):
            painter.save()
            painter.translate(self._innerRadius + self._lineLength, self._innerRadius + self._lineLength)
            rotateAngle = float(360 * i) / float(self._numberOfLines)
            painter.rotate(rotateAngle)
            painter.translate(self._innerRadius, 0)
            distance = self.lineCountDistanceFromPrimary(i, self._currentCounter, self._numberOfLines)
            color = self.currentLineColor(distance, self._numberOfLines, self._trailFadePercentage,
                                          self._minimumTrailOpacity, self._color)
            painter.setBrush(color)
            painter.drawRoundedRect(QRect(0, -self._lineWidth / 2, self._lineLength, self._lineWidth), self._roundness,
                                    self._roundness, Qt.RelativeSize)
            painter.restore()

    def start(self):
        self.updatePosition()
        self._isSpinning = True
        self.show()

        if self.parentWidget and self._disableParentWhenSpinning:
            self.parentWidget().setEnabled(False)

        if not self._timer.isActive():
            self._timer.start()
            self._currentCounter = 0

    def stop(self):
        self._isSpinning = False
        self.hide()

        if self.parentWidget() and self._disableParentWhenSpinning:
            self.parentWidget().setEnabled(True)

        if self._timer.isActive():
            self._timer.stop()
            self._currentCounter = 0

    def setNumberOfLines(self, lines):
        self._numberOfLines = lines
        self._currentCounter = 0
        self.updateTimer()

    def setLineLength(self, length):
        self._lineLength = length
        self.updateSize()

    def setLineWidth(self, width):
        self._lineWidth = width
        self.updateSize()

    def setInnerRadius(self, radius):
        self._innerRadius = radius
        self.updateSize()

    def color(self):
        return self._color

    def roundness(self):
        return self._roundness

    def minimumTrailOpacity(self):
        return self._minimumTrailOpacity

    def trailFadePercentage(self):
        return self._trailFadePercentage

    def revolutionsPersSecond(self):
        return self._revolutionsPerSecond

    def numberOfLines(self):
        return self._numberOfLines

    def lineLength(self):
        return self._lineLength

    def lineWidth(self):
        return self._lineWidth

    def innerRadius(self):
        return self._innerRadius

    def isSpinning(self):
        return self._isSpinning

    def setRoundness(self, roundness):
        self._roundness = max(0.0, min(100.0, roundness))

    def setColor(self, color=Qt.black):
        self._color = QColor(color)

    def setRevolutionsPerSecond(self, revolutionsPerSecond):
        self._revolutionsPerSecond = revolutionsPerSecond
        self.updateTimer()

    def setTrailFadePercentage(self, trail):
        self._trailFadePercentage = trail

    def setMinimumTrailOpacity(self, minimumTrailOpacity):
        self._minimumTrailOpacity = minimumTrailOpacity

    def rotate(self):
        self._currentCounter += 1
        if self._currentCounter >= self._numberOfLines:
            self._currentCounter = 0
        self.update()

    def updateSize(self):
        size = (self._innerRadius + self._lineLength) * 2
        self.setFixedSize(size, size)

    def updateTimer(self):
        self._timer.setInterval(1000 / (self._numberOfLines * self._revolutionsPerSecond))

    def updatePosition(self):
        if self.parentWidget() and self._centerOnParent:
            self.move(self.parentWidget().width() / 2 - self.width() / 2,
                      self.parentWidget().height() / 2 - self.height() / 2)
            dialogCenter = self.mapToGlobal(self.rect().center())
            parentWindowCenter = self.parentWidget().window().mapToGlobal(self.parentWidget().window().rect().center())
            self.move(parentWindowCenter - dialogCenter)
            parentRect = QRect(self.parentWidget().mapToGlobal(QPoint(0, 0)), self.parentWidget().size())
            self.move(QStyle.alignedRect(Qt.LeftToRight, Qt.AlignCenter, self.size(), parentRect).topLeft())

    def lineCountDistanceFromPrimary(self, current, primary, totalNrOfLines):
        distance = primary - current
        if distance < 0:
            distance += totalNrOfLines
        return distance

    def currentLineColor(self, countDistance, totalNrOfLines, trailFadePerc, minOpacity, colorinput):
        color = QColor(colorinput)
        if countDistance == 0:
            return color
        minAlphaF = minOpacity / 100.0
        distanceThreshold = int(math.ceil((totalNrOfLines - 1) * trailFadePerc / 100.0))
        if countDistance > distanceThreshold:
            color.setAlphaF(minAlphaF)
        else:
            alphaDiff = color.alphaF() - minAlphaF
            gradient = alphaDiff / float(distanceThreshold + 1)
            resultAlpha = color.alphaF() - gradient * countDistance
            # If alpha is out of bounds, clip it.
            resultAlpha = min(1.0, max(0.0, resultAlpha))
            color.setAlphaF(resultAlpha)
        return color
#-*-编码:utf-8-*-
"""
麻省理工学院许可证(MIT)
版权所有(c)2012-2014亚历山大·图尔金
版权所有(c)2014 William Hallatt
版权所有(c)2015雅各布·达维德
版权所有(c)2016 Luca Weiss
特此向任何获得副本的人免费授予许可
本软件和相关文档文件(“软件”)的
在软件中不受限制,包括但不限于权利
使用、复制、修改、合并、发布、分发、再许可和/或销售
软件的副本,并允许向其提供软件的人员
按照以下条件提供:
上述版权声明和本许可声明应包含在所有
软件的副本或大部分。
本软件按“原样”提供,无任何形式的明示或明示担保
默示,包括但不限于适销性保证,
适用于特定用途且不受侵犯。在任何情况下
作者或版权持有人应承担任何索赔、损害或其他责任
无论是在合同诉讼、侵权诉讼或其他诉讼中,由以下原因引起的责任:,
与本软件有关,或与本软件的使用或其他交易有关
软件。
"""
输入数学
从PyQt5.QtCore导入*
从PyQt5.QtGui导入*
从PyQt5.QtWidgets导入*
类QtWaitingSpinner(QWidget):
定义初始(self,parent,centerOnParent=True,disableParentWhenSpinning=False,modity=Qt.NonModal):
super()
self.\u centerOnParent=centerOnParent
self.\u disableparentwhenpinning=disableparentwhenpinning
#在初始化()中
自身颜色=QColor(Qt.黑色)
自圆度=100.0
自最小牵引能力=3.14159265358979323846
自成比例=80.0
自旋转秒=1.570796632679489661923
self.\u numberOfLines=20
自。_线宽=10
自身线宽=2
自内半径=10
self.\u currentCounter=0
self.\u IsSpining=False
自定时器=QTimer(自)
self.\u timer.timeout.connect(self.rotate)
self.updateSize()
self.updateTimer()
self.hide()
#结束初始化()
self.setWindowModality(模态)
self.setAttribute(Qt.WA_半透明背景)
def paintEvent(自我、QPaintEvent):
self.updatePosition()
油漆工=油漆工(自身)
painter.fillRect(self.rect(),Qt.transparent)
painter.setRenderInt(QPainter.Antialiasing,True)
如果self.\u currentCounter>=self.\u行数:
self.\u currentCounter=0
画师设置笔(Qt.NoPen)
对于范围内的i(0,self.\u numberOfLines):
保存
painter.translate(self.\u innerRadius+self.\u lineLength,self.\u innerRadius+self.\u lineLength)
旋转角度=浮动(360*i)/浮动(自身数量)
画师。旋转(旋转角度)
painter.translate(自内半径,0)
距离=自线路计数距离主线路(i,自当前计数器,自线路数)
颜色=自身.currentLineColor(距离,自身.\u行数,自身.\u跟踪百分比,
self.\u最小产能、self.\u颜色)
画家。挫折(颜色)
画师。drawRoundedRect(QRect(0,-self.\u线宽/2,self.\u线宽,self.\u线宽),self.\u圆度,
自圆度(Qt.相对尺寸)
恢复
def启动(自):
self.updatePosition()
自锁定=真
self.show()
如果self.parentWidget和self.\u在启动时禁用parents:
self.parentWidget().setEnabled(False)
如果不是self.\u timer.isActive():
self.\u timer.start()
self.\u currentCounter=0
def停止(自):
self.\u IsSpining=False
self.hide()
如果self.parentWidget()和self.\u在启动时禁用parents:
self.parentWidget().setEnabled(True)
如果self.\u timer.isActive():
self.\u timer.stop()
self.\u currentCounter=0
def setNumberOfLines(自身,线路):
self.\u numberOfLines=行
self.\u currentCounter=0
self.updateTimer()
def setLineLength(自身,长度):
self.\u lineLength=长度
self.updateSize()
def SETLINEWITH(自身,宽度):
self.\u线宽=宽度
self.updateSize()
def setInnerRadius(自身,半径):
自内半径=半径
self.updateSize()
def颜色(自):
返回自我
def圆度(自身):
回归自我
def最小牵引能力(自):
返回自我。\u最小牵引能力
def trailFadePercentage(自):
返回自我。\u跟踪百分比
def转数秒(自身):
返回自我。_转数秒
def编号(自身):
返回自我。\u行数
def线路长度(自身):
返回自我。\u行长度
def线宽(自):
返回自身。\u线宽
def内半径(自):
返回自我。\u内半径
def isSpinning(自):
返回自我
def设置圆度(自身、圆度):
自圆度=最大值(0.0,最小值(100.0,圆度))
def setColor(自身,颜色=Qt.黑色):
选择
Traceback (most recent call last):
  File "C:\Users\BonifacioFdez\AppData\Roaming\Python\Python36\site-packages\twisted\internet\base.py", line 428, in fireEvent
    DeferredList(beforeResults).addCallback(self._continueFiring)
  File "C:\Users\BonifacioFdez\AppData\Roaming\Python\Python36\site-packages\twisted\internet\defer.py", line 322, in addCallback
    callbackKeywords=kw)
  File "C:\Users\BonifacioFdez\AppData\Roaming\Python\Python36\site-packages\twisted\internet\defer.py", line 311, in addCallbacks
    self._runCallbacks()
  File "C:\Users\BonifacioFdez\AppData\Roaming\Python\Python36\site-packages\twisted\internet\defer.py", line 654, in _runCallbacks
    current.result = callback(current.result, *args, **kw)
--- <exception caught here> ---
  File "C:\Users\BonifacioFdez\AppData\Roaming\Python\Python36\site-packages\twisted\internet\base.py", line 441, in _continueFiring
    callable(*args, **kwargs)
  File "C:\Users\BonifacioFdez\AppData\Roaming\Python\Python36\site-packages\twisted\internet\base.py", line 1256, in _reallyStartRunning
    self._handleSignals()
  File "C:\Users\BonifacioFdez\AppData\Roaming\Python\Python36\site-packages\twisted\internet\posixbase.py", line 295, in _handleSignals
    _SignalReactorMixin._handleSignals(self)
  File "C:\Users\BonifacioFdez\AppData\Roaming\Python\Python36\site-packages\twisted\internet\base.py", line 1221, in _handleSignals
    signal.signal(signal.SIGINT, self.sigInt)
  File "C:\Program Files\Python36\lib\signal.py", line 47, in signal
    handler = _signal.signal(_enum_to_int(signalnum), _enum_to_int(handler))
builtins.ValueError: signal only works in main thread
pip install qt5reactor
app = QApplication(sys.argv)

import qt5reactor
qt5reactor.install()

reactor.run()