• pyinstaller打包技巧


    简介

    当我们使用Python开发好程序需要打包成exe时,主流的做法便是使用pyinstaller,这玩意,看似简单,其实挺麻烦的,坑比较多,特别是涉及到比较复杂的库时,另外一个麻烦的事情是,打包失败后,搜索到的很多解决方案是没有效果的。

    前一段时间,我用Python开发了视频同步助手,也是用pyinstaller打包的,其中涉及到opencv-python、ffmpeg、moviepy等包,嗯,这个过程比较磨人,在我配合pyinstaller源码与其文档后,掌握了一些技巧,本文简单总结记录一下,希望对你有所帮助。

    动态导入问题

    如果你项目中使用了opencv-python库,简单利用pyinstaller打包,很容易出现打包成功了,却无法运行exe的情况,如下图:

    e761845bd8fdba247013853ea469fd75.png

    从报错细节来看,它让你检查OpenCV是否安装(Check OpenCV installation),但这其实不是报错原因,核心在这句:

    native_module = importlib.import_module("cv2")

    importlib库在业务型项目中是比较少使用的,其作用就是动态载入相应的库,而我们在日常的业务开发中,使用import关键字来实现库的载入。

    很多Python开源项目会使用importlib来实现插件系统,值得学习,但这里却因为importlib的原因,让pyinstaller打包失败。

    阅读pyinstaller文档中的【What PyInstaller Does and How It Does It】小节,可知,pyinstaller在打包时,会将项目的依赖也打包进来,但不包含下面几种情况:

    1. 实现了__import__()方法的类实例,在项目中使用时,无法被pyinstaller检测

    2. 通过importlib.import_module()方法导入的库,无法被pyinstaller检测

    3. 通过sys.path执行的逻辑,无法被pyinstaller检测

    嗯,pyinstaller存在这些局限,而很多知名的库却大量出现上面的三种情况,比如Django、opencv-python。

    怎么办?文档给出了4种解决方案:

    1. 通过pyinstaller命令行打包时,通过相应的配置参数,给出额外的信息

    2. 将项目修改成使用import关键字导入的形式

    3. 编写spec文件,给出额外信息,这与第1种方法相同,命令行上指定的参数,等价于spec配置文件中的配置

    4. 使用hook,实现动态替换

    首先排除方法2,因为这种方式只适用于你自己的项目,而Django、opencv-python这类第三方库,改不动,改动了也不好维护。

    然后排除方法1与方法3,对于简单情况,这两种方法是可以的,文本后面点也会介绍,但一些第三方库,动态导入的地方比较多,你通过写死配置的形式不太靠谱。

    嗯,剩下方法4了。

    什么是pyinstaller的hook?其实就是动态替换一些信息的一种方法。以opencv-python为例,开发者自己知道不同版本的opencv-python动态导入时,会导入什么地方的数据,通过hook的形式,在不改动opencv-python的基础上,动态映射成我们自己的导入方式。

    pyinstaller文档中给出了hook的开发细节,但不用急着动手,pyinstaller的社区已经将一些知名库的hook都开发好了,当你安装好pyinstaller时,相应的hook库其实也安装好了,叫pyinstaller-hooks-contrib。

    49b9d504671f33b76a713e20db0b62ef.png

    pyinstaller-hooks-contrib 是社区维护的pyinstaller hooks机制

    a36bf0274fdd9ee5f24e7f23c89c27af.png

    我们以opencv-python为例,找到opencv-python代码动态导入的位置,如下图:

    5ee6940b6300dd45e155b3f57eba89df.png

    当我们打包opencv-python时,需要注意opencv-python的版本,因为不同版本的opencv-python,需要hook的位置可能会改变,我们看到pyinstaller opencv-python相关的hook代码中的注释也可以看出其版本要求:

    caa2f50d136ed968d9b31d8ce08908e0.png

    经过多次实验,下面的版本关系可以让opencv-python成功打包。

    1. pip uninstall pyinstaller-hooks-contrib
    2. pip install pyinstaller-hooks-contrib==2021.3
    3. pip uninstall pyinstaller
    4. pip install pyinstaller==4.5.1
    5. pip uninstall opencv-python
    6. pip install opencv-python==4.5.4.58

    但,单纯的解决版本问题,还是无法很好的使用opencv-python,我们还需要将opencv-python的完整路径告诉pyinstaller,这需要使用方法1或方法3,我个人习惯使用方法3,即利用spec配置文件的形式来给pyinstaller更多额外信息。

    spec文件

    阅读pyinstaller文档中的【Using Spec Files】小节可知,spec文件会告诉pyinstaller打包时,如何处理被打包脚本,且spec文件实际上是可执行的python代码。

    从文档可知,spec文件主要有4个用途:

    1. 当你希望将数据文件与打包程序捆绑在一起时

    2. 当你希望包含运行时库时(DLL、SO等文件)

    3. 当你希望将Python run-time options添加到可执行文件时

    4. 当您想创建一个包含合并的公共模块的多程序包时

    用途3与用途4没有在实际项目中使用过,所以不讨论,我们主要来看看用途1与用途2。

    我们可以使用下面命令创建spec文件:

    pyi-makespec main.py

    下面是【无感视频同步助手】的spec文件,相比于创建出的默认spec文件,内容多会多一些,建议你直接从我这里复制出去用。

    1. # -*- mode: python ; coding: utf-8 -*-
    2. import json
    3. import os
    4. import sys
    5. import PyInstaller.config
    6. # 存放最终打包成app的相对路径
    7. buildPath = 'build'
    8. PyInstaller.config.CONF['distpath'] = buildPath
    9. # 存放打包成app的中间文件的相对路径
    10. cachePath = os.path.join(buildPath, 'cache')
    11. if not os.path.exists(cachePath):
    12.     os.makedirs(cachePath)
    13. PyInstaller.config.CONF['workpath'] = cachePath
    14. # icon相对路径
    15. icoPath = os.path.join('logo.ico')
    16. # 项目名称
    17. appName = '无感视频同步助手'
    18. # 版本号
    19. version = '1.0.0'
    20. # 对Python字节码加密
    21. block_cipher = pyi_crypto.PyiBlockCipher(key='875650321356')
    22. a = Analysis(['gui_main.py'],
    23.             pathex=["venv\\Lib\\site-packages\\cv2"],
    24.             binaries=[("venv\\Lib\\site-packages\\cv2\\opencv_videoio_ffmpeg453_64.dll"".")],
    25.             datas=[('gui\\frontend''gui\\frontend')],
    26.             hiddenimports=[],
    27.             hookspath=[],
    28.             hooksconfig={},
    29.             runtime_hooks=[],
    30.             excludes=[],
    31.             win_no_prefer_redirects=False,
    32.             win_private_assemblies=False,
    33.             cipher=block_cipher,
    34.             noarchive=False)
    35. pyz = PYZ(a.pure, a.zipped_data,
    36.             cipher=block_cipher)
    37. exe = EXE(pyz,
    38.         a.scripts,
    39.         a.binaries,
    40.         a.zipfiles,
    41.         a.datas,
    42.         [],
    43.         name=appName,
    44.         debug=False,
    45.         bootloader_ignore_signals=False,
    46.         strip=False,
    47.         upx=True,
    48.         upx_exclude=[],
    49.         runtime_tmpdir=None,
    50.         console=False,
    51.         disable_windowed_traceback=False,
    52.         target_arch=None,
    53.         codesign_identity=None,
    54.         entitlements_file=None,
    55.         icon=icoPath)

    其中:

    pathex=["venv\\Lib\\site-packages\\cv2"],

    便是将opencv-python完整项目的路径告诉pyinstaller,这样打包pyinstaller-python时,再配合上正确的pyinstaller与opencv-python版本,便可以打包出可正常打开的exe。

    知识点:第三方库代码相关的放在pathex字段中

    打包后的opencv-python无法处理视频

    一切似乎很ok,但真正运行业务逻辑时,会报错:

    b22be02b8417b744914de4cbaf1caba2.png

    经过加日志重打包后分析可知,它在下面位置报错:

    0a3362ac63e94be3d36c280188988a64.png

    opencv-python处理视频其实利用了ffmpeg.dll,而我们打包时,如果没有告诉pyinstaller ffmpeg.dll的位置,pyinstaller就不会将其打包进来,则会导致运行报错。

    所以,spec文件中需要下面的内容:

    binaries=[("venv\\Lib\\site-packages\\cv2\\opencv_videoio_ffmpeg453_64.dll"".")],

    知识点:dll、so这类动态库,要写在binaries字段中。

    静态资源打包

    【无感视频同步助手】使用了html、css来做布局,这些不是python代码,对python而言,类似于image、video之类的静态资源,这类静态资源,我们需要写到spec文件的datas字段中:

    datas=[('gui\\frontend''gui\\frontend')],

    打包moviepy

    搞定opencv-python后,你可以用类似的方法来搞moviepy这个库,毕竟moviepy也是基于ffmpeg来弄的,这不简单。

    嗯,不会灵活变通的话,可能会懵逼,因为moviepy有如下导入方式,且社区没有提供moviepy的hook:

    0ee477fc71fd2a4bb5776ddb2364ff85.png

    moviepy的作者偷懒,直接通过exec来批量导入需要的库,不可为不骚。

    怎么解决?

    使用方法2,没错,将其改成使用import关键字导入的形式,但不是改moviepy的代码。我们创建moviepy_import.py文件,将需要导入的库都写进去。

    51f6dbbd6e3554c73d59aa2182200451.png

    然后再项目入口py文件中,import moviepy_import,解决moviepy批量导入的骚写法。

    此外,moviepy打包还有另外一个问题,因为moviepy使用了imageio_ffmpeg这个库,而imageio_ffmpeg会使用ffmpeg,但我们打包时,没有将ffmpeg文件打包进去,moviepy在运行时便会报错。

    浏览imageio_ffmpeg目录,发现它自己会安装对应版本的ffmpeg。

    246f40bfb678340c70f0d80389fe3420.png

    找到moviepy报错位置,其实是imageio_ffmpeg库的_utils.py文件中的get_ffmpeg_exe()方法,如下图:

    906caf304b7f3c27888f7839d3a53cdc.png

    其实就是找不到ffmpeg而报错,我的解决方法是手动设置一下:

    96c58270b3688b4e4a1bd7eedef5f91f.png

    结尾

    嗯,目前我笔记里有记录的坑就上文中这些了,一个体会是,阅读源码和阅读文档的能力很重要,特别是资料比较少的情况。

    以上,我是二两,下篇文章见。

  • 相关阅读:
    M的编程备忘录之C++——类和对象(上)
    zabbix中文乱码解决方法
    利用福禄克DSX-5000 CH测试串扰
    大数据知识点之大数据5V特征
    聊聊ChatGLM中P-tuning v2的应用
    小商品公众号微信店铺搭建的作用是什么
    Unity——模拟AI视觉
    【AI视野·今日Robot 机器人论文速览 第八十四期】Thu, 7 Mar 2024
    goland的Markdown拖动插入链接编码有问题
    xxl-job架构原理
  • 原文地址:https://blog.csdn.net/weixin_30230009/article/details/126775254