python打包系列1 - pyinstaller打包遇坑笔记

本文记录了使用pyinstaller将Python程序打包成exe的全过程,包括环境配置、打包选项解析、隐藏导入模块处理及Windows DLL的加载问题。在打包过程中遇到的模块找不到、动态DLL加载异常等坑,通过修改spec文件和调整路径设置得以解决。

最近工作中需要将python打包成exe,我是用pyinstaller打包。这期间遇到了不少坑,最终打包成功,并在其他windows机器正常运行。

环境

我打包的python代码,依赖了opencv、numpy以及若干第三方库(MVS 相机接口库)。
本机环境:
win10 x64
python3.9
anconda虚拟环境
pycharm运行时版本: 17.0.4.1+7-b469.62 amd64
pyinstaller版本: 4.8

目标机器环境:
win10 x64

pyinstaller打包

pyinstaller比较重要的命令,-F,-D(默认方式,可不指定),-w

  1. -F 把所有依赖的dll都打包到了exe中(一个巨型exe文件),缺点是启动巨慢,特别是依赖了深度学习框架等多种包后
  2. -D 除了exe还会生成很多动态库,启动比-F方式要快很多,但是相比脚本执行,依然会慢很多
  3. -w 不弹出终端(即控制台界面,如果没有控制台界面,报错时会弹出错误信息对话框;有控制台就出现在控制台里)

其他命令

