实用技巧!Pyinstaller打包技巧!

本文介绍使用PyInstaller将Python项目打包成exe的过程及遇到的问题解决方法,重点讲解如何处理opencv-python和moviepy等复杂库的打包难题。

9a32aa3f2c788be1268f5c76d3704dd9.png

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

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

动态导入问题

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

f504208ca809e9ecbe5f05993258e1e2.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。

cad30798a6abae392397fce00f50958c.png

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

b08f69f428858b23522271a0262bd830.png

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

edcdffa30f944f3ec33942c6b56ebf43.png

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

b37cba86405dcb16435fb1b47986eac2.png

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

pip uninstall pyinstaller-hooks-contrib
pip install pyinstaller-hooks-contrib==2021.3

pip uninstall pyinstaller
pip install pyinstaller==4.5.1


pip uninstall opencv-python
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文件,内容多会多一些,建议你直接从我这里复制出去用。

# -*- mode: python ; coding: utf-8 -*-

import json
import os
import sys

import PyInstaller.config

# 存放最终打包成app的相对路径
buildPath = 'build'
PyInstaller.config.CONF['distpath'] = buildPath

# 存放打包成app的中间文件的相对路径
cachePath = os.path.join(buildPath, 'cache')
if not os.path.exists(cachePath):
    os.makedirs(cachePath)
PyInstaller.config.CONF['workpath'] = cachePath

# icon相对路径
icoPath = os.path.join('logo.ico')

# 项目名称
appName = '无感视频同步助手'

# 版本号
version = '1.0.0'

# 对Python字节码加密
block_cipher = pyi_crypto.PyiBlockCipher(key='875650321356')


a = Analysis(['gui_main.py'],
            pathex=["venv\\Lib\\site-packages\\cv2"],
            binaries=[("venv\\Lib\\site-packages\\cv2\\opencv_videoio_ffmpeg453_64.dll", ".")],
            datas=[('gui\\frontend', 'gui\\frontend')],
            hiddenimports=[],
            hookspath=[],
            hooksconfig={},
            runtime_hooks=[],
            excludes=[],
            win_no_prefer_redirects=False,
            win_private_assemblies=False,
            cipher=block_cipher,
            noarchive=False)
pyz = PYZ(a.pure, a.zipped_data,
            cipher=block_cipher)


exe = EXE(pyz,
        a.scripts,
        a.binaries,
        a.zipfiles,
        a.datas,
        [],
        name=appName,
        debug=False,
        bootloader_ignore_signals=False,
        strip=False,
        upx=True,
        upx_exclude=[],
        runtime_tmpdir=None,
        console=False,
        disable_windowed_traceback=False,
        target_arch=None,
        codesign_identity=None,
        entitlements_file=None,
        icon=icoPath)

其中:

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

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

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

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

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

cb299443a2cfbd940fd52f22328bde52.png

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

50bfc5ca95065fabac64b0dafcdc1303.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:

7fcb8f48ca4eeb79050e6806eb160b8f.png

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

怎么解决?

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

ac1157d8e4647d728b2044dd338c3fb1.png

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

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

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

d1eac2fc27e1f0acf54f3238118f3c07.png

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

3f479095a8f3d05ee2a836459aada2b2.png

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

565fdb91244c3f030925b2e45463cdd6.png

结尾

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


 
推荐阅读:
入门: 最全的零基础学Python的问题  | 零基础学了8个月的Python  | 实战项目 |学Python就是这条捷径
干货:爬取豆瓣短评,电影《后来的我们》 | 38年NBA最佳球员分析 |   从万众期待到口碑扑街!唐探3令人失望  | 笑看新倚天屠龙记 | 灯谜答题王 |用Python做个海量小姐姐素描图 |碟中谍这么火,我用机器学习做个迷你推荐系统电影
趣味:弹球游戏  | 九宫格  | 漂亮的花 | 两百行Python《天天酷跑》游戏!
AI: 会做诗的机器人 | 给图片上色 | 预测收入 | 碟中谍这么火,我用机器学习做个迷你推荐系统电影
小工具: Pdf转Word,轻松搞定表格和水印! | 一键把html网页保存为pdf!|  再见PDF提取收费! | 用90行代码打造最强PDF转换器,word、PPT、excel、markdown、html一键转换 | 制作一款钉钉低价机票提示器! |60行代码做了一个语音壁纸切换器天天看小姐姐!|

年度爆款文案

点阅读原文,看B站我的视频!

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值