使用Pyinstaller打包Python项目包含了大量的坑,这篇文章总结实践得到的Pyinstaller打包经验。本文的例子为Python3.6代码,Pyinstaller3.4,在windows下打包为64位和32位版本。
目录
Pyinstaller基本使用方法
Python项目的打包方法
1.spec文件生成
2.spec文件配置
3.使用spec执行打包命令
Python模块的打包问题
冻结打包路径
其它问题
Pyinstaller基本使用方法
Pyinstaller可以通过简单的命令进行python代码的打包工作,其基本的命令为:
pyinstaller -option xxx.py
options的详情可参考官方帮助文档https://pyinstaller.readthedocs.io/en/stable/usage.html
这边只介绍用到的option:-d生成一个文件目录包含可执行文件和相关动态链接库和资源文件等;-f仅生成一个可执行文件
-D, --onedir Create a one-folder bundle containing an executable (default)
-F, --onefile Create a one-file bundled executable.
对于打包结果较大的项目,选用-d生成目录相比单可执行文件的打包方式,执行速度更快,但包含更加多的文件。本文的例子选中-d方式打包。
Python项目的打包方法
以一个多文件和目录的Python项目为例,项目文件包含:1.Python源代码文件;2.图标资源文件;3.其它资源文件
以图中项目为例,Python源代码文件在多个目录下:bin, lib\app, lib\models, lib\views;图标资源文件在lib\icon目录下;其它资源文件在data目录下,包括文本文件,视频文件等等。
1.spec文件生成
为了进行自定义配置的打包,首先需要编写打包的配置文件.spec文件。当使用pyinstaller -d xxx.py时候会生成默认的xxx.spec文件进行默认的打包配置。通过配置spec脚本,并执行pyinstaller -d xxx.spec完成自定义的打包。
通过生成spec文件的命令,针对代码的主程序文件生成打包对应的spec文件
pyi-makespec -w xxx.py
打开生成的spec文件,修改其默认脚本,完成自定义打包需要的配置。spec文件是一个python脚本,其默认的结构如下例所示
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
# -*- mode: python -*-
block_cipher
=
None
a
=
Analysis([
'fastplot.py'
],
pathex
=
[
'D:\\install_test\\DAGUI-0.1\\bin'
],
binaries
=
[],
datas
=
[],
hiddenimports
=
[],
hookspath
=
[],
runtime_hooks
=
[],
excludes
=
[],
win_no_prefer_redirects
=
False
,
win_private_assemblies
=
False
,
cipher
=
block_cipher)
pyz
=
PYZ(a.pure, a.zipped_data,
cipher
=
block_cipher)
exe
=
EXE(pyz,
a.scripts,
exclude_binaries
=
True
,
name
=
'fastplot'
,
debug
=
False
,
strip
=
False
,
upx
=
True
,
console
=
False
)
coll
=
COLLECT(exe,
a.binaries,
a.zipfiles,
a.datas,
strip
=
False
,
upx
=
True
,
name
=
'fastplot'
)
spec文件中主要包含4个class: Analysis, PYZ, EXE和COLLECT.
Analysis以py文件为输入,它会分析py文件的依赖模块,并生成相应的信息
PYZ是一个.pyz的压缩包,包含程序运行需要的所有依赖
EXE根据上面两项生成
COLLECT生成其他部分的输出文件夹,COLLECT也可以没有
2.spec文件配置
首先给出举例python项目的spec文件配置
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
# -*- mode: python -*-
import
sys
import
os.path as osp
sys.setrecursionlimit(
5000
)
block_cipher
=
None
SETUP_DIR
=
'D:\\install_test\\FASTPLOT\\'
a
=
Analysis([
'fastplot.py'
,
'frozen_dir.py'
,
'D:\\install_test\\FASTPLOT\\lib\\app\\start.py'
,
'D:\\install_test\\FASTPLOT\\lib\\models\\analysis_model.py'
,
'D:\\install_test\\FASTPLOT\\lib\\models\\datafile_model.py'
,
'D:\\install_test\\FASTPLOT\\lib\\models\\data_model.py'
,
'D:\\install_test\\FASTPLOT\\lib\\models\\figure_model.py'
,
'D:\\install_test\\FASTPLOT\\lib\\models\\time_model.py'
,
'D:\\install_test\\FASTPLOT\\lib\\models\\mathematics_model.py'
,
'D:\\install_test\\FASTPLOT\\lib\\views\\constant.py'
,
'D:\\install_test\\FASTPLOT\\lib\\views\\custom_dialog.py'
,
'D:\\install_test\\FASTPLOT\\lib\\views\\data_dict_window.py'
,
'D:\\install_test\\FASTPLOT\\lib\\views\\data_process_window.py'
,
'D:\\install_test\\FASTPLOT\\lib\\views\\data_sift_window.py'
,
'D:\\install_test\\FASTPLOT\\lib\\views\\mathematics_window.py'
,
'D:\\install_test\\FASTPLOT\\lib\\views\\para_temp_window.py'
,
'D:\\install_test\\FASTPLOT\\lib\\views\\mainwindow.py'
,
'D:\\install_test\\FASTPLOT\\lib\\views\\paralist_window.py'
,
'D:\\install_test\\FASTPLOT\\lib\\views\\plot_window.py'
],
pathex
=
[
'D:\\install_test\\FASTPLOT'
],
binaries
=
[],
datas
=
[(SETUP_DIR
+
'lib\\icon'
,
'lib\\icon'
),(SETUP_DIR
+
'data'
,
'data'
)],
hiddenimports
=
[
'pandas'
,
'pandas._libs'
,
'pandas._libs.tslibs.np_datetime'
,
'pandas._libs.tslibs.timedeltas'
,
'pandas._libs.tslibs.nattype'
,
'pandas._libs.skiplist'
,
'scipy._lib'
,
'scipy._lib.messagestream'
],
hookspath
=
[],
runtime_hooks
=
[],
excludes
=
[],
win_no_prefer_redirects
=
False
,
win_private_assemblies
=
False
,
cipher
=
block_cipher)
pyz
=
PYZ(a.pure, a.zipped_data,
cipher
=
block_cipher)
exe
=
EXE(pyz,
a.scripts,
exclude_binaries
=
True
,
name
=
'fastplot'
,
debug
=
False
,
strip
=
False
,
upx
=
True
,
console
=
True
)
coll
=
COLLECT(exe,
a.binaries,
a.zipfiles,
a.datas,
strip
=
False
,
upx
=
True
,
name
=
'fastplot'
)
a) py文件打包配置
针对多目录多文件的python项目,打包时候需要将所有相关的py文件输入到Analysis类里。Analysis类中的pathex定义了打包的主目录,对于在此目录下的py文件可以只写文件名不写路径。如上的spec脚本,将所有项目中的py文件路径以列表形式写入Analysis,这里为了说明混合使用了绝对路径和相对路径。
b) 资源文件打包配置
资源文件包括打包的python项目使用的相关文件,如图标文件,文本文件等。对于此类资源文件的打包需要设置Analysis的datas,如例子所示datas接收元组:datas=[(SETUP_DIR+'lib\icon','lib\icon'),(SETUP_DIR+'data','data')]。元组的组成为(原项目中资源文件路径,打包后路径),例子中的(SETUP_DIR+'lib\icon','lib\icon')表示从D:\install_test\FASTPLOT\lib\icon下的图标文件打包后放入打包结果路径下的lib\icon目录。
c)Hidden import配置
pyinstaller在进行打包时,会解析打包的python文件,自动寻找py源文件的依赖模块。但是pyinstaller解析模块时可能会遗漏某些模块(not visible to the analysis phase),造成打包后执行程序时出现类似No Module named xxx。这时我们就需要在Analysis下hiddenimports中加入遗漏的模块,如例子中所示。
d)递归深度设置
在打包导入某些模块时,常会出现"RecursionError: maximum recursion depth exceeded"的错误,这可能是打包时出现了大量的递归超出了python预设的递归深度。因此需要在spec文件上添加递归深度的设置,设置一个足够大的值来保证打包的进行,即
1
2
import
sys
sys.setrecursionlimit(
5000
)
3.使用spec执行打包命令
pyinstaller -D xxx.spec
打包生成两个文件目录build和dist,build为临时文件目录完成打包后可以删除;dist中存放打包的结果,可执行文件和其它程序运行的关联文件都在这个目录下。
Python模块的打包问题
程序调用的很多包,在打包时候可能会出现一些问题,针对这写问题需要做一些处理才能保证打包的程序正常执行。
1.PyQt plugins缺失
使用PyQt编写UI交互界面的python代码在进行打包时可能会出现一些特别的问题。
执行使用了PyQt的打包程序,常会出现这样的错误,提示缺少Qt platfrom plugin “windows”,如下图
打包后程序运行后,使用png格式的图标可以正常显示,但使用的ico格式图标不显示(对于所有图标和关联文件都无法使用的情况涉及到路径问题,后文会另外解释)。
这两个错误产生的问题都是因为打包时没有将PyQt相关的动态链接库目录生成到打包目录下,因此可以通过将这些需要的文件目录拷贝到打包生成目录下,解决plugin缺失问题。以使用PyQt5编写的python软件打包为例,完成打包后的结果目录下包含PyQt5文件夹,将PyQt5\Qt\plugins下的所有内容(如下图)拷贝到打包结果目录。这样就可以解决PyQt plugins缺失的问题。
2.动态链接库缺失问题
更一般的,打包后可能会缺失某些动态链接库,造成执行程序出错,如
ImportError: DLL load failed: 找不到指定的模块
在打包过程中一般会有与此相关的warning提示(lib not found)无法找到这些动态链接库。例如在32位版本的打包中,可能会出现scipy模块相关的dll文件无法找到。这时就需要在打包的spec文件中指定动态链接库路径,使其关联到打包后的路径中。
binaries=[('C:\Program Files\Python36-32\Lib\site-packages\scipy\extra-dll','.')]
Analysis下的binaries是为打包文件添加二进制文件,缺失的动态链接库可以通过这种方式自动加入到打包路径中。
3.窗体风格变化问题
在某些情况下,如在精简环境下的python程序打包中,执行打包后的程序会出现窗体风格变为老式的win风格,这是由于打包时候PyQt的styles动态库没有找到。因此只需要在Python 目录下找到 Lib\site-packages\PyQt5\Qt\plugins\styles,将styles整个目录复制到打包结果目录。
冻结打包路径
执行打包后的程序,经常会出现程序使用的图标无法显示,程序使用的关联文件无法关联。或者,在打包的本机上运行正常,但是将打包后的程序放到其它机器上就有问题。这些现象都很有可能是由程序使用的文件路径发生改变产生的,因此在打包时候我们需要根据执行路径进行路径“冻结”。
1.使用绝对路径
在python代码中使用绝对路径调用外部文件可以保证打包时候路径可追溯,因此在本机上运行打包后程序基本没问题。但是当本机上对应路径的资源文件被改变,或者将打包程序应用到别的机器,都会出现搜索不到资源文件的问题。这种方式不是合适的打包发布python软件的方式。
2.使用冻结路径
增加一个py文件,例如叫frozen_dir.py
-- coding: utf-8 --
"""
Created on Sat Aug 25 22:41:09 2018
frozen dir
@author: yanhua
"""
import sys
import os
def app_path():
"""Returns the base application path."""
if hasattr(sys, 'frozen'):
Handles PyInstaller
return os.path.dirname(sys.executable)
return os.path.dirname(file)
其中的app_path()函数返回一个程序的执行路径,为了方便我们将此文件放在项目文件的根目录,通过这种方式建立了相对路径的关系。
源代码中使用路径时,以app_path()的返回值作为基准路径,其它路径都是其相对路径。以本文中使用的python项目打包为例,如下所示
import frozen_dir
SETUP_DIR = frozen_dir.app_path()
FONT_MSYH = matplotlib.font_manager.FontProperties(
fname = SETUP_DIR + r'\data\fonts\msyh.ttf',
size = 8)
DIR_HELP_DOC = SETUP_DIR + r'\data\docs'
DIR_HELP_VIDEO = SETUP_DIR + r'\data\videos'
通过冻结路径,使用了基准目录下的data目录下的fonts, docs, videos。
主程序中也做了类似的调整,改变其设置路径方法
import frozen_dir
SETUP_DIR = frozen_dir.app_path()+r'\lib'
sys.path.append(SETUP_DIR)
使用这样的方法进行打包,打包后的可执行程序就可以在其它机器上运行。
其它问题
由于操作系统和运行环境的不同,pyinstaller打包中还可能遇到很多其它问题,最后总结一些我在打包中遇到的其它坑:
1.权限问题
通常时在打包时出现的某些文件拒绝访问或没有权限执行某些操作等。解决这个的方法一般有这几个方面:
a)使用管理员权限运行cmd或其它命令行窗口
b)关闭杀毒软件
c)使用完全权限的管理员账户
2.中文路径
pyinstaller打包后的路径使用中文没有问题,不过为了减少打包时候出错的可能,尽量将打包使用的资源文件和代码文件路径设置为英文。
3.打包后文件的大小
通常python打包为可执行文件都会得到一个较大的包,这是无法避免的,但是我们还是可以通过一些方法来尽量精简打包后的执行程序:
a)在代码中减少不必要的import,如from xxx import *
b)在精简的运行环境(如原生python环境)下打包,缺什么包就下什么包,避免不必要的python包被打包入程序。尤其是anaconda这样的集成环境下打包的结果会大很多。
c)使用UPX
版权归原作者 xuexishiwochengz 所有, 如有侵权,请联系我们删除。