pyinstaller打包技巧

简介

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

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

动态导入问题

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

pyinstaller打包技巧_第1张图片

从报错细节来看,它让你检查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。

pyinstaller打包技巧_第2张图片

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

pyinstaller打包技巧_第3张图片

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

pyinstaller打包技巧_第4张图片

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

pyinstaller打包技巧_第5张图片

经过多次实验,下面的版本关系可以让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,但真正运行业务逻辑时,会报错:

pyinstaller打包技巧_第6张图片

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

pyinstaller打包技巧_第7张图片

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:

pyinstaller打包技巧_第8张图片

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

怎么解决?

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

pyinstaller打包技巧_第9张图片

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

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

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

pyinstaller打包技巧_第10张图片

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

pyinstaller打包技巧_第11张图片

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

pyinstaller打包技巧_第12张图片

结尾

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

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

你可能感兴趣的:(python,java,mysql,大数据,人工智能)