基于SAM2的眼动数据跟踪3——python转exe
目录
一、前言
二、如何将python转exe
问题:Python→exe 到底要写哪些东西?
问题:为什么onnxruntime_libs = [ (str(site_packages / 'onnxruntime/capi/libonnxruntime_providers_cuda.so'), 'onnxruntime/capi'), (str(site_packages / 'onnxruntime/capi/libonnxruntime_providers_shared.so'), 'onnxruntime/capi') ] 原来是.so,是不行的,改成.dll就行?跟系统有关系?
问题:hiddenimports里面到底要写啥?
问题:strip=False, upx=False, 这什么意思?
问题:为什么datas里面写了路径,后面还要跟一个名字?
问题:为什么打包后生成的是一个_internal文件夹和exe,_internal这个名称是什么时候确定的?(其实不重要)
问题:为什么Windows里需要在.spec中加sys.setrecursionlimit(5000) ?
问题:假设我现在有个python项目,我想把它转为exe,我该怎么做?
一、前言
我下载的X-AnyLabeling是3.1.1版本,如果你用最新版本,可能会遇到一些需要自己解决的问题,我把它放在了D:\zero_track\X-AnyLabeling-3.1.1,如果你跟我保持一致则你可以跟我采用完全一致的步骤方法进行,否则你需要根据你的项目路径进行更改。此外我使用的conda环境是(X_AnyLabeling_win_env),这个环境要基于SAM2的眼动数据跟踪1介绍的那样根据源码Readme去配置,这个环境位置我是放在D:\miniforge3\envs\X_AnyLabeling_win_env,如果你装的是conda而不是miniforge3或者是名字有所不同,则需要根据你的路径进行更改,下面x-anylabeling-linux-gpu.spec的修改内容中不再赘述这些。
建议是跟我保持一致,因为我之所以将X-AnyLabeling-3.1.1放在D:\zero_track是因为在zero_track项目中我打算对SAM算法各种改进项目以及各种目标跟踪算法进行一个性能比较和研究测试或者是与其他研究联动等等,而X-AnyLabeling-3.1.1只是其中一个。不过,这只是计划。
二、如何将python转exe
我们之前都是通过这样一句python语句打开X_AnyLabeling程序的:
(X_AnyLabeling_win_env) D:\zero_track\X-AnyLabeling-3.1.1> python anylabeling/app.py现在我们要将python转exe。在scripts/build_executable.sh中,可以看到有不同的系统选择,也可以选择cpu还是gpu。
#!/bin/bash system=$1 if [ $system = "win-cpu" ]; then echo "Building Windows CPU version..." export X_ANYLABELING_DEVICE=CPU pyinstaller --noconfirm x-anylabeling-win-cpu.spec elif [ $system = "win-gpu" ];then echo "Building Windows GPU version..." export X_ANYLABELING_DEVICE=GPU pyinstaller --noconfirm x-anylabeling-win-gpu.spec elif [ $system = "linux-cpu" ];then echo "Building Linux CPU version..." export X_ANYLABELING_DEVICE=CPU pyinstaller --noconfirm x-anylabeling-linux-cpu.spec elif [ $system = "linux-gpu" ];then echo "Building Linux GPU version..." export X_ANYLABELING_DEVICE=GPU pyinstaller --noconfirm x-anylabeling-linux-gpu.spec elif [ $system = "macos" ];then echo "Building macOS version..." export X_ANYLABELING_DEVICE=CPU pyinstaller --noconfirm x-anylabeling-macos.spec else echo "System value '$system' is not recognized." fi这里我们选择的是win-gpu,因为我们是在windows上进行python转exe的,同时我们需要的是gpu版本的,如果你要跑SAM2视频跟踪,不要想着cpu能跑,那将会慢到卡死。
在转exe之前,我们需要先确保依赖已经正确安装,如果是gpu版本:
(X_AnyLabeling_win_env) D:\zero_track\X-AnyLabeling-3.1.1> pip install -r requirements-gpu-dev.txt (如果是cpu版本,则是pip install -r requirements-dev.txt)然后打开 anylabeling/app_info.py,将__preferred_device__ = "CPU" 改为__preferred_device__ = "GPU"
然后我们只需要打开git bash 终端(这是windows系统下能用的linux终端),然后输入(先不要运行,看完下面)
bash scripts/build_executable.sh win-gpu如果选择win-gpu的话,实际上执行的是下面这句,所以说如果在windows系统下没有装git终端,则可以直接在cmd命令终端下使用这句(先不要运行,看完下面):
pyinstaller --noconfirm x-anylabeling-win-gpu.spec在此之前,我们需要改一个文件。
在x-anylabeling-win-gpu.spec中,
# -*- mode: python -*- # vim: ft=python import sys import sysconfig from pathlib import Path site_packages = Path(sysconfig.get_path("purelib")) # 找到当前虚拟环境的 site-packages onnxruntime_libs = [ # 准备两张 CUDA 动态库 (str(site_packages / 'onnxruntime/capi/libonnxruntime_providers_cuda.so'), 'onnxruntime/capi'), (str(site_packages / 'onnxruntime/capi/libonnxruntime_providers_shared.so'), 'onnxruntime/capi') ] a = Analysis( ['anylabeling/app.py'], # 入口脚本 pathex=['anylabeling'], # 额外模块搜索路径 binaries=[], # 无额外二进制 datas=[ # 需要拷贝的数据文件 ('anylabeling/configs/auto_labeling/*.yaml', 'anylabeling/configs/auto_labeling'), ('anylabeling/configs/*.yaml', 'anylabeling/configs'), ('anylabeling/views/labeling/widgets/auto_labeling/auto_labeling.ui', 'anylabeling/views/labeling/widgets/auto_labeling'), ('anylabeling/services/auto_labeling/configs/bert/*', 'anylabeling/services/auto_labeling/configs/bert'), ('anylabeling/services/auto_labeling/configs/clip/*', 'anylabeling/services/auto_labeling/configs/clip'), ('anylabeling/services/auto_labeling/configs/ppocr/*', 'anylabeling/services/auto_labeling/configs/ppocr'), ('anylabeling/services/auto_labeling/configs/ram/*', 'anylabeling/services/auto_labeling/configs/ram'), *onnxruntime_libs # 把上面两张 .so 也当数据拷 ], hiddenimports=[], # 无隐藏导入 hookspath=[], runtime_hooks=[], excludes=[], ) pyz = PYZ(a.pure, a.zipped_data) # 把纯 Python 代码压成 zlib exe = EXE( pyz, a.scripts, a.binaries, a.zipfiles, a.datas, # ← 所有东西一起打进一个文件 name='X-Anylabeling-Linux-GPU', debug=False, # 不输出调试 strip=False, upx=False, runtime_tmpdir=None, console=False, # 不弹控制台 icon='anylabeling/resources/images/icon.icns', ) # 包成 .app app = BUNDLE( exe, name='X-AnyLabeling.app', icon='anylabeling/resources/images/icon.icns', bundle_identifier=None, info_plist={'NSHighResolutionCapable': 'True'}, )上面的结果就是生成单文件 exe(Linux 下无后缀),双击即可运行,所有依赖都在 exe 内部。但是我觉得这样不好,因为我们有要修改的配置文件,全封装进exe不但运行的时候非常慢(每次运行exe要把依赖复制到tmp文件夹很耗时),而且什么都修改不了。
所以我们修改为下面的单文件夹模式,这样会在D:\zero_track\X-AnyLabeling-3.1.1\dist生成:X-AnyLabeling-GPU文件夹,里面是一个_internal文件夹和一个X-AnyLabeling-GPU.exe。可以看到我们下面把参数a.binaries, a.zipfiles, a.datas从EXE那里转移到了BUNDLE,这就是将一些东西从原本的单个exe抽出来放在_internal文件夹
# -*- mode: python -*- # vim: ft=python import sys import sysconfig from pathlib import Path sys.setrecursionlimit(5000) # required on Windows # site_packages = Path(sysconfig.get_path("purelib")) a = Analysis( ['anylabeling/app.py'], # [新增] pathex=['anylabeling', r'D:\zero_track\X-AnyLabeling-3.1.1\segment-anything-2-camera','D:/zero_track/X-AnyLabeling-3.1.1/xanylabeling_data'], binaries=[], datas=[ ('anylabeling/configs/auto_labeling/*.yaml', 'anylabeling/configs/auto_labeling'), ('anylabeling/configs/*.yaml', 'anylabeling/configs'), ('anylabeling/views/labeling/widgets/auto_labeling/auto_labeling.ui', 'anylabeling/views/labeling/widgets/auto_labeling'), ('anylabeling/services/auto_labeling/configs/bert/*', 'anylabeling/services/auto_labeling/configs/bert'), ('anylabeling/services/auto_labeling/configs/clip/*', 'anylabeling/services/auto_labeling/configs/clip'), ('anylabeling/services/auto_labeling/configs/ppocr/*', 'anylabeling/services/auto_labeling/configs/ppocr'), ('anylabeling/services/auto_labeling/configs/ram/*', 'anylabeling/services/auto_labeling/configs/ram'), # [新增] ('D:/miniforge3/envs/X_AnyLabeling_win_env/Lib/site-packages/onnxruntime/capi/onnxruntime_providers_cuda.dll', 'onnxruntime/capi'), ('D:/miniforge3/envs/X_AnyLabeling_win_env/Lib/site-packages/onnxruntime/capi/onnxruntime_providers_shared.dll', 'onnxruntime/capi'), # sam2配置文件地址 ('D:/zero_track/X-AnyLabeling-3.1.1/segment-anything-2-camera/sam2_configs', 'sam2_configs'), # 模型地址 ('D:/zero_track/X-AnyLabeling-3.1.1/xanylabeling_data/models/*', 'xanylabeling_data/models'), # 模型路径配置文件地址 ('D:/zero_track/X-AnyLabeling-3.1.1/xanylabeling_data/models.json', 'xanylabeling_data/models.json') ], # [新增] hiddenimports=[ 'sam2', 'sam2.build_sam', 'sam2.sam2_image_predictor', 'sam2_configs', ], hookspath=[], runtime_hooks=[], excludes=[], ) pyz = PYZ(a.pure, a.zipped_data) # [修改] exe = EXE( pyz, a.scripts, # a.binaries, # a.zipfiles, # a.datas, exclude_binaries=True, # 关键:不打包二进制,留给 COLLECT name='X-Anylabeling-Linux-GPU', debug=True, # 打开调试信息 strip=False, upx=False, # runtime_tmpdir=None, console=True, # 是否带控制台 icon='anylabeling/resources/images/icon.icns', ) # [修改] coll = BUNDLE( exe, a.binaries, a.zipfiles, a.datas, strip=False, upx=False, name='X-AnyLabeling.app', )问题:Python→exe 到底要写哪些东西?
PyInstaller 的核心工作只有 3 步:
Analysis
把入口脚本(app.py)里import到的所有模块、动态库、数据文件、隐藏导入……统统“抓”出来。
需要告诉它:
入口脚本是谁
额外二进制(
.dll/.so)在哪额外数据文件(
.yaml/.ui/.json)在哪哪些包是“动态 import”必须手工加(hiddenimports)
哪些包不要打进去(excludes)
EXE
把 Analysis 的结果变成“可执行文件”。
需要决定:
单文件模式(
one-file,exclude_binaries=False)还是单文件夹模式(one-folder,exclude_binaries=True)是否带控制台(
console=True/False)是否开调试信息(
debug=True/False)图标、名字、UPX 压缩等
COLLECT / BUNDLE
单文件夹模式下,再把“剩下的二进制 + 数据”收集到一个目录;macOS 下用 BUNDLE 生成.app包。写完这三段,保存成
.spec,然后pyinstaller --noconfirm xxx.spec
就出来 exe(或 app)。
问题:为什么onnxruntime_libs = [ (str(site_packages / 'onnxruntime/capi/libonnxruntime_providers_cuda.so'), 'onnxruntime/capi'), (str(site_packages / 'onnxruntime/capi/libonnxruntime_providers_shared.so'), 'onnxruntime/capi') ] 原来是.so,是不行的,改成.dll就行?跟系统有关系?
是的,
.so与.dll的区别纯粹是“操作系统约定”,跟 Python 本身无关:表格
复制
操作系统 动态库后缀 环境变量/搜索路径 Linux .soLD_LIBRARY_PATHWindows .dllPATHmacOS .dylibDYLD_LIBRARY_PATHONNX Runtime 的 GPU 版在安装时会根据当前平台复制同名不同后缀的文件:
Linux 下只有
libonnxruntime_providers_cuda.solibonnxruntime_providers_shared.soWindows 下只有
onnxruntime_providers_cuda.dllonnxruntime_providers_shared.dll因此
site_packages / 'onnxruntime/capi/libonnxruntime_providers_cuda.so'在 Windows 机器上根本找不到同名文件,PyInstaller 会报“文件不存在”或运行时报
Could not locate onnxruntime_providers_cuda.dll把后缀改成
.dll只是让路径字符串与真实文件一致,没有别的魔法。
跨平台写 spec 时可以用一个小技巧:import sys, sysconfig, os from pathlib import Path site_packages = Path(sysconfig.get_path("purelib")) ext = {'win32': '.dll', 'linux': '.so', 'darwin': '.dylib'}[sys.platform] onnxruntime_libs = [ (str(site_packages / f'onnxruntime/capi/onnxruntime_providers_cuda{ext}'), 'onnxruntime/capi'), (str(site_packages / f'onnxruntime/capi/onnxruntime_providers_shared{ext}'), 'onnxruntime/capi') ]这样同一份
.spec在 Linux/Windows/macOS 都能自动拿到正确文件。
问题:hiddenimports里面到底要写啥?
hiddenimports里只写**“PyInstaller 静态分析抓不到,但运行时一定会 import”的模块名。
一句话:“让 exe 在运行时不报 ModuleNotFoundError” 的最小集合。**一、先搞清为什么需要它
PyInstaller 靠文本扫描
import xxx语句来收集依赖,但遇到下面 4 种情况就瞎了:
动态
__import__()/importlib.import_module()插件式加载(字符串拼接名字)
二进制扩展(
.pyd/.so)被其它扩展调用运行时按条件才 import(
if flag: import y)这些模块不会出现在分析结果里,可执行文件里也就没打包进去,于是双击 exe 直接:
ModuleNotFoundError: No module named 'xxx'二、到底写什么?——“运行时一定出现”的顶级包名或完整子模块名
写法就是纯字符串,一行一个,不要路径、不要文件后缀、不要写
.py
场景
正确写法
错误写法
顶级包
'sam2'
'sam2.py'、'D:\\sam2'子模块
'sam2.build_sam'
'sam2/build_sam.py'包内动态 import
'mmcv.ops'
'mmcv/ops'扩展模块
'onnxruntime.capi._pybind_state'
'_pybind_state.pyd'三、快速定位该写谁
跑一遍 exe,看 traceback
缺谁就把谁名字加进去 → 最靠谱。全局搜索代码里的字符串拼接
例for name in ['sam2', 'yolov5', 'ppocr']: importlib.import_module(f'models.{name}')那么
models.sam2、models.yolov5、models.ppocr都要写。用社区钩子
很多库已有官方钩子(hook-xxx.py),装了钩子就不用手写;
没有钩子的库才手动补。四、常见真实例子
hiddenimports = [ # SAM2 动态 import 'sam2', 'sam2.build_sam', 'sam2.sam2_image_predictor', # 插件式 OCR 'ppocr.data', 'ppocr.modeling', # 二进制扩展 'onnxruntime.capi._pybind_state', # CUDA 后端 'onnxruntime.providers.cuda', # 第三方库延迟加载 'sklearn.utils._typedefs', 'sklearn.neighbors._partition_nodes', ]五、一句话总结
hiddenimports就是**“PyInstaller 漏掉但程序实际会 import 的模块名清单”**,
缺啥写啥,不要多写,不要写路径,写完重新打包 → 运行 → 再缺再加,直到 exe 不再报ModuleNotFoundError为止。
问题:strip=False, upx=False, 这什么意思?
这两行是PyInstaller 在打包阶段对二进制文件做的“瘦身”与“压缩”开关——
默认都是False(不动原文件),一旦设为True就可能让体积变小,但也可能把程序弄崩。
strip=False
只影响 Linux / macOS 下的
.so/.dylib
对应命令行工具strip:把动态库里的调试符号表、重定位信息等全部剃掉。体积能再小 5 %–30 %,但
– 崩溃时gdb/lldb看不到函数名,堆栈全是???;
– 极少数库(CUDA、PyTorch)被strip后会直接加载失败。Windows 的
.pyd/.dll不受该选项影响(PE 文件格式不同,没有strip概念)。
upx=False
UPX = “Ultimate Packer for eXecutables”,把可执行文件/动态库压成自解压格式。
体积可再小 20 %–60 %,但
– 启动时会先解压到内存,冷启动变慢;
– Windows Defender / 杀毒软件常把 UPX 压过的文件当成可疑加壳,容易误报;
– 部分库(opencv_world*.dll、onnxruntime_providers_cuda.dll、torch*.dll)被 UPX 后加载失败(内部自校验不通过)。
实战建议
场景 推荐值 理由 本地调试 都 False出问题能看完整符号,不会被压缩干扰 正式发布,且充分测试过 可 strip=TrueLinux/macOS 体积再小一圈 想发到全网下载 保持 upx=False杀毒误报、加载失败代价更高;真在乎体积用 7z 分发即可 一句话总结
strip=False=不剥符号,upx=False=不用 UPX 压缩,
两者都是“求稳”做法,体积换兼容性;
只要出现过“打包后 exe 打不开 / 杀毒报毒 / CUDA 初始化失败”,第一时间把这两个开关关掉再试。
问题:为什么datas里面写了路径,后面还要跟一个名字?
datas 里每一项是二元组:
('源路径', '打包后在 exe 内部的目录名')
第 1 个元素告诉 PyInstaller去哪里找文件;
第 2 个元素告诉 PyInstaller在生成的 bundle 里把文件放到哪个目录(也就是运行时
sys._MEIPASS下的相对路径)。例子 1:把单个文件放进去
datas=[ ('conf/settings.yaml', 'conf') ]打包后目录结构(运行时临时目录):
sys._MEIPASS/ ├─ conf/ │ └─ settings.yaml代码里就可以:
import os, sys yaml_path = os.path.join(sys._MEIPASS, 'conf', 'settings.yaml')例子 2:把整个文件夹映射进去
datas=[ ('models/*.pt', 'models') ]所有匹配的
.pt文件会被复制到:sys._MEIPASS/models/xxx.pt例子 3:换内部名字
datas=[ ('assets/icon.png', 'res/img') ]运行时:
sys._MEIPASS/res/img/icon.png与原始目录结构无关,内部路径完全由第 2 个字符串决定。
一句话记忆
第 1 段是“硬盘上去哪儿拿”
第 2 段是“拿进来以后放哪儿”
不写第 2 段,PyInstaller 就不知道在 bundle 里建什么目录,程序就找不到文件。
问题:为什么打包后生成的是一个_internal文件夹和exe,_internal这个名称是什么时候确定的?(其实不重要)
_internal这个名字不是.spec文件里写的,而是PyInstaller 在单文件夹模式(one-folder)下的硬编码默认子目录名。
只要满足下面两个条件,它就会自动出现:
打包时用了单文件夹模式(即
EXE(..., exclude_binaries=True)然后COLLECT(...)或BUNDLE(...)把二进制再收集回来);没有通过命令行
--name或COLLECT(name=...)显式指定目录名。出现时机
在
PyInstaller/building/api.py的COLLECT类里写死:# PyInstaller 5.x 源码摘录 class COLLECT(Target): def __init__(self, *args, **kws): ... # 如果没给 name,就用脚本名 + '_internal' if self.name is None: self.name = os.path.splitext(os.path.basename(self.scripts[0]))[0] + '_internal'因此当你用默认
.spec(或由pyinstaller your.py自动生成)时,PyInstaller 会:
生成
dist\your.exe;再生成
dist\your_internal\并把所有.dll/.pyd和数据塞进去;启动时
your.exe先把_internal解压到临时目录,再把该路径加入sys.path/sys.dllhandle,最后调真正的入口。想改名怎么办
命令行
pyinstaller your.spec --name MyApp目录会变成
dist\MyApp\和dist\MyApp_internal\。手动改
.spec
把COLLECT或BUNDLE的name参数写死coll = COLLECT(exe, a.binaries, a.zipfiles, a.datas, name='runtime') # 会生成 dist\runtime\一句话总结
_internal是 PyInstaller单文件夹模式默认的后缀目录名,源码里写死;
不出现它的办法只有:用单文件模式或者自己指定目录名。
问题:为什么Windows里需要在.spec中加sys.setrecursionlimit(5000) ?
在 Windows 下用 PyInstaller 打包超大项目(或深目录+长路径)时,经常会在
.spec开头看到:import sys sys.setrecursionlimit(5000)这不是 PyInstaller 的“官方要求”,而是规避 Python 默认递归深度限制的临时 workaround。
触发场景
PyInstaller 在Analysis 阶段会递归地:
扫描
import链解析
hook文件处理
datas/binaries路径如果项目很大、目录层级深(尤其放在
D:\very\long\path\to\project\...),Python 的路径解析+模块遍历就会深递归,最终抛出:RecursionError: maximum recursion depth exceeded
默认限制
CPython 的默认递归深度:
>>> sys.getrecursionlimit() 1000对普通代码够用,但对 PyInstaller 的“巨型 AST + 长路径”可能不够。
为什么 Windows 更常见
Windows 路径长度上限 260 字符,PyInstaller 内部会把长路径拆成多级拼接,递归层数比 Linux/macOS 更高。
很多 Windows 用户习惯把项目放在桌面/下载文件夹,路径本身就很深。
解决方式
在
.spec文件最顶部(任何函数调用前)把限制临时调高即可:import sys sys.setrecursionlimit(5000) # 或 10000,够用就行这只是运行打包脚本时生效,不会影响最终 exe 的运行性能,也不改变 Python 解释器本身。
官方态度
PyInstaller 文档把这条写进 FAQ:
If you encounter
RecursionErrorduring analysis, increase the limit in your.specfile.一句话总结
Windows 长路径 + 大项目 → PyInstaller 递归深度爆表 →.spec里手动sys.setrecursionlimit(5000)只是让打包过程不崩溃,对最终程序无任何副作用。
问题:假设我现在有个python项目,我想把它转为exe,我该怎么做?
下面给你一条**“从 0 到拿到可运行 exe”**的最短路线,分三步:
(先能跑,再优化;全程在虚拟环境里做,避免把系统 Python 搅乱)
准备阶段:在虚拟环境里安装依赖
# 1. 创建并激活虚拟环境(Python 3.8~3.11 最稳) python -m venv venv venv\Scripts\activate # Linux/Mac 用 source venv/bin/activate # 如果是conda环境 # 指定 Python 版本,环境名随意 conda create -n exepack python=3.10 -y conda activate exepack # 2. 安装你的项目 + PyInstaller pip install -r requirements.txt # 你的依赖 pip install pyinstaller
先“裸跑”PyInstaller,看能不能启动
进入项目入口脚本所在目录,假设入口是
myapp\main.py:# 最简单命令,生成单文件夹模式 pyinstaller myapp\main.py --name MyApp --clean -y解释:
--name MyApp让 exe 叫MyApp.exe,目录叫MyApp(而不是默认的main.exe/main_internal)。
--clean强制清空缓存。
-y不询问直接覆盖。跑完会出来:
dist\ ├─ MyApp\ │ ├─ MyApp.exe # 启动器 │ └─ …一堆 dll/pyd 文件双击
MyApp.exe
如果能正常打开 → 恭喜,直接跳到第 3 步微调。
如果闪退/报错 → 看弹出的控制台日志,缺啥补啥(见下方“常见补缺”)。
微调:写一份最小
.spec,把缺的东西加进去第 2 步只是“快速验证”。真正交付要写
.spec文件,否则每次命令行都得敲一长串。
让 PyInstaller 先生成模板:
pyi-makespec myapp\main.py --name MyApp --onefile得到
MyApp.spec。
用文本编辑器打开
MyApp.spec,按需要补 3 处:# ① 数据文件(ui、yaml、图标等) datas=[ ('myapp/config/*.yaml', 'myapp/config'), ('myapp/resources/icon.png', 'myapp/resources'), ], # ② 隐藏导入(运行时报 ModuleNotFoundError 就加) hiddenimports=[ 'sklearn.utils._typedefs', 'onnxruntime.capi._pybind_state', ], # ③ 单文件还是单文件夹(示例保持单文件) exe = EXE( ... console=False, # 正式发布用 False;调试阶段可 True icon='myapp/resources/icon.ico', )
重新打包:
pyinstaller MyApp.spec --clean -y产物:
dist\MyApp.exe # 单文件,双击即可运行常见补缺速查表
报错现象 解决手段 ModuleNotFoundError: xxx把 xxx加到hiddenimportsFileNotFoundError: config\a.yaml把该 yaml 加到 datas;代码里用sys._MEIPASS拼接路径(见下)双击无窗口闪退 先 --console=True看日志;解决后再改回False杀毒软件报毒 关掉 UPX(默认就关),用数字签名工具签一下 exe读取打包后资源的标准代码
import sys, os def resource_path(rel): """获取打包后资源绝对路径""" base = getattr(sys, '_MEIPASS', os.path.abspath('.')) return os.path.join(base, rel) config = resource_path('myapp/config/settings.yaml')一句话总结
venv或者conda + 装依赖
先裸跑
pyinstaller main.py --name MyApp验证能否启动用
pyi-makespec生成.spec,把数据/隐藏导入加进去,再pyinstaller MyApp.spec出最终 exe按这个流程,10 分钟就能拿到可运行的 exe;后续再慢慢优化体积、图标、自动更新等高级需求。
