首页 > 解决方案 > 如何在 PyQt5 应用程序中发出 Web 请求而不挂起 UI?

问题描述

我有一个在 Python 3.7 中运行的 PyQt5 应用程序,它发出很多 Web 请求,但一次不超过一两个。大多数请求很快,但有时需要几秒钟。整个 UI 挂起,直到请求完成。我在下面有一个完整的示例应用程序来演示这一点。

环顾四周,有些人建议使用QThread,但如果我尝试这样做,我会抱怨从错误的线程访问 UI 对象。该方法还阻止我将 Web 响应返回给调用者。在下面的示例代码中,响应仅用于打印状态码,但在实际应用中,会解析并使用 JSON 响应。我发现这个问题的大多数答案都超过五年了,通常引用 PyQt4 或更早版本,或者引用现在已经过时的包,例如grequestsrequest-threads。有些人引用httpx(仍处于测试阶段)或aiohttp,它们有自己的事件循环,并且在 PyQt5 应用程序中并不总是很好。

样本被尽可能地精简以证明问题。如果您单击“发出请求”并立即单击“单击计数器”,您将看到计数器在 Web 请求完成之前不会增加。这并不奇怪,因为请求阻塞了 Qt 事件循环。

我怎样才能简单地让当前代码在请求被提交和异步管理的地方工作,但仍然能够将响应传递回调用者?

这里是test.py

import sys
from datetime import datetime

import requests
from PyQt5 import QtWidgets, uic
from requests import RequestException


class UI(QtWidgets.QMainWindow):
    def __init__(self):
        super(UI, self).__init__()
        uic.loadUi('test.ui', self)
        self.web = WebManager(self)
        self.issue_request_button.clicked.connect(self.issue_request)
        self.issue_click_button.clicked.connect(self.click_counter)
        self.show()

    def click_counter(self):
        self.click_count_label.setText(str(int(self.click_count_label.text()) + 1))

    def issue_request(self):
        r = self.web.issue_get(self.request_url.text())
        print(r.status_code if r is not None else 'No successful response')


class WebManager:
    def __init__(self, gui):
        self.gui = gui

    def issue_get(self, url_request):
        try:
            start = datetime.now()
            headers = {'Accept': 'application/json', 'Content-Type': 'application/json'}
            r = requests.get(url_request, headers=headers)
            self.fill_request_response_info(r, datetime.now() - start)
            return r
        except RequestException as e:
            print(f'Request failed with an Exception of type {type(e).__name__}')
            return None

    def fill_request_response_info(self, response, total_request_time):
        request = response.request
        self.gui.request_url_textbox.setText(request.method + ' ' + request.url)
        self.gui.request_headers_textbox.setText('\n'.join(k + ':' + v for k, v in request.headers.items()))
        self.gui.response_status_textbox.setText(f'{response.status_code}  {response.reason}')
        self.gui.response_elapsed_time_textbox.setText(f'{response.elapsed} / {total_request_time}')
        self.gui.response_headers_textbox.setText('\n'.join(k + ':' + v for k, v in response.headers.items()))
        self.gui.response_body_textbox.setText(response.content.decode('utf-8'))


app = QtWidgets.QApplication(sys.argv)
window = UI()
app.exec()

这是它需要的 UI 文件test.ui

