返回

PyQt6 MySQL应用PyInstaller打包崩溃终极排查

python

PyQt6 应用、MySQL 与 PyInstaller 打包后的崩溃问题排查

使用 PyQt6、MySQL 数据库和 MySQL Connector 开发的应用程序,在 Python 环境下运行正常,但使用 PyInstaller 打包成可执行文件 (exe) 后,出现无法打开或者打开后崩溃的情况,这让人很头疼。 这个示例中的 CRUD 应用,登录页面正常显示,输入用户名密码并点击登录按钮后崩溃,主应用甚至无法启动。 下面,我们来分析原因,并给出解决方法。

一、 问题原因分析

这类问题通常由以下几个方面引起:

  1. 数据库连接配置问题: 打包后的应用程序运行环境与开发环境不同,可能导致数据库连接参数失效。
  2. 依赖项缺失: PyInstaller 未能正确识别或打包所有必要的依赖项,尤其是与 MySQL Connector 相关的动态链接库或驱动。
  3. 文件路径问题: 应用程序内部使用了相对路径访问某些资源文件(例如配置文件、图片等),打包后路径失效。
  4. 权限问题: 有时打包后的程序可能由于缺少需要的操作系统的运行权限而导致崩溃.
  5. PyInstaller版本兼容性: 不同版本的PyInstaller对于各种第三方库有着兼容上的差异。

二、 解决方案

针对以上原因,可以尝试以下解决方案:

1. 数据库连接配置检查与修改

  • 原理: 确保打包后的应用能找到正确的数据库服务器地址、端口、用户名、密码和数据库名称。

  • 方法:

    • 环境变量: 把数据库连接参数放在系统环境变量中,程序从环境变量读取。 这样做的好处是避免直接把敏感信息硬编码到代码里。
    • 配置文件: 将数据库连接参数存储在单独的配置文件(如 .ini, .json, .yaml)中,应用启动时读取配置文件。打包时,务必将配置文件包含在内。
    • 绝对路径 (不推荐): 如果是本地数据库,可以尝试数据库路径。但这大大降低了程序的可移植性。
  • 代码示例 (环境变量方式):

import os
import mysql.connector
from PyQt6.QtWidgets import QMessageBox

def create_connection():
    try:
        conn = mysql.connector.connect(
            host=os.environ.get('DB_HOST', 'localhost'), # 从环境变量读取,默认localhost
            user=os.environ.get('DB_USER', 'crud_user'),
            password=os.environ.get('DB_PASSWORD', '12345'),
            database=os.environ.get('DB_NAME', 'crud_app'),
            port=int(os.environ.get('DB_PORT', '3307'))  # 转换成整数
        )
        return conn
    except mysql.connector.Error as err:
        QMessageBox.critical(None, "Database Error", f"Failed to connect to database:\n{err}")
        return None
#设置环境变量 (仅本次运行有效. 为了永久有效, 请设置操作系统的环境变量)
os.environ['DB_HOST'] = 'localhost'
os.environ['DB_USER'] = 'crud_user'
os.environ['DB_PASSWORD'] = '12345'
os.environ['DB_NAME'] = 'crud_app'
os.environ['DB_PORT'] = '3307'

2. 显式包含 MySQL Connector 相关文件

  • 原理: PyInstaller 可能无法自动检测到 mysql-connector-python 的某些关键文件。需要手动告诉 PyInstaller 把它们包含进来。
  • 方法:
    • --hidden-import: 使用 --hidden-import 参数添加 mysql.connector.locales.engmysql.connector 以及相关的子模块。

    • 修改 .spec 文件: 更精确地控制打包过程。(更推荐的方法)

    1. 使用 pyi-makespec options your_script.py 生成 .spec文件。
    2. .spec 文件里编辑 hiddenimports 列表。
    3. 使用 pyinstaller your_script.spec 使用这个修改后的spec来打包.
  • 命令行示例 (使用 --hidden-import):
pyinstaller --onefile --windowed --hidden-import=mysql.connector.locales.eng --hidden-import=mysql.connector  your_script.py
  • 代码示例(修改 .spec 文件,假设已经生成了 your_script.spec):
# your_script.spec  (部分)
...

a = Analysis(['your_script.py'],
             pathex=[],
             binaries=[],
             datas=[],
             hiddenimports=['mysql.connector.locales.eng', 'mysql.connector', 'mysql.connector.connection_cext', 'mysql.connector.constants','mysql.connector.conversion','mysql.connector.cursor_cext','mysql.connector.dbapi','mysql.connector.errors','mysql.connector.locales','mysql.connector.network','mysql.connector.protocol','mysql.connector.time','mysql.connector.types','mysql.connector.utils','mysql.connector.charsets', 'mysql.connector.charsets.codec'], # 在这里添加
             hookspath=[],
             runtime_hooks=[],
             excludes=[],
             win_no_prefer_redirects=False,
             win_private_assemblies=False,
             cipher=block_cipher,
             noarchive=False)
