PyInstaller是一个跨平台的Python应用打包工具,支持 Windows/Linux/MacOS三大主流平台,能够把 Python 脚本及其所在的 Python 解释器打包成可执行文件,从而允许最终用户在无需安装 Python 的情况下执行应用程序。
PyInstaller 制作出来的执行文件并不是跨平台的,如果需要为不同平台打包,就要在相应平台上运行PyInstaller进行打包。
PyInstaller打包的流程:读取编写好的Python项目–>分析其中条用的模块和库,并收集其文件副本(包括Python的解释器)–>将副本和Python项目文件(放在一个文件夹//封装在一个可执行文件)中。
pip install PyInstaller
PyInstaller包的安装可以在Anaconda环境下以conda install pyinstaller进行安装,在PyCharm中可以通过pip install pyinstaller进行安装。安装成功后就可以着手进行打包了。当然打包需要用到以下一些相关命令了。
常用到的命令为-F、-D、-i、-p、-w等,其中-i用于指定生成项目的图标,需要使用绝对路径。对于打包结果较大的项目,选用-d生成目录相比单可执行文件的打包方式,执行速度更快,但包含更加多的文件。本文的例子选中-D方式打包。
pyinstaller main.py
PyInstaller 最简单使用只需要指定作为程序入口的脚本文件。PyInstaller 执行打包程序后会在当前目录下创建下列文件和目录:
main.spec 文件,其前缀和脚本名相同,指定了打包时所需的各种参数;
build 子目录,其中存放打包过程中生成的临时文件。warnxxxx.txt文件记录了生成过程中的警告/错误信息。如果 PyInstaller 运行有问题,需要检查warnxxxx.txt文件来获取错误的详细内容。
xref-xxxx.html文件输出 PyInstaller 分析脚本得到的模块依赖关系图。
dist子目录,存放生成的最终文件。如果使用单文件模式将只有单个执行文件;如果使用目录模式的话,会有一个和脚本同名的子目录,其内才是真正的可执行文件以及附属文件。
单目录模式是 PyInstaller 将 Python 程序编译为同一个目录下的多个文件,其中 xxxx.exe 是程序入口点(xxxx 是脚本文件名称,可以通过命令行修改)。单目录模式是 PyInstaller 的默认模式,可以自己加上 -D 或者 --onedir 开关显式开启。
单目录模式打包生成的目录除可执行文件外,还包括 Python 解释器(PythonXX.dll)、系统运行库(ucrtbase.dll 以及其它 apixx.dll),以及一些编译后的 Python 模块(.pyd 文件)。
文件模式是将整个程序编译为单一的可执行文件。需要在命令行添加 -F 或者 --onefile 开关开启。
Python脚本是解释型程序,而不是 原生的编译程序,并不能产生出真正单一的可执行文件。如果使用单文件模式,PyInstaller打包生成的是自动解压程序,需要先把所有文件解压到一个临时目录(通常名为_MEIxxxx,xxxx是随机数字),再从临时目录加载解释器和附属文件。程序运行完毕后,如果一切正常,会将临时目录再删除。
PyInstaller会对运行时的Python解释器修改。如果直接运行 Python 脚本,那么sys.frozen 变量不存在,如果通过 PyInstaller 生成的可执行文件运行,PyInstaller 会设置sys.frozen 变量为 True;如果使用单文件模式,sys._MEIPASS 变量包含了PyInstaller 自动创建的临时目录名。
单文件模式因为有临时目录和解压文件过程,所以程序启动速度会比较慢。如果程序运行到一半崩溃,则临时目录将没有机会被删除。
PyInstaller在Windows/Linux/Mac环境下的使用:执行命令相同,只需要在不同环境下执行即可
# 1.执行命令
pyinstaller -F xxx.py
# 2.去生成的dist文件夹找xxx.exe运行
# 3.运行成功,xxx.exe则为可执行文件,删除其它文件
# 1.执行命令,start_agent.py为程序入口文件
pyinstall -D ×××.py
# 2.删除生成的bulid和dist文件夹,仅保留start_agent.spec文件
# 3.修改start_agent.spec文件,详见下
# 4.执行命令
pyinstaller -F ×××.spec
# 5.去dist文件夹下找start_agent文件
# 6.运行成功,删除临时文件目录build;dist目录为打包的结果,可执行文件和其它程序运行的关联文件都在这个目录下
# -*- mode: python ; coding: utf-8 -*-
import sys
sys.setrecursionlimit(5000)
block_cipher = None
# 以py文件为输入,分析py文件的依赖模块,并生成相应的信息
a = Analysis(['×××.py'],# 要打包.py文件名列表,和xxx.py同级可以不同添加
pathex=['../workspaces/...'], # 项目路径
binaries=[],# 程序调用外部pyd、dll文件(二进制文件路径)以数组形式传入;例:('D:\\pro\\text.dll', 'pro'),将'pdftotext.dll'pro,与原项目结构一致即可
datas=[],# 存放的资源文件(图片、文本等静态文件)以数组形成传入;例:('D:\\static\\c.ioc','static'),将'cc.ioc'打包之后放在static目录,与原项目结构一致即可
hiddenimports=[],# pyinstaller解析模块时可能会遗漏某些模块(not visible to the analysis phase),造成打包后执行程序时出现类似No Module named xxx;这时就需要在hiddenimports中加入遗漏的模块
hookspath=[],
runtime_hooks=[],
excludes=[],# 去除不必要的模块import,写在excludes中添加此模块
win_no_prefer_redirects=False,
win_private_assemblies=False,
cipher=block_cipher,
noarchive=False)
# .pyz的压缩包,包含程序运行需要的所有依赖
pyz = PYZ(a.pure, a.zipped_data,
cipher=block_cipher)
# 根据Analysis和PYZ生成单个exe程序所需要的属性及其配置
exe = EXE(pyz,
a.scripts,
[],
exclude_binaries=True,
name='×××',# 生成exe文件的名字
debug=False,# debug模式
bootloader_ignore_signals=False,
strip=False,
upx=True,
console=True ) # 是否在打开exe文件时打开cmd命令框
# 收集前三个部分的内容进行整合,生成程序所需要的依赖包,及资源文件和配置
coll = COLLECT(exe,
a.binaries,
a.zipfiles,
a.datas,
strip=False,
upx=True,
upx_exclude=[],
name='×××')
对配置文件进行修改如下:
# -*- mode: python ; coding: utf-8 -*-
import sys
sys.setrecursionlimit(5000)
block_cipher = None
# 以py文件为输入,分析py文件的依赖模块,并生成相应的信息
a = Analysis(['×××.py'],# 要打包.py文件名列表,和xxx.py同级可以不同添加
pathex=['../workspaces/...'], # 项目路径
binaries=[],# 程序调用外部pyd、dll文件(二进制文件路径)以数组形式传入;例:('D:\\pro\\text.dll', 'pro'),将'pdftotext.dll'pro,与原项目结构一致即可
datas=[],# 存放的资源文件(图片、文本等静态文件)以数组形成传入;例:('D:\\static\\c.ioc','static'),将'cc.ioc'打包之后放在static目录,与原项目结构一致即可
hiddenimports=[],# pyinstaller解析模块时可能会遗漏某些模块(not visible to the analysis phase),造成打包后执行程序时出现类似No Module named xxx;这时就需要在hiddenimports中加入遗漏的模块
hookspath=[],
runtime_hooks=[],
excludes=[],# 去除不必要的模块import,写在excludes中添加此模块
win_no_prefer_redirects=False,
win_private_assemblies=False,
cipher=block_cipher,
noarchive=False)
dict_database = Tree('../workspaces/.../common',prefix='common')
a.datas +=dict_database
dict_conf = Tree('../workspaces/.../conf',prefix='conf')
a.datas +=dict_conf
dict_logs = Tree('../workspaces/.../logs',prefix='logs')
a.datas +=dict_logs
# .pyz的压缩包,包含程序运行需要的所有依赖
pyz = PYZ(a.pure, a.zipped_data,
cipher=block_cipher)
# 根据Analysis和PYZ生成单个exe程序所需要的属性及其配置
exe = EXE(pyz,
a.scripts,
[],
exclude_binaries=True,
name='×××',# 生成exe文件的名字
debug=False,# debug模式
bootloader_ignore_signals=False,
strip=False,
upx=True,
console=True ) # 是否在打开exe文件时打开cmd命令框
# 收集前三个部分的内容进行整合,生成程序所需要的依赖包,及资源文件和配置
coll = COLLECT(exe,
a.binaries,
a.zipfiles,
a.datas,
strip=False,
upx=True,
upx_exclude=[],
name='×××')
spec文件中主要包含4个class: Analysis, PYZ, EXE和COLLECT.
中间部分添加了缺失的依赖
中间添加了依赖文件夹的绝对路径,是和这个路径相对应的
pathex=[’…/workspaces/...’],
这个路径是之前工具自己生成的,所以我们添加的两个目录的绝对路径也和它保持一致,这个修改完成后久可以了
1.windows尽量使用绝对路径,用双斜杠\ \
2.linux路径/home/my_project/web
3.路径避免使用中文
- 打包.spec文件报错:RecursionError: maximum recursion depth exceeded
```python
1.在spec文件上添加递归深度的设置
import sys
sys.setrecursionlimit(5000)
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 frozenPath
# 根目录路径
appPath = frozenPath.app_path()
background = QtGui.QPixmap(appPath+"/img/1.png")