尧图建网站 尧图建网站 YAOTU WEB BUILD 免费咨询
ARTICLE DETAIL

资讯详情

深耕网站建设与建站编程的一线实战洞察。

Python Web 项目使用 PyInstaller 打包为 Windows EXE 的常见问题与解决方法

Python Web 项目使用 PyInstaller 打包为 Windows EXE 的常见问题与解决方法 一、问题背景在 Windows 环境下很多 Python Web 项目不仅包含 Python 后端代码还可能依赖前端静态资源、配置文件、数据目录以及额外的本地可执行程序。例如项目中可能通过 Python 调用一个由 Go、C/C 或 Rust 编译出来的命令行程序同时这个外部程序又依赖若干 DLL 动态链接库。在开发环境中直接运行 Python 代码时这些文件通常都位于项目根目录下因此程序可以正常启动。但是当使用 PyInstaller 将项目打包成 EXE 后如果没有显式配置资源收集规则就很容易出现如下问题ModuleNotFoundError: No module named uvicorn或者未检测到外部可执行文件或者ERROR: Error loading ASGI app. Could not import module backend.main.这些错误本质上并不完全相同需要分别处理。二、问题一缺少 uvicorn 依赖1. 错误现象打包完成后运行 EXE出现Traceback (most recent call last): File run.py, line 18, in module import uvicorn ModuleNotFoundError: No module named uvicorn2. 原因分析这说明当前 Python 环境中没有安装uvicorn或者requirements.txt中没有声明该依赖。对于 FastAPI 项目来说常见依赖通常包括fastapi pydantic PyYAML uvicorn[standard]其中uvicorn是 ASGI 服务器FastAPI 项目通常依赖它来启动 Web 服务。如果代码中存在importuvicorn但环境中没有安装uvicorn打包后的 EXE 自然也无法运行。3. 解决方法先安装依赖python-m pip install-r requirements.txt如果只是临时补装uvicorn也可以执行python-m pip installuvicorn[standard]然后验证是否安装成功python-cimport uvicorn; print(uvicorn.__file__)如果能够输出类似下面的路径说明依赖已经安装成功C:\Users\用户名\.conda\envs\环境名\lib\site-packages\uvicorn\__init__.py三、问题二外部可执行文件和 DLL 没有被打包进去1. 错误现象程序启动后提示未检测到外部可执行文件 请确认 xxx.exe 与相关 DLL 位于同一目录同时检查打包目录时发现dir.\dist\MyApp\_internal\dist-win64提示找不到路径或者目标目录不存在。2. 原因分析PyInstaller 默认主要分析 Python 代码依赖。对于.py文件中通过import引入的 Python 模块PyInstaller 通常可以自动识别。但是下面这些资源PyInstaller 通常不会自动收集外部 exe 文件 DLL 动态链接库 yaml/json 配置文件 前端静态资源目录 模板目录 运行时数据目录 输出目录例如项目中有如下目录结构project-root/ ├─ run.py ├─ backend/ ├─ frontend/ ├─ config.yaml ├─ dist-win64/ │ ├─ external-core.exe │ ├─ libxxx.dll │ └─ libyyy.dll ├─ data/ └─ outputs/如果直接使用python-m PyInstaller-D-n MyApp.\run.pyPyInstaller 会自动生成一个MyApp.spec文件但这个文件中通常是binaries[]datas[]这意味着外部 exe、DLL、配置文件、前端资源都没有被加入打包结果。因此运行 EXE 时Python 代码存在但运行所需的外部资源不存在程序就会报错。四、核心原则不要反复使用普通 PyInstaller 命令覆盖 spec 文件第一次执行python-m PyInstaller-D-n MyApp.\run.py会生成一个默认的MyApp.spec文件。但是如果后续已经手动修改了MyApp.spec就不要再执行这条命令。因为它可能重新生成并覆盖原来的 spec 文件导致之前添加的datas、binaries、hiddenimports等配置全部丢失。正确做法是python-m PyInstaller--clean--noconfirm.\MyApp.spec也就是说后续打包都应该基于修正后的 spec 文件而不是重新从run.py生成默认 spec。五、推荐的 spec 文件写法下面给出一个通用的MyApp.spec示例用于打包一个 FastAPI 项目同时包含外部可执行文件、DLL、配置文件、前端资源和数据目录。# -*- mode: python ; coding: utf-8 -*-importosfromPyInstaller.utils.hooksimportcollect_submodules block_cipherNoneROOTos.path.abspath(os.path.dirname(__file__))datas[]binaries[]hiddenimports[]# 收集后端动态导入模块hiddenimportscollect_submodules(backend)# 收集配置文件config_fileos.path.join(ROOT,config.yaml)ifos.path.exists(config_file):datas.append((config_file,.))# 收集前端静态资源frontend_diros.path.join(ROOT,frontend)ifos.path.isdir(frontend_dir):datas.append((frontend_dir,frontend))# 收集数据目录data_diros.path.join(ROOT,data)ifos.path.isdir(data_dir):datas.append((data_dir,data))# 收集输出目录outputs_diros.path.join(ROOT,outputs)ifos.path.isdir(outputs_dir):datas.append((outputs_dir,outputs))# 收集外部 exe 和 DLLnative_diros.path.join(ROOT,dist-win64)ifos.path.isdir(native_dir):binaries.append((os.path.join(native_dir,external-core.exe),dist-win64))fordll_namein[libxxx.dll,libyyy.dll,]:dll_pathos.path.join(native_dir,dll_name)ifos.path.exists(dll_path):binaries.append((dll_path,dist-win64))aAnalysis([run.py],pathex[ROOT],binariesbinaries,datasdatas,hiddenimportshiddenimports,hookspath[],hooksconfig{},runtime_hooks[],excludes[],noarchiveFalse,optimize0,)pyzPYZ(a.pure,a.zipped_data,cipherblock_cipher)exeEXE(pyz,a.scripts,[],exclude_binariesTrue,nameMyApp,debugFalse,bootloader_ignore_signalsFalse,stripFalse,upxTrue,consoleTrue,)collCOLLECT(exe,a.binaries,a.datas,stripFalse,upxTrue,upx_exclude[],nameMyApp,)需要注意里面的文件名要根据自己的项目实际情况修改。例如external-core.exelibxxx.dlllibyyy.dll应替换为项目实际使用的外部可执行文件和 DLL 名称。六、PyInstaller 6 的 _internal 目录问题PyInstaller 6 在onedir模式下打包后的目录通常类似dist/ └─ MyApp/ ├─ MyApp.exe └─ _internal/ ├─ backend/ ├─ frontend/ ├─ config.yaml ├─ dist-win64/ │ ├─ external-core.exe │ ├─ libxxx.dll │ └─ libyyy.dll └─ ...因此程序运行时不能简单假设资源位于dist/MyApp/很多资源实际位于dist/MyApp/_internal/所以代码中应该根据 PyInstaller 的运行环境动态获取资源根目录。七、推荐的资源路径获取方式可以在run.py或公共工具模块中写一个函数frompathlibimportPathimportsysdefresource_root()-Path:ifgetattr(sys,frozen,False):returnPath(getattr(sys,_MEIPASS,Path(sys.executable).parent))returnPath(__file__).resolve().parent这个函数的含义是开发环境中资源根目录是源码所在目录。打包环境中资源根目录是 PyInstaller 解包或收集资源的位置。在onedir模式下通常对应dist/MyApp/_internal之后查找资源时就可以写成ROOTresource_root()native_exeROOT/dist-win64/external-core.execonfig_fileROOT/config.yamlfrontend_dirROOT/frontend这样同一套代码可以同时兼容开发环境和打包环境。八、问题三ASGI app 动态导入失败1. 错误现象运行 EXE 后前面的初始化逻辑都正常但是 uvicorn 启动失败ERROR: Error loading ASGI app. Could not import module backend.main.2. 原因分析很多 FastAPI 项目会这样启动uvicorn.run(backend.main:app,host127.0.0.1,port8000)这在源码环境中通常没有问题。但是对 PyInstaller 来说backend.main:app是字符串形式的动态导入。PyInstaller 静态分析 Python 依赖时不一定能准确知道应该把backend.main及其子模块全部打包进去。因此打包后运行时uvicorn 再去根据字符串导入backend.main就可能失败。3. 解决方法一在 run.py 中显式导入 app推荐将启动方式改为importuvicornfrombackend.mainimportappdefmain():uvicorn.run(app,host127.0.0.1,port8000,reloadFalse,)if__name____main__:main()也就是说不再让 uvicorn 通过字符串导入backend.main:app而是在 Python 代码中显式导入frombackend.mainimportapp这样 PyInstaller 更容易识别依赖关系。4. 解决方法二在 spec 中添加 hiddenimports同时在MyApp.spec中加入fromPyInstaller.utils.hooksimportcollect_submodules hiddenimports[]hiddenimportscollect_submodules(backend)这样可以强制收集backend包下的所有子模块避免动态导入失败。九、run.py 的推荐写法下面是一个更适合 PyInstaller 打包的run.py示例frompathlibimportPathimportsysimportsubprocessimportuvicornfrombackend.mainimportappdefresource_root()-Path:ifgetattr(sys,frozen,False):returnPath(getattr(sys,_MEIPASS,Path(sys.executable).parent))returnPath(__file__).resolve().parentdefcheck_native_runtime()-None:rootresource_root()native_dirroot/dist-win64native_exenative_dir/external-core.exeprint(*72)print(Python Web Application)print(*72)print(f[路径] 运行资源目录{root})ifnotnative_exe.exists():print([异常] 未检测到外部可执行文件。)print(f[异常] 期望路径{native_exe})raiseFileNotFoundError(str(native_exe))print(f[正常] 已检测到外部可执行文件{native_exe})defmain()-None:check_native_runtime()print([启动] 服务地址http://127.0.0.1:8000)print([退出] 按 CtrlC 停止服务)print(*72)uvicorn.run(app,host127.0.0.1,port8000,reloadFalse,)if__name____main__:main()这里的关键点有三个。第一显式导入frombackend.mainimportapp第二不使用reloadTrue。打包后的 EXE 不适合使用热重载。第三不使用sys.executable-m uvicorn...因为在打包环境中sys.executable指向的是当前 EXE 本身而不是开发环境中的python.exe。如果继续使用sys.executable -m uvicorn可能导致模块查找异常甚至出现递归启动问题。十、完整打包流程1. 安装依赖python-m pip install-r requirements.txt或者至少安装python-m pip install fastapi pydantic PyYAMLuvicorn[standard]2. 验证 uvicornpython-cimport uvicorn; print(uvicorn.__file__)3. 使用 spec 文件打包不要使用python-m PyInstaller-D-n MyApp.\run.py应该使用python-m PyInstaller--clean--noconfirm.\MyApp.spec4. 检查外部资源是否被打包dir.\dist\MyApp\_internal\dist-win64正常情况下应该能看到external-core.exe libxxx.dll libyyy.dll5. 运行程序.\dist\MyApp\MyApp.exe6. 打开浏览器访问http://127.0.0.1:8000十一、常见错误与对应解决方案错误现象原因解决方法ModuleNotFoundError: No module named uvicorn当前环境未安装 uvicorn执行python -m pip install uvicorn[standard]_internal\dist-win64不存在spec 中没有配置 binaries/datas修改 spec将外部 exe 和 DLL 加入 binaries外部 exe 检测失败资源路径写死未兼容 PyInstaller使用sys._MEIPASS或Path(sys.executable).parent获取资源根目录Could not import module backend.mainuvicorn 字符串动态导入失败改为from backend.main import app并在 spec 中添加 hiddenimports修改 spec 后仍然无效又执行了普通 PyInstaller 命令覆盖 spec后续只使用python -m PyInstaller --clean --noconfirm .\MyApp.spec打包后热重载异常EXE 环境不适合reloadTrue设置reloadFalse十二、总结使用 PyInstaller 打包 Python Web 项目时不能只关注 Python 代码本身。一个完整可运行的 EXE 通常还需要同时处理Python 依赖是否安装完整FastAPI、Uvicorn 等动态导入模块是否被正确收集外部 exe、DLL、配置文件、前端静态资源是否被加入 spec打包后的资源路径是否兼容_internal目录启动方式是否适合 EXE 环境。核心原则是普通命令只适合生成初始 spec 文件正式打包应依赖手工维护后的 spec 文件。推荐流程是python-m pip install-r requirements.txt python-cimport uvicorn; print(uvicorn.__file__)python-m PyInstaller--clean--noconfirm.\MyApp.specdir.\dist\MyApp\_internal\dist-win64.\dist\MyApp\MyApp.exe只要依赖、资源收集、动态导入和运行路径这四个问题处理好FastAPI 项目即使包含外部本地可执行文件和 DLL也可以稳定打包成 Windows 可运行程序。
返回列表