Python编程:基于requests的http客户端程序

     一个带有图形界面的完整HTTP客户端程序,使用PyQt5作为UI框架,Requests库作为HTTP客户端核心。

完整工程代码

import sys
import json
import os
from typing import Dict, Optional, Union

import requests
from requests.auth import HTTPBasicAuth, HTTPDigestAuth
from PyQt5.QtWidgets import (QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout,
                             QLabel, QLineEdit, QComboBox, QTextEdit, QPushButton,
                             QGroupBox, QFormLayout, QTabWidget, QFileDialog, QMessageBox,
                             QTableWidget, QTableWidgetItem, QHeaderView, QCheckBox)
from PyQt5.QtCore import Qt, QThread, pyqtSignal


class RequestWorker(QThread):
    """
    工作线程,用于执行HTTP请求
    """
    finished = pyqtSignal(requests.Response, str)

    def __init__(self, method, url, params=None, data=None, json_data=None,
                 headers=None, auth=None, auth_type=None, files=None,
                 timeout=30, verify_ssl=True, allow_redirects=True, proxies=None):
        super().__init__()
        self.method = method
        self.url = url
        self.params = params
        self.data = data
        self.json_data = json_data
        self.headers = headers
        self.auth = auth
        self.auth_type = auth_type
        self.files = files
        self.timeout = timeout
        self.verify_ssl = verify_ssl
        self.allow_redirects = allow_redirects
        self.proxies = proxies
        self.error = None

    def run(self):
        try:
            # 准备认证
            auth_obj = self._prepare_auth(self.auth, self.auth_type)

            session = requests.Session()
            response = session.request(
                method=self.method.upper(),
                url=self.url,
                params=self.params,
                data=self.data,
                json=self.json_data,
                headers=self.headers,
                auth=auth_obj,
                files=self.files,
                timeout=self.timeout,
                allow_redirects=self.allow_redirects,
                verify=self.verify_ssl,
                proxies=self.proxies,
            )
            self.finished.emit(response, "")
        except Exception as e:
            self.finished.emit(None, str(e))

    @staticmethod
    def _prepare_auth(auth: Optional[Union[tuple, str]], auth_type: str):
        """准备认证对象"""
        if not auth:
            return None

        if auth_type == "basic":
            if isinstance(auth, tuple) and len(auth) == 2:
                return HTTPBasicAuth(*auth)
            elif isinstance(auth, str):
                username, _, password = auth.partition(":")
                return HTTPBasicAuth(username, password)
        elif auth_type == "digest":
            if isinstance(auth, tuple) and len(auth) == 2:
                return HTTPDigestAuth(*auth)
            elif isinstance(auth, str):
                username, _, password = auth.partition(":")
                return HTTPDigestAuth(username, password)
        return None