...

3. 检查和修改文件路径

  • 原理: 开发时使用的相对路径在打包后可能不再有效。

  • 方法: 如果程序中使用了相对路径来读取其他的文件(除了代码以外, 例如数据库的证书,一个配置文件, 一个数据表单等等),要修改成在运行是获取当前exe所在的绝对路径并以此作为基础,以拼接出正确的其他文件的绝对路径。

  • 代码实例:

import os
import sys
from PyQt6.QtWidgets import QApplication, QWidget, QLabel, QVBoxLayout
from pathlib import Path
# 用来在程序运行时判断目前是代码还是打包的exe文件
def resource_path(relative_path):
    """ 获取资源的绝对路径。 """
    if getattr(sys, 'frozen', False):
        # 打包后的情况 (exe)
        base_path = sys._MEIPASS
        #注意!对于--onedir 模式,要用Path(sys.executable).parent, 对于 --onefile MEIPASS 保持一样.
    else:
        # 未打包的情况 (源码)
        base_path = os.path.abspath(".")
    return os.path.join(base_path, relative_path)

class MyWindow(QWidget):
    def __init__(self):
        super().__init__()
        layout = QVBoxLayout()

        # 假设我们有一个图片 logo.png, 和代码在同一个目录.
        image_path = resource_path("logo.png")

        # 加载并显示图片 (这只是一个例子,你需要根据实际情况加载)
        label = QLabel()
        # label.setPixmap(QPixmap(image_path))  # 假设是图片
        layout.addWidget(label)

        self.setLayout(layout)

app = QApplication(sys.argv)
window = MyWindow()
window.show()
sys.exit(app.exec())

4. 尝试其他的 Pyinstaller 指令与参数组合

  • 原理 : 有时调整指令可以解决奇怪的问题.
  • 方法 :
    • 使用 --onedir 而不是 --onefile: --onedir 将所有文件放在同一个目录,这样有时候依赖不容易出错, 方便调试(出问题的话,可以直接去对应文件夹检查文件)。 而--onefile将整个程序压缩到一个exe里面, 好处是不需要额外的文件夹来放置依赖文件,坏处是调试相对较麻烦。
    • 使用 --debug=all: 运行程序后打开控制台,方便查看详细的调试信息,查找出错的位置.
    • 不用--windowed 或者 --noconsole: 有些情况下,不使用图形界面或者显示控制台能够避免某些冲突.
#onedir and debug
pyinstaller --onedir --debug=all your_script.py

5. 操作系统权限(对于非windows平台很重要)

  • 原理: 打包后的程序可能需要更高的权限来访问系统资源,包括网络连接、文件访问等等。
  • 方法:
    • (Linux/macOS): 给与程序运行需要的相关权限. 例如:chmod
    chmod +x your_compiled_app # 使程序可执行.
    

6. 降级或升级相关库

  • 原理 : 不同版本的库之间可能存在细微差异. 排除由于不同版本的兼容性而导致打包问题的出现.
  • 方法
    • 尝试更低版本的 mysql-connector-python. 有时,较旧、但更稳定的版本能更好地与 PyInstaller 配合。
    • 尝试更新的 PyInstaller 版本: pip install --upgrade pyinstaller

三、 进阶技巧与调试

  1. 详细日志记录: 在代码中添加更详细的日志输出,特别是数据库连接和操作部分。 这样在打包后出现问题时,可以通过日志来追踪问题。

    import logging
    
    # 配置日志记录
    logging.basicConfig(level=logging.DEBUG,
                        format='%(asctime)s - %(levelname)s - %(message)s',
                        filename='app.log',  # 将日志写入文件
                        filemode='w') #覆盖
     #使用logger
     #logging.debug("message")
    
  2. 最小可复现示例: 尽量简化代码,创建一个最小的、能复现问题的示例。这样可以更快地定位问题所在,也更容易寻求帮助。 (你问题里提到的CRUD 例子就属于这种情况)。

  3. 使用 --clean: pyinstaller --clean your_script.py, 可以保证删除之前的编译产物,确保完全重新构建。

通过以上步骤的逐一排查和尝试,大部分由 PyInstaller 打包 PyQt6 和 MySQL 应用引起的崩溃问题都应该能够得到解决。 请根据实际情况灵活运用。