<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
 <class>MainWindow</class>
 <widget class="QMainWindow" name="MainWindow">
  <property name="geometry">
   <rect>
    <x>0</x>
    <y>0</y>
    <width>750</width>
    <height>734</height>
   </rect>
  </property>
  <property name="windowTitle">
   <string>Test Window</string>
  </property>
  <widget class="QWidget" name="centralwidget">
   <layout class="QVBoxLayout" name="verticalLayout_3">
    <item>
     <layout class="QHBoxLayout" name="horizontalLayout">
      <item>
       <widget class="QPushButton" name="issue_request_button">
        <property name="text">
         <string>Make Request</string>
        </property>
       </widget>
      </item>
      <item>
       <widget class="QLabel" name="label">
        <property name="text">
         <string>URL:</string>
        </property>
       </widget>
      </item>
      <item>
       <widget class="QLineEdit" name="request_url">
        <property name="text">
         <string>https://archive.org/advancedsearch.php?q=subject:google+sheets&amp;output=json</string>
        </property>
       </widget>
      </item>
     </layout>
    </item>
    <item>
     <layout class="QHBoxLayout" name="horizontalLayout_3">
      <item>
       <widget class="QPushButton" name="issue_click_button">
        <property name="text">
         <string>Click Counter</string>
        </property>
       </widget>
      </item>
      <item>
       <widget class="QLabel" name="click_count_label">
        <property name="text">
         <string>0</string>
        </property>
       </widget>
      </item>
      <item>
       <spacer name="horizontalSpacer">
        <property name="orientation">
         <enum>Qt::Horizontal</enum>
        </property>
        <property name="sizeHint" stdset="0">
         <size>
          <width>40</width>
          <height>20</height>
         </size>
        </property>
       </spacer>
      </item>
     </layout>
    </item>
    <item>
     <widget class="QGroupBox" name="http_request_groupBox">
      <property name="title">
       <string>HTTP Request</string>
      </property>
      <layout class="QVBoxLayout" name="verticalLayout">
       <item>
        <widget class="QLabel" name="req_url_label">
         <property name="text">
          <string>Request URL:</string>
         </property>
        </widget>
       </item>
       <item>
        <layout class="QHBoxLayout" name="horizontalLayout_2">
         <item>
          <widget class="QLineEdit" name="request_url_textbox"/>
         </item>
        </layout>
       </item>
       <item>
        <widget class="QLabel" name="req_headers_label">
         <property name="text">
          <string>Request Headers:</string>
         </property>
        </widget>
       </item>
       <item>
        <widget class="QTextEdit" name="request_headers_textbox"/>
       </item>
      </layout>
     </widget>
    </item>
    <item>
     <widget class="QGroupBox" name="http_response_groupBox">
      <property name="title">
       <string>HTTP Response</string>
      </property>
      <layout class="QVBoxLayout" name="verticalLayout_2" stretch="0,0,1,0,1">
       <item>
        <layout class="QGridLayout" name="gridLayout" rowstretch="0,0">
         <item row="0" column="0">
          <widget class="QLabel" name="http_status_label">
           <property name="text">
            <string>HTTP Status:</string>
           </property>
          </widget>
         </item>
         <item row="0" column="3">
          <widget class="QLabel" name="elapsed_time_label">
           <property name="text">
            <string>Elapsed Time:</string>
           </property>
          </widget>
         </item>
         <item row="1" column="0">
          <widget class="QLineEdit" name="response_status_textbox"/>
         </item>
         <item row="1" column="3">
          <widget class="QLineEdit" name="response_elapsed_time_textbox"/>
         </item>
        </layout>
       </item>
       <item>
        <widget class="QLabel" name="response_headers_label">
         <property name="text">
          <string>Response Headers:</string>
         </property>
        </widget>
       </item>
       <item>
        <widget class="QTextEdit" name="response_headers_textbox"/>
       </item>
       <item>
        <widget class="QLabel" name="response_body_label">
         <property name="text">
          <string>Response Body:</string>
         </property>
        </widget>
       </item>
       <item>
        <widget class="QTextEdit" name="response_body_textbox"/>
       </item>
      </layout>
     </widget>
    </item>
   </layout>
  </widget>
 </widget>
 <tabstops>
  <tabstop>issue_request_button</tabstop>
  <tabstop>request_url</tabstop>
  <tabstop>issue_click_button</tabstop>
  <tabstop>request_url_textbox</tabstop>
  <tabstop>request_headers_textbox</tabstop>
  <tabstop>response_status_textbox</tabstop>
  <tabstop>response_elapsed_time_textbox</tabstop>
  <tabstop>response_headers_textbox</tabstop>
  <tabstop>response_body_textbox</tabstop>
 </tabstops>
 <resources/>
 <connections/>
</ui>

标签: python-3.xpython-requestspyqt5

解决方案


推荐阅读