class HTTPClientUI(QMainWindow):
    def __init__(self):
        super().__init__()
        self.setWindowTitle("HTTP客户端")
        self.setGeometry(100, 100, 900, 700)
        
        # 初始化UI
        self.init_ui()
        
        # 初始化变量
        self.current_file = None
        self.worker = None

    def init_ui(self):
        """初始化用户界面"""
        main_widget = QWidget()
        main_layout = QVBoxLayout()
        
        # 创建标签页
        tab_widget = QTabWidget()
        
        # 请求标签页
        request_tab = QWidget()
        self.setup_request_tab(request_tab)
        tab_widget.addTab(request_tab, "请求")
        
        # 响应标签页
        response_tab = QWidget()
        self.setup_response_tab(response_tab)
        tab_widget.addTab(response_tab, "响应")
        
        # 历史标签页
        history_tab = QWidget()
        self.setup_history_tab(history_tab)
        tab_widget.addTab(history_tab, "历史")
        
        main_layout.addWidget(tab_widget)
        main_widget.setLayout(main_layout)
        self.setCentralWidget(main_widget)

    def setup_request_tab(self, tab):
        """设置请求标签页"""
        layout = QVBoxLayout()
        
        # 请求方法组
        method_group = QGroupBox("请求设置")
        method_layout = QFormLayout()
        
        self.method_combo = QComboBox()
        self.method_combo.addItems(["GET", "POST", "PUT", "DELETE", "HEAD", "OPTIONS", "PATCH"])
        method_layout.addRow(QLabel("方法:"), self.method_combo)
        
        self.url_edit = QLineEdit("https://httpbin.org/get")
        method_layout.addRow(QLabel("URL:"), self.url_edit)
        
        self.send_btn = QPushButton("发送请求")
        self.send_btn.clicked.connect(self.send_request)
        method_layout.addRow(self.send_btn)
        
        method_group.setLayout(method_layout)
        layout.addWidget(method_group)
        
        # 参数组
        params_group = QGroupBox("参数")
        params_layout = QVBoxLayout()
        
        self.params_table = QTableWidget(0, 2)
        self.params_table.setHorizontalHeaderLabels(["参数名", "值"])
        self.params_table.horizontalHeader().setSectionResizeMode(QHeaderView.Stretch)
        
        params_btn_layout = QHBoxLayout()
        add_param_btn = QPushButton("添加参数")
        add_param_btn.clicked.connect(self.add_parameter_row)
        remove_param_btn = QPushButton("删除选中")
        remove_param_btn.clicked.connect(self.remove_parameter_row)
        
        params_btn_layout.addWidget(add_param_btn)
        params_btn_layout.addWidget(remove_param_btn)
        
        params_layout.addWidget(self.params_table)
        params_layout.addLayout(params_btn_layout)
        params_group.setLayout(params_layout)
        layout.addWidget(params_group)
        
        # 请求体组
        self.body_group = QGroupBox("请求体")
        body_layout = QVBoxLayout()
        
        self.body_type_combo = QComboBox()
        self.body_type_combo.addItems(["无", "表单数据", "JSON", "文件"])
        self.body_type_combo.currentIndexChanged.connect(self.update_body_ui)
        body_layout.addWidget(self.body_type_combo)
        
        # 表单数据
        self.form_table = QTableWidget(0, 2)
        self.form_table.setHorizontalHeaderLabels(["字段名", "值"])
        self.form_table.horizontalHeader().setSectionResizeMode(QHeaderView.Stretch)
        
        form_btn_layout = QHBoxLayout()
        add_form_btn = QPushButton("添加字段")
        add_form_btn.clicked.connect(self.add_form_row)
        remove_form_btn = QPushButton("删除选中")
        remove_form_btn.clicked.connect(self.remove_form_row)
        
        form_btn_layout.addWidget(add_form_btn)
        form_btn_layout.addWidget(remove_form_btn)
        
        self.form_widget = QWidget()
        form_layout = QVBoxLayout()
        form_layout.addWidget(self.form_table)
        form_layout.addLayout(form_btn_layout)
        self.form_widget.setLayout(form_layout)
        body_layout.addWidget(self.form_widget)
        
        # JSON数据
        self.json_edit = QTextEdit()
        self.json_edit.setPlaceholderText('{"key": "value"}')
        self.json_widget = QWidget()
        json_layout = QVBoxLayout()
        json_layout.addWidget(self.json_edit)
        self.json_widget.setLayout(json_layout)
        body_layout.addWidget(self.json_widget)
        
        # 文件上传
        self.file_edit = QLineEdit()
        self.file_edit.setPlaceholderText("选择文件...")
        self.file_edit.setReadOnly(True)
        browse_btn = QPushButton("浏览...")
        browse_btn.clicked.connect(self.browse_file)
        
        file_layout = QHBoxLayout()
        file_layout.addWidget(self.file_edit)
        file_layout.addWidget(browse_btn)
        
        self.file_widget = QWidget()
        file_widget_layout = QVBoxLayout()
        file_widget_layout.addLayout(file_layout)
        self.file_widget.setLayout(file_widget_layout)
        body_layout.addWidget(self.file_widget)
        
        self.body_group.setLayout(body_layout)
        layout.addWidget(self.body_group)
        
        # 其他选项组
        options_group = QGroupBox("其他选项")
        options_layout = QFormLayout()
        
        self.verify_ssl_check = QCheckBox("验证SSL证书")
        self.verify_ssl_check.setChecked(True)
        options_layout.addRow(self.verify_ssl_check)
        
        self.allow_redirects_check = QCheckBox("允许重定向")
        self.allow_redirects_check.setChecked(True)
        options_layout.addRow(self.allow_redirects_check)
        
        self.timeout_edit = QLineEdit("30")
        options_layout.addRow(QLabel("超时(秒):"), self.timeout_edit)
        
        self.proxy_edit = QLineEdit()
        self.proxy_edit.setPlaceholderText("http://proxy.example.com:8080")
        options_layout.addRow(QLabel("代理:"), self.proxy_edit)
        
        options_group.setLayout(options_layout)
        layout.addWidget(options_group)
        
        # 认证组
        auth_group = QGroupBox("认证")
        auth_layout = QFormLayout()
        
        self.auth_type_combo = QComboBox()
        self.auth_type_combo.addItems(["无", "Basic", "Digest"])
        auth_layout.addRow(QLabel("认证类型:"), self.auth_type_combo)
        
        self.username_edit = QLineEdit()
        self.username_edit.setPlaceholderText("用户名")
        auth_layout.addRow(QLabel("用户名:"), self.username_edit)
        
        self.password_edit = QLineEdit()
        self.password_edit.setPlaceholderText("密码")
        self.password_edit.setEchoMode(QLineEdit.Password)
        auth_layout.addRow(QLabel("密码:"), self.password_edit)
        
        auth_group.setLayout(auth_layout)
        layout.addWidget(auth_group)
        
        tab.setLayout(layout)
        
        # 初始化UI状态
        self.update_body_ui(0)
        self.json_widget.hide()
        self.file_widget.hide()

    def setup_response_tab(self, tab):
        """设置响应标签页"""
        layout = QVBoxLayout()
        
        # 响应信息组
        info_group = QGroupBox("响应信息")
        info_layout = QFormLayout()
        
        self.status_label = QLabel("状态: 未发送请求")
        info_layout.addRow(self.status_label)
        
        self.time_label = QLabel("耗时: -")
        info_layout.addRow(self.time_label)
        
        self.size_label = QLabel("大小: -")
        info_layout.addRow(self.size_label)
        
        info_group.setLayout(info_layout)
        layout.addWidget(info_group)
        
        # 响应头组
        headers_group = QGroupBox("响应头")
        headers_layout = QVBoxLayout()
        
        self.headers_table = QTableWidget(0, 2)
        self.headers_table.setHorizontalHeaderLabels(["头名称", "值"])
        self.headers_table.horizontalHeader().setSectionResizeMode(QHeaderView.Stretch)
        
        headers_layout.addWidget(self.headers_table)
        headers_group.setLayout(headers_layout)
        layout.addWidget(headers_group)
        
        # 响应体组
        body_group = QGroupBox("响应体")
        body_layout = QVBoxLayout()
        
        self.body_tabs = QTabWidget()
        
        # 原始响应
        self.raw_text = QTextEdit()
        self.raw_text.setReadOnly(True)
        self.body_tabs.addTab(self.raw_text, "原始")
        
        # 预览
        self.preview_text = QTextEdit()
        self.preview_text.setReadOnly(True)
        self.body_tabs.addTab(self.preview_text, "预览")
        
        body_layout.addWidget(self.body_tabs)
        body_group.setLayout(body_layout)
        layout.addWidget(body_group)
        
        tab.setLayout(layout)

    def setup_history_tab(self, tab):
        """设置历史标签页"""
        layout = QVBoxLayout()
        
        self.history_table = QTableWidget(0, 5)
        self.history_table.setHorizontalHeaderLabels(["时间", "方法", "URL", "状态码", "耗时"])
        self.history_table.horizontalHeader().setSectionResizeMode(QHeaderView.Stretch)
        self.history_table.doubleClicked.connect(self.load_history_item)
        
        btn_layout = QHBoxLayout()
        clear_btn = QPushButton("清空历史")
        clear_btn.clicked.connect(self.clear_history)
        
        btn_layout.addWidget(clear_btn)
        
        layout.addWidget(self.history_table)
        layout.addLayout(btn_layout)
        tab.setLayout(layout)

    def update_body_ui(self, index):
        """根据选择的请求体类型更新UI"""
        self.form_widget.hide()
        self.json_widget.hide()
        self.file_widget.hide()
        
        if index == 1:  # 表单数据
            self.form_widget.show()
        elif index == 2:  # JSON
            self.json_widget.show()
        elif index == 3:  # 文件
            self.file_widget.show()

    def add_parameter_row(self):
        """添加参数行"""
        row = self.params_table.rowCount()
        self.params_table.insertRow(row)

    def remove_parameter_row(self):
        """删除选中的参数行"""
        selected = self.params_table.selectedItems()
        if selected:
            row = selected[0].row()
            self.params_table.removeRow(row)

    def add_form_row(self):
        """添加表单行"""
        row = self.form_table.rowCount()
        self.form_table.insertRow(row)

    def remove_form_row(self):
        """删除选中的表单行"""
        selected = self.form_table.selectedItems()
        if selected:
            row = selected[0].row()
            self.form_table.removeRow(row)

    def browse_file(self):
        """浏览文件"""
        file_path, _ = QFileDialog.getOpenFileName(self, "选择文件")
        if file_path:
            self.file_edit.setText(file_path)

    def send_request(self):
        """发送HTTP请求"""
        # 获取请求参数
        method = self.method_combo.currentText()
        url = self.url_edit.text().strip()
        
        if not url:
            QMessageBox.warning(self, "错误", "URL不能为空")
            return
        
        # 准备查询参数
        params = {}
        for row in range(self.params_table.rowCount()):
            key_item = self.params_table.item(row, 0)
            value_item = self.params_table.item(row, 1)
            
            if key_item and key_item.text().strip():
                key = key_item.text().strip()
                value = value_item.text().strip() if value_item else ""
                params[key] = value
        
        # 准备请求体
        data = None
        json_data = None
        files = None
        
        body_type = self.body_type_combo.currentIndex()
        if body_type == 1:  # 表单数据
            data = {}
            for row in range(self.form_table.rowCount()):
                key_item = self.form_table.item(row, 0)
                value_item = self.form_table.item(row, 1)
                
                if key_item and key_item.text().strip():
                    key = key_item.text().strip()
                    value = value_item.text().strip() if value_item else ""
                    data[key] = value
        elif body_type == 2:  # JSON
            json_text = self.json_edit.toPlainText().strip()
            if json_text:
                try:
                    json_data = json.loads(json_text)
                except json.JSONDecodeError as e:
                    QMessageBox.warning(self, "错误", f"无效的JSON格式: {str(e)}")
                    return
        elif body_type == 3:  # 文件
            file_path = self.file_edit.text().strip()
            if file_path:
                if os.path.exists(file_path):
                    files = {'file': open(file_path, 'rb')}
                else:
                    QMessageBox.warning(self, "错误", "文件不存在")
                    return
        
        # 准备请求头
        headers = {
            'User-Agent': 'PyQt HTTP Client/1.0'
        }
        
        # 准备认证
        auth = None
        auth_type = None
        if self.auth_type_combo.currentIndex() > 0:  # 有认证
            username = self.username_edit.text().strip()
            password = self.password_edit.text().strip()
            
            if username:
                auth_type = self.auth_type_combo.currentText().lower()
                auth = (username, password)
        
        # 准备其他选项
        timeout = int(self.timeout_edit.text()) if self.timeout_edit.text().isdigit() else 30
        verify_ssl = self.verify_ssl_check.isChecked()
        allow_redirects = self.allow_redirects_check.isChecked()
        
        proxies = None
        proxy_text = self.proxy_edit.text().strip()
        if proxy_text:
            proxies = {
                'http': proxy_text,
                'https': proxy_text,
            }
        
        # 禁用发送按钮,防止重复发送
        self.send_btn.setEnabled(False)
        
        # 创建工作线程
        self.worker = RequestWorker(
            method=method,
            url=url,
            params=params,
            data=data,
            json_data=json_data,
            headers=headers,
            auth=auth,
            auth_type=auth_type,
            files=files,
            timeout=timeout,
            verify_ssl=verify_ssl,
            allow_redirects=allow_redirects,
            proxies=proxies
        )
        self.worker.finished.connect(self.handle_response)
        self.worker.start()

    def handle_response(self, response, error):
        """处理响应"""
        # 重新启用发送按钮
        self.send_btn.setEnabled(True)
        
        if error:
            QMessageBox.critical(self, "错误", f"请求失败: {error}")
            return
        
        # 更新响应信息
        self.status_label.setText(f"状态: {response.status_code} {response.reason}")
        self.time_label.setText(f"耗时: {response.elapsed.total_seconds():.3f}秒")
        self.size_label.setText(f"大小: {len(response.content)}字节")
        
        # 更新响应头
        self.headers_table.setRowCount(0)
        for key, value in response.headers.items():
            row = self.headers_table.rowCount()
            self.headers_table.insertRow(row)
            self.headers_table.setItem(row, 0, QTableWidgetItem(key))
            self.headers_table.setItem(row, 1, QTableWidgetItem(value))
        
        # 更新响应体
        content_type = response.headers.get('Content-Type', '')
        
        # 原始响应
        try:
            text = response.text
        except UnicodeDecodeError:
            text = f"二进制内容 ({len(response.content)} 字节)"
        
        self.raw_text.setPlainText(text)
        
        # 预览响应
        if 'application/json' in content_type:
            try:
                json_data = response.json()
                self.preview_text.setPlainText(json.dumps(json_data, indent=2, ensure_ascii=False))
            except ValueError:
                self.preview_text.setPlainText(text)
        else:
            self.preview_text.setPlainText(text)
        
        # 添加到历史记录
        self.add_to_history(response)
        
        # 关闭文件(如果有)
        if hasattr(response, 'request') and hasattr(response.request, 'files'):
            if response.request.files:
                for f in response.request.files.values():
                    if hasattr(f, 'close'):
                        f.close()

    def add_to_history(self, response):
        """添加到历史记录"""
        row = self.history_table.rowCount()
        self.history_table.insertRow(row)
        
        # 时间
        from datetime import datetime
        time_str = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
        self.history_table.setItem(row, 0, QTableWidgetItem(time_str))
        
        # 方法
        method = response.request.method
        self.history_table.setItem(row, 1, QTableWidgetItem(method))
        
        # URL
        url = response.request.url
        self.history_table.setItem(row, 2, QTableWidgetItem(url))
        
        # 状态码
        status = str(response.status_code)
        self.history_table.setItem(row, 3, QTableWidgetItem(status))
        
        # 耗时
        elapsed = f"{response.elapsed.total_seconds():.3f}s"
        self.history_table.setItem(row, 4, QTableWidgetItem(elapsed))

    def load_history_item(self, index):
        """加载历史记录项"""
        row = index.row()
        method = self.history_table.item(row, 1).text()
        url = self.history_table.item(row, 2).text()
        
        self.method_combo.setCurrentText(method)
        self.url_edit.setText(url)
        
        # 切换到请求标签页
        self.centralWidget().findChild(QTabWidget).setCurrentIndex(0)

    def clear_history(self):
        """清空历史记录"""
        self.history_table.setRowCount(0)


