Pyinstaller打包完整python项目 使用虚拟环境的python项目的打包
Pyinstaller打包方式一般分为
第一种:直接输入指令
第二种:利用spec文件进行打包
由于直接输入指令实际就是根据指令生成spec文件,再根据spec文件的内容进行打包操作,所以重点说明spec文件的内容
一 Pyinstaller直接输入指令
1.安装pyinstaller
pip install pyinstaller
2.切换到工作目录
cd xxxxxxxxxxx
3.简单打包命令
可以用命令直接打包单文件,遇到项目文件很多时也可以用命令打包,但命令太长了过于繁杂,可以参考链接,
Pyinstaller可以通过简单的命令进行python代码的打包工作,其基本的命令为:
pyinstaller -option xxx.py
假如项目启动入口为xxx.py,首先需要以管理员身份启动cmd,然后cd到xxx.py所在的文件夹
当我们使用命令行直接打包,即 pyinstaller -D xxx.py 时 ,代码内部发生以下两件事:
1.生成默认的xxx.spec文件
2.直接根据默认的main.spec文件进行执行pyinstaller -D xxx.spec完成默认的打包
options常用参数(按需求选择):
- -D 与 -F 相反用法,生成一个文件目录包含可执行文件和相关动态链接库和资源文件等,对于打包结果较大的项目,选用-D生成目录相比-F的打包方式,执行速度更快,但包含更加多的文件
- -F 表示在 dist 文件夹下只生成单个可执行文件(内部包含所有依赖),不加默认会在 dist 生成一大堆依赖文件+可执行文件。
- -w 表示去掉控制台窗口,如果你的程序是有界面的,可以不写这个参数,
- -c 表示去掉窗框,使用控制台,推荐使用,会打印各种信息和log到控制台,加上这个参数生成的spec中的console=True
- -p 表示自己定义需要加载的类路径,项目中包含多个自建模块的时候需要加上 -p aaa.py -p bbb.py -p ccc.py
- -i 表示可执行文件的图标,后面跟图标的路径,可以自定义exe文件的图标,我尝试了好多次没成功
打包完毕后在 dist 文件夹下双击项目启动文件就可以执行了
二 Python利用spec文件进行打包完整项目的打包方法
当项目越来越大,引用的资源会越来越多时,那么使用pyinstaller进行打包,如果不利用spec文件,是很难满足打包需求的。
其实你在使用 pyinstaller -option main.py简单命令打包时 ,本质也是根据命令行中一系列的option参数,生成main.spec,然后自动执行pyinstaller main.spec完成打包
不过,如果你想把自己的资源文件一起打进包去,则需要对生成的spec文件进行一些手动编辑后,再手动执行 pyinstaller main.spec完成打包
参考:https://blog.csdn.net/weixin_42052836/article/details/82315118
以一个多文件和目录的Python项目为例,项目文件包含:1.Python源代码文件;2.图标资源文件;3.其它资源文件
以图中项目为例,Python源代码文件在多个目录下:bin, lib\app, lib\models, lib\views;图标资源文件在lib\icon目录下;其它资源文件在data目录下,包括文本文件,视频文件等等
1.生成 spec文件
为了实现自定义配置的打包:
第一步:需要生成启动文件默认的spec文件:(选择一个即可)
pyi-makespec -w xxx.py
pyi-makespec -c xxx.py # 生成的spec文件中console=True,即需要打印到控制台
第二步:打开生成的spec文件,根据自己的项目结构和需求,修改其默认脚本,完成自定义打包需要的配置。
spec文件是一个python脚本,其默认的结构如下:
# -*- 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根据上面两项生成,里面包含图标、版本(如果命令有会自动生成,命令没传也可以手动修改),可以设置生成的exe的名字
-
COLLECT生成其他部分的输出文件夹,COLLECT也可以没有,一般不需要更改,可以设置生成的项目文件夹名称
2.修改 spec文件
我们修改默认生成的spec文件,紫色为修改部分:
# -*- 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项目,如上的spec脚本,将所有项目中的py文件路径以列表形式写入Analysis,这里为了说明混合使用了绝对路径和相对路径。这里面的所有列表项都必须是py文件!(经过测试,其实只要启动入口只需要默认生成的main.py就好了,其他的py文件不管是独立的py文件还是在那个python包下都可以不要,因为入口文件中的模块加载A.py,A又加载B.py,这其中的导入关系是可以识别的)
- Analysis类中的pathex定义了打包的主目录,对于在此目录下的py文件可以只写文件名不写路径(默认生成的,不用管)
b) 打包 资源文件(其实就是复制资源到打包后文件夹对应位置)
- 资源文件包括打包的python项目使用的相关文件,如图标文件,文本文件等。
- 对于此类资源文件的打包需要设置Analysis的datas,将非py文件的路径与存放的文件夹名写在元组里,如:datas=[(SETUP_DIR+'lib\\icon','lib\\icon'),(SETUP_DIR+'data','data')]
- datas:
第一个参数:Python中的资源文件等非py类型文件的路径
第二个参数:打包后路径,要和路径中的文件夹名称相同
- 例子中的(SETUP_DIR+'lib\\icon','lib\\icon')表示将D:\\install_test\\FASTPLOT\\lib\\icon文件夹以及其中内容打包后还放在打包路径下的lib\\icon目录
- 假如项目某个文件夹打包时是空文件夹,但是运行中需要用到,比如log文件夹,即使将log地址写进datas中也不会生成,需要自己在代码里面用到某个文件夹去查看是否存在然后递归创建
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文件上添加递归深度的设置,设置一个足够大的值来保证打包的进行,即
import sys sys.setrecursionlimit(5000)
e)去除不必要的模块import
- 有时需要让pyinstaller不打包某些用不到的模块,可通过在excludes=[]中添加此模块实现,如
excludes=['zmq']
3.使用spec执行打包命令
pyinstaller -D xxx.spec
打包生成两个文件目录build和dist:
build为临时文件目录完成打包后可以删除;
dist中存放打包的结果,可执行文件和其它程序运行的关联文件都在这个目录下,dist文件夹里包含了整个项目所需的代码和环境,可在其他电脑中进行使用
三 pipenv管理的虚拟环境下python项目的打包
当项目不使用默认的pip管理,采用更高级的pipenv管理的时候,是额外的一个虚拟环境,各种依赖都在虚拟幻境里,就需要切换到虚拟环境中操作,话不多说先上图:
该项目采用最新版 python 3.8.0
第一步:管理员方式打开cmd,cd到项目根目录
cd C:\Users\13154\Desktop\taibaorpa
第二步:创建项目的虚拟环境,如果已经有了,跳过这一步:
pipenv install
pipenv会根据项目的pipfile自动安装其中记载的所有依赖
第三步:进入虚拟环境
pipenv shell
进入后,路径最前面会出现虚拟环境
第四步:查看依赖是否安装成功,也可以不查看
pipenv graph
第五步:安装pyinstaller
pip install pyinstaller
第六步:生成spec
pyi-makespec -D -c main.py # 生成的spec文件中console=True,即需要打印到控制台
第七步:修改spec
# -*- mode: python ; coding: utf-8 -*- import sys import os.path as osp sys.setrecursionlimit(5000) SETUP_DIR = 'C:\\Users\\13154\\Desktop\\taibaorpa\\' block_cipher = None a = Analysis(['main.py', SETUP_DIR+'asynctask.py', SETUP_DIR+'errors.py', SETUP_DIR+'file.py', SETUP_DIR+'msg.py', SETUP_DIR+'msg_pattern.py', SETUP_DIR+'pageon.py', SETUP_DIR+'report.py', SETUP_DIR+'script.py', SETUP_DIR+'settings.py', SETUP_DIR+'taibaorpa.py', SETUP_DIR+'taiboweb.py', SETUP_DIR+'webdriverhelper.py', SETUP_DIR+'wechat.py',], pathex=['C:\\Users\\13154\\Desktop\\taibaorpa'], binaries=[], datas=[(SETUP_DIR+'driver\\chrome','driver\\chrome'), (SETUP_DIR+'log\\screenshot_taiboweb','log\\screenshot_taiboweb'), (SETUP_DIR+'log\\screenshot_wechat','log\\screenshot_wechat'), (SETUP_DIR+'log\\wechat_files','log\\wechat_files'), (SETUP_DIR+'report','report'), (SETUP_DIR+'res\\img','res\\img'), (SETUP_DIR+'res\\vid','res\\vid'), (SETUP_DIR+'README.md','.')], hiddenimports=[], hookspath=[], 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, [], exclude_binaries=True, name='main', debug=False, bootloader_ignore_signals=False, strip=False, upx=True, console=True ) coll = COLLECT(exe, a.binaries, a.zipfiles, a.datas, strip=False, upx=True, upx_exclude=[], name='main')
第八步:根据spec打包
pyinstaller main.spec
然后打开dist/main文件夹,里面的main.exe就是执行文件,双击能正确启动则打包成功
四 pyinstaller给打包后的文件添加文件版本信息
五 Python模块的打包问题
这里总结了一些遇到的错误:pyinstaller打包项目报错总结
程序调用的很多包,在打包时候可能会出现一些问题,针对这写问题需要做一些处理才能保证打包的程序正常执行。
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整个目录复制到打包结果目录。
4.当打包时出现:UnicodeDecodeError: 'utf-8' codec can't decode byte 0xce in position 122
可在打包的命令行中输入chcp 65001设置命令行显示utf-8字符,然后再执行打包命令。或者,修改pyinstaller包下的compat.py,根据报错对应的行将
out = out.decode(encoding)
改为
out = out.decode(encoding, 'replace')
5.执行打包命令时报错 ImportError: No module named 'queue'
- 原因:尚不清楚
- 解决方法:如果该模块你用不到,可以在执行打包命令时用 --hidden-import 不打包进去。如果程序中需要该模块,在主文件最上面写上 improt queue
6.打包命令执行成功,但双击可执行程序弹出报错窗口failed to excute script xxx
- 原因:打包时内部缺少了某个依赖,这时需要看看控制台打印了什么报错信息,打包时加了 -w 参数的请再打包一次记得去掉 -w以便于查看log信息
- 现象:基本都是在控制台上发现报错 No module named 'xxxxx',如 No module named 'queue' 或者 ModuleNotFoundError: No module named 'PyQt5.sip'
- 解决方法1:同2,如果该模块你用不到,可以在执行打包命令时用 --hidden-import 不打包进去。如果程序中需要该模块,在主文件最上面写上 improt xxxxx。如 import queue 或 import PyQt5.sip
- 解决方法2:我用pipenv管理的项目,但是在真实环境下打包,打包好后运行出现这个,重新走 第二章 中流程,成功打包
7.冻结打包路径
执行打包后的程序,经常会出现程序使用的图标无法显示,程序使用的关联文件无法关联。或者,在打包的本机上运行正常,但是将打包后的程序放到其它机器上就有问题。这些现象都很有可能是由程序使用的文件路径发生改变产生的,因此在打包时候我们需要根据执行路径进行路径“冻结”。
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