可选参数:
  -h,--help显示此帮助消息并退出
  -v,--version显示程序版本信息并退出。
  --distpath DIR放置捆绑的应用程序的位置(默认值:。\ dist)
  --workpath WORKPATH将所有临时工作文件,.log,.pyz放在哪里
                        等(默认值:。\ build)
  -y,--noconfirm替换输出目录(默认值:
                        SPECPATH \ dist \ SPECNAME)而不要求
                        确认
  --upx-dir UPX_DIR UPX实用程序的路径(默认值:搜索执行)
                        路径)
  -a,-ascii不包括unicode编码支持(默认值:
                        包括(如果有)
  --clean清理PyInstaller缓存并删除临时文件
                        在建造之前。
  --log-level LEVEL生成时控制台消息中的详细信息量。水平
                        可能是TRACE,DEBUG,INFO,WARN,ERROR,
                        严重(默认:INFO)。

产生什么:
  -D,--onedir创建一个包含可执行文件的单文件夹捆绑包
                        (默认)
  -F,--onefile创建一个文件捆绑的可执行文件。
  --specpath DIR文件夹,用于存储生成的规范文件(默认值:
                        当前目录)
  -n NAME,--name NAME分配给捆绑的应用程序和规范文件的名称
                        (默认值:第一个脚本的基本名称)

捆绑内容,搜索位置:
  --add-data <SRC; DEST或SRC:DEST>
                        要添加到的其他非二进制文件或文件夹
                        可执行文件。路径分隔符是平台
                        特定的`os.pathsep``(在Windows上是``;``
                        和``:``在大多数Unix系统上)。这个选项
                        可以多次使用。
  --add-binary <SRC; DEST或SRC:DEST>
                        要添加到可执行文件的其他二进制文件。
                        有关更多详细信息,请参见--add-data选项。这个
                        该选项可以多次使用。
  -p DIR,--paths DIR搜索导入的路径(例如使用PYTHONPATH)。
                        允许使用多个路径,以“;”分隔,或使用
                        此选项多次
  --hidden-import MODULENAME,-hiddenimport MODULENAME
                        命名在代码中不可见的导入
                        脚本。此选项可以多次使用。
  --additional-hooks-dir HOOKSPATH
                        搜索钩子的其他路径。这个选项
                        可以多次使用。
  --runtime-hook RUNTIME_HOOKS
                        定制运行时挂钩文件的路径。运行时挂钩是
                        与可执行文件捆绑在一起的代码是
                        在设置任何其他代码或模块之前执行
                        运行时环境的特殊功能。这个
                        该选项可以多次使用。
  --exclude-module排除
                        可选模块或软件包(Python名称,而不是
                        路径名称)将被忽略(好像不是)
                        找到)。此选项可以多次使用。
  --key KEY用于加密Python字节码的密钥。

如何产生:
  -d {all,imports,bootloader,noarchive},--debug {all,imports,bootloader,noarchive}
                        提供调试冻结的协助
                        应用。可以多次提供此参数
                        选择以下几个选项的时间。
                        
                        -全部:以下所有三个选项。
                        
                        -导入:为基础指定-v选项
                          Python解释器,导致其打印消息
                          每次模块初始化时,显示
                          来源(文件名或内置模块)
                          已加载。看到
                          https://docs.python.org/3/using/cmdline.html#id4。
                        
                        -自举程序:告诉自举程序发出进度
                          初始化并启动
                          捆绑的应用。用于诊断问题
                          缺少进口。
                        
                        -存档:而不是存储所有冻结的Python
                          源文件作为结果中的存档
                          可执行文件,将它们存储为文件
                          输出目录。
                        
  -s,--strip将符号表条应用于可执行文件并
                        共享库

执行打包

进入pycharm终端执行下列语句:
pyinstaller -D pyMain.py -w

注意:pyMain.py是程序的入库,-D 和 -w d都是可选的。吐槽一下,打包执行过程巨慢!!

执行后,会在项目中增加两个2文件夹和一个spec文件如下图:
在这里插入图片描述
重要的是xxx.spec文件和dist文件夹。

  • 生产的.exe文件(或者文件夹)在dist文件夹中
  • spec文件则是打包的描述文件,包含各种参数信息

pyinstaller打包库逻辑

  1. Pycharm开发环境(虚拟或实际的)自动打进包内

  2. py文件静态引用(import xxx或者 from xxx import xxx)自动打入包内,但必须在xx.spec文件的pathex属性中添加该路径。

    重点:在我的测试中,并不能自动导入模块(如下图),运行时报错显示找不到模块内的某个子模块(即:xxx.py),因此需要修改xx.spec文件,在其pathex属性中添加该路径,下面也会讲到。
    查网友的记录得知,pyinstaller版本升级后,打包逻辑会有改变。测试时,pyinstaller版本为4.8,也许是更高的版本可以自行导入,没有测试还不知晓,目前还是添加路径为妙。
    在这里插入图片描述
    在.spec文件中增加这三个文件的路径,如下图:
    在这里插入图片描述
    再次对.spec文件打包

     pyinstaller pyMain.spec
    

    之前报错被解决,软件界面正常显示。
    但是调用Windows DLL还是不正常,下面的测试会讲到解决方法,这里是pyinstaller的一个坑。如果,不算这个问题,其他都已经正常了。

  3. py文件动态引用的模块(python模块),则无法在打包时自动加载,必须在xx.spec文件,增加hiddenimports属性列表项

     import importlib
     m = importlib.import_module('origin')   # 动态引用
    

    解决方法:修改xx.spec文件,增加hiddenimports属性列表项,如下图
    在这里插入图片描述

  4. py文件动态引用的Windows DLL(打包时无需自动加载)
    可以不打包Windows DLL,也可以打包。我在测试中遇到了运行该DLL不正常的情况,于是做了如下尝试。
    项目为下图:
    在这里插入图片描述
    该DLL不正常
    推测1:找不到MvCameraControl.dll
    推测2:MvCameraControl.dll还需要调用其他DLL,而其他DLL没有包含在打包中

尝试1:写入绝对地址

... MvCameraControl_class.py
MvCamCtrldll = WinDLL("C:\Program Files (x86)\Common Files\MVS\Runtime\Win64_x64\MvCameraControl.dll")   
# 写入绝对地址,当然要保证目标电脑该地执行有此DLL
...

... xx.spec
a = Analysis(['BasicDemo.py'],
         pathex=[],
         binaries=[],
         datas=[],
         hiddenimports=[],
         hookspath=[],
         hooksconfig={},
         runtime_hooks=[],
         excludes=[],
         win_no_prefer_redirects=False,
         win_private_assemblies=False,
         cipher=block_cipher,
         noarchive=False)
...

测试结果:失败,无法打开程序界面

事发后发现,Windows DLL的绝对路径不是必须的,但是xx.spec文件的pathex属性不写入对应的模块地址是不行的。

尝试2:增加绝对路径,增加pathex属性

... MvCameraControl_class.py
MvCamCtrldll = WinDLL("C:\Program Files (x86)\Common Files\MVS\Runtime\Win64_x64\MvCameraControl.dll")   # 增加了路径
...

... xx.spec
a = Analysis(['BasicDemo.py'],
         pathex=[
         "./MvImport",        # 增加了 MvImport路径
         ],   
         binaries=[],
         datas=[],
         hiddenimports=[],
         hookspath=[],
         hooksconfig={},
         runtime_hooks=[],
         excludes=[],
         win_no_prefer_redirects=False,
         win_private_assemblies=False,
         cipher=block_cipher,
         noarchive=False)
...
注意:此时,MVImport文件夹内有MvCameraControl.dll(拷贝进来的)

测试结果:
本机测试成功,打开界面正常,运行枚举相机功能正常
其他电脑测试测试成功!运行枚举相机功能正常

尝试3:去除项目内的MvCameraControl.dll,其他不变
在尝试2的基础上,其他不变,删除MVImport文件夹内的MvCameraControl.dll,删除dist文件夹(防止干扰测试结果),再次编译

测试结果:
本机测试成功,打开界面正常,运行枚举相机功能正常
其他电脑测试测试成功!运行枚举相机功能正常

此时思考:我觉得很奇怪, 是这个 pathex=[“./MvImport”, ] 起作用了吗?

尝试4: 去除MvCameraControl.dll的绝对路径,其他不变
在尝试3的基础上,将MvCamCtrldll = WinDLL(“C:\Program Files (x86)\Common Files\MVS\Runtime\Win64_x64\MvCameraControl.dll”) 改为MvCamCtrldll = WinDLL(“MvCameraControl.dll”) ,删除dist文件夹(防止干扰测试结果),再次编译

测试结果:
本机测试成功,打开界面正常,运行枚举相机功能正常
其他电脑测试测试成功!运行枚举相机功能正常

此时思考:感觉有点蒙,这么设置在之前是不能运行的,太奇怪了。还有没搞清楚的地方?

尝试5:用自己的项目进行测试
用DynamicPositioningdebug项目做测试,设置方法与尝试4一样
在这里插入图片描述

在这里插入图片描述

删除dist文件夹,去除干扰项,执行下面的代码
pyinstaller -D DynamicPositioningdebug.spec

测试结果:
本机测试失败,软件闪退。

此时思考:思索设置方面并没有与尝试3有任何区别。那么,是否pyinstaller本身有什么缺陷?

测试6:重新生成xx.spec文件,再次打包

在不做任何修改的情况下,执行pyinstaller -D DynamicPositioningdebug.py 。
当然生成的exe肯定是执行不了的,等于就是重建了DynamicPositioningdebug.spec文件。
修改新的DynamicPositioningdebug.spec文件,和上面的修改是一样的。
执行pyinstaller -D DynamicPositioningdebug.spec,执行完毕后测试

测试结果:
本机测试成功了!!软件运行正常!其他电脑测试也成功了!

总结:

pyinstaller 指令实在太坑了,这个问题折磨了我好久。
反复修改xx.spec文件会对打包造成不可知的影响,即使配置xx.spec文件正确了,打包后依然可能执行不了。
解决办法只有重置xx.spec文件,并重新配置,再次打包。

知识小记:WinDLL函数与importlib.import_module函数的区别

  • WinDLL() 就是动态加载Windows DLL,dll的位置必须在可以发现的地方(比如:根目录,system32,环境变量-path指定的文件夹)。因此,WinDLL函数动态加载DLL是不需要列表(hiddenimports=[])进行标注。
  • importlib.import_module函数动态加载的是Python模块,需要在列表(hiddenimports=[])进行标注。
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值