1.介绍
PyInstaller 是一个强大的 Python 打包工具,可以将 Python 程序打包成独立的可执行文件。以下会基于如何在win系统上将python程序打包为exe可执行程序为例,介绍安装方式、快速使用、注意事项以及特别用法。
2.安装方式
通过 pip 安装
PyInstaller 可以通过 pip 命令在线安装。这是最简单和推荐的安装方式。
pip install pyinstaller
这种方法适用于所有操作系统
3.快速开始
3.1 基本用法
打包一个 无任何调用及依赖的Python脚本 非常简单,只需指定作为程序入口的脚本文件即可。
pyinstaller myscript.py
这个命令执行会生成下图文件
- 当前目录下写入
myscript.spec
(与脚本名相同) - 在当前目录创建
build
目录,并写入一些日志文件和工作文件。 dist
如果不存在则在当前目录中创建。- 将可执行文件夹写入文件夹
myscript
中dist
,dist包含一个目录_internal
和一个文件myscript.exe
,_internal
目录中包含脚本所有的额外依赖包括python解释器、dll动态库等。
如何想打包成一个exe文件的,可指定参数 -F或者--onefile
,例如,
pyinstaller -F myscript.py
其中 -F
参数表示生成单个可执行文件。
参数(仅解释部分个人认为还算常用的参数)
参数 | 参数描述 |
---|---|
-F, –onefile | 打包一个单个文件,如果你的代码都写在一个.py文件的话,可以用这个,如果是多个.py文件就别用 |
-D, –onedir | 打包多个文件,在dist中生成很多依赖文件,适合以框架形式编写工具代码,我个人比较推荐这样,代码易于维护 |
-w,–windowed,–noconsole | 使用Windows子系统执行.当程序启动的时候不会打开命令行(只对Windows有效) |
-c,–nowindowed,–console | 在部署时包含 TCL/TK |
–icon=<FILE.ICO> | 将file.ico添加为可执行文件的资源**(只对Windows系统有效),改变程序的图标 pyinstaller -**i ico路径 xxxxx.py |
–icon=<FILE.EXE,N> | 将file.exe的第n个图标添加为可执行文件的资源**(只对Windows系统有效)** |
-n NAME, –name=NAME | 可选的项目**(产生的spec的)名字.如果省略,第一个脚本的主文件名将作为spec**的名字 |
3.2 高阶用法
既然是高阶用法,那便需要更深层次的理解,并能解决一些困难问题,例如多脚本调用、复杂依赖等问题。本章节首先从spec文件讲起,基于 spec文件会讲解如何通过修改spec文件以达到一些目的。
3.2.1 spec介绍
上一节提到在执行 pyinstaller myscript.py
后会生成 myscript.spec
文件,该文件实际上是决定整个打包过程的配置文件,因此对于pyinstaller的高阶用法将针对spec文件展开。
首先认识一下该文件的内容:
# -*- mode: python ; coding: utf-8 -*-
# 第一步:分析入口脚本,分析所有导入以及依赖。
# 分析完后a会产生4个变量:
# a.pure 依赖的纯 python 文件->("module", "D:\\XXX\XXX\module.py", "PYMODULE")
# a.scripts 依次执行的脚本文件->('hook', 'D:\\XXX\\hook.py', 'PYSOURCE')
# a.binaries 依赖的二进制文件->('python38.dll', 'D:\\XXX\\python38.dll', 'BINARY')
# a.datas 依赖的非二进制文件->('input.txt', 'D:\\XXX\\input.txt', 'DATA'
a = Analysis(
['myscript.py'], # 入口python脚本,即待分析的脚本入口
pathex=[], # 模块搜索的路径,默认当前环境变量
binaries=[], # 脚本所需的非python模块,例如DLL动态库,[ ( '/usr/lib/libiodbc.2.dylib', '.' ) ]
datas=[], # 脚本所需的非二进制文件,[ ( 'src/README.txt', '.' ), ( '/mygame/sfx/*.mp3', 'sfx' )]
hiddenimports=[], # 预先指定PyInstaller 无法自动检测到的模块
hookspath=[],
hooksconfig={},
runtime_hooks=[],
excludes=[], # 预先需要排除的模块,即不希望打包进来的模块
noarchive=False,
optimize=0,
)
# 创建包含python的主程序以及依赖项,该部分代码会被打包进exe文件,在exe运行时会解压到临时文件然后被调用。
pyz = PYZ(a.pure)
# 创建exe文件
exe = EXE(
pyz, # 包含了纯python代码
a.scripts, # 包含了data以及依赖项
[], # 包含需要打包到 exe 文件内的二进制文件
exclude_binaries=True, # 默认为True,所有的二进制文件将被排除在exe之外
name='myscript', # exe文件命名
debug=False, # 打包过程是否打印调试信息
bootloader_ignore_signals=False,
strip=False,
upx=True,
console=True, # 默认为True,在控制台窗口中运行,否则作为后台进程运行
disable_windowed_traceback=False,
argv_emulation=False,
target_arch=None,
codesign_identity=None,
entitlements_file=None,
)
# 组织收集exe的依赖
coll = COLLECT(
exe,
a.binaries,
a.datas,
strip=False,
upx=True,
upx_exclude=[],
name='myscript', # dist目录下的目录名称
)
然后已经存在该spec文件后,可以通过执行如下命令进行打包。
pyinstaller myscript.spec
3.2.2 适用场景
(1)打包的依赖库缺少文件、存在额外的数据要拷贝
# -*- mode: python ; coding: utf-8 -*-
########################>>重点在这里<<#####################################
# 加入 打包过程中遇到numpy的依赖问题
import os
from importlib.util import find_spec
# 空列表,用于准备要复制的数据
datas = []
# 存在依赖问题的模块
manual_modules = ['numpy', 'librosa']
for m in manual_modules:
if not find_spec(m):
raise Except(f"{m}模块未找到!")
datas.append((os.path.dirname(find_spec(m).origin, m)) # 以 (src, dst) 元组的形式添加到 datas 列表
# 额外复制的文件
my_files = ['/data/input.txt', ]
for file in my_files:
datas.append((file, '.')) # 将文件复制到打包目标路径的根目录
###########################################################################
a = Analysis(
['myscript.py'], # 入口python脚本,即待分析的脚本入口
pathex=[], # 模块搜索的路径,默认当前环境变量
binaries=[], # 脚本所需的非python模块,例如DLL动态库,[ ( '/usr/lib/libiodbc.2.dylib', '.' ) ]
datas=datas, # 脚本所需的非二进制文件,[ ( 'src/README.txt', '.' ), ( '/mygame/sfx/*.mp3', 'sfx' )]
hiddenimports=[], # 预先指定PyInstaller 无法自动检测到的模块
hookspath=[],
hooksconfig={},
runtime_hooks=[],
excludes=[], # 预先需要排除的模块,即不希望打包进来的模块
noarchive=False,
optimize=0,
)
......
(2)暴露打包后可能会修改的python脚本
可以利用a.datas的特点,将一些纯python脚本在打包时排除在exe之外,例如将核心代码 myscript.myscripyt_core.py
排除在exe外,以便后期修改代码。
其原理是将保存在 a.pure
中的纯python代码替换到 a.datas
中。
......
a = Analysis(
['myscript.py'], # 入口python脚本,即待分析的脚本入口
pathex=[], # 模块搜索的路径,默认当前环境变量
binaries=[], # 脚本所需的非python模块,例如DLL动态库,[ ( '/usr/lib/libiodbc.2.dylib', '.' ) ]
datas=[], # 脚本所需的非二进制文件,[ ( 'src/README.txt', '.' ), ( '/mygame/sfx/*.mp3', 'sfx' )]
hiddenimports=[], # 预先指定PyInstaller 无法自动检测到的模块
hookspath=[],
hooksconfig={},
runtime_hooks=[],
excludes=[], # 预先需要排除的模块,即不希望打包进来的模块
noarchive=False,
optimize=0,
)
######################>>重点在这里<<###########################
# 需要暴露在外的python(不带 .py)
my_modules = ['myscript.myscript_core"]
# 将被排除的模块添加到 a.datas,同时将module排除在pure_list
pure_list = []
for mod in a.pure:
if mod[0] in my_modules:
mod[-1] = "DATA"
a.datas.append(mod)
else:
pure_list.append(mod)
a.pure = pure_list
##############################################################
# 创建包含python的主程序以及依赖项,该部分代码会被打包进exe文件,在exe运行时会解压到临时文件然后被调用。
pyz = PYZ(a.pure)
......
4. 注意事项
在打包复杂的python工程或者项目时请注意:
- 务必指定入口脚本(不可使用-m 模块方式执行)。
- python脚本中尽量避免使用from XXX import *。
- 避免使用importlib来动态导入模块,避免使用。