if __name__ == "__main__":
    app = QApplication(sys.argv)
    client = HTTPClientUI()
    client.show()
    sys.exit(app.exec_())

功能特点

  1. 完整的HTTP方法支持:GET, POST, PUT, DELETE, HEAD, OPTIONS, PATCH

  2. 多标签页界面

    • 请求设置

    • 响应查看

    • 历史记录

  3. 请求参数支持

    • URL查询参数

    • 表单数据

    • JSON数据

    • 文件上传

  4. 认证支持

    • Basic认证

    • Digest认证

  5. 其他功能

    • 自定义请求头

    • 代理设置

    • SSL验证控制

    • 重定向控制

    • 超时设置

  6. 响应查看

    • 原始响应

    • 格式化预览(特别是JSON)

    • 响应头查看

  7. 历史记录

    • 保存所有请求记录

    • 双击可重新加载历史请求

使用说明

  1. 安装依赖

    pip install PyQt5 requests
  2. 运行程序

    python http_client_ui.py
  3. 基本使用

    • 选择HTTP方法

    • 输入URL

    • 根据需要添加参数、请求体等

    • 点击"发送请求"按钮

    • 在"响应"标签页查看结果

程序结构

  1. RequestWorker类:继承自QThread,用于在后台线程执行HTTP请求,避免阻塞UI

  2. HTTPClientUI类:主窗口类,包含所有UI组件和逻辑

    • setup_request_tab():设置请求标签页

    • setup_response_tab():设置响应标签页

    • setup_history_tab():设置历史标签页

  3. 信号槽机制:用于线程间通信,保持UI响应

这个程序提供了完整的HTTP客户端功能,界面友好,适合日常API测试和开发使用。

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值