文章目录
介绍
pyinstaller 是一个将python应用及其依赖库打包为可执行软件的三方库,在没有python开发环境的系统中仍可以直接运行。
- 支持Windows(win8+), macOS, and Linux,在windows下打包的可执行程序只能在windows下运行,是面向平台的打包;
- 安装 pip install -U pyinstaller , 或者指定某个版本
pyinstaller==6.15.0 - 打包原理
- 分析依赖,从py应用的入口程序开始 递归地分析 所有的导入依赖包;
- 收集依赖,将python解释器、依赖包、py应用项目代码等拷贝到一个目录中,生成一个压缩文件(如.exe);
- 创建引导程序,创建可执行程序的引导程序;
- 解压执行,在执行打包后的可执行程序时,默认将压缩文件解压到Temp目录下,并从入口开始执行
- 简单的打包
- 命令行进入到项目目录下,
pyinstaller main.py
- 命令行进入到项目目录下,
- 两种打包模式
- 单个文件,通过参数--onefile, -F指定,生成一个可执行文件,如windows下的exe,所有的项目脚本及依赖包都会打包到一个可执行文件中,启动执行会比单目录模式稍慢;执行该单文件exe时,会默认在Temp目录下解压出所有的依赖包、python解释器等(_MEIxxx唯一目录),并从引导程序开始执行;
- 单个目录,通过参数--onedir, -D指定,生成一个目录(包含所有的依赖)和一个可执行文件(包含项目文件);
- 适合debug调试问题
- 如果仅更改了项目脚本文件,只需重新发布exe程序即可,不用更新_internal目录;但是如果依赖也更新了,那么必须重新发布整个包(exe+_internal目录);
- exe中的启动引导程序bootloader创建一个临时的python环境,使用python解释器开始执行所有的python脚本,从_internal目录中找到所有的依赖包;
- 隐式导入问题,在python脚本中的隐式导入,pyinstaller无法感知,必须通过其他的参数来处理;
- import()
- importlib.import_module()
- 运行时操作sys.path
- 不会打包项目的py源码,而是打包编译后的pyc字节码,pyc原则上是可以反编译的,为了代码逻辑的安全性,最好使用Cython将py源码转为C代码,进而编译为机器码,更加安全;
命令行参数
pyinstaller main.py 打包时,可以指定一系列的命令行参数;
-
-h 帮助信息
-
- -distpath DIR,打包产物存储目录
-
--workpath 工作目录
-
--clean,构建之前清理缓存和临时文件;
-
--log-level LEVEL 构建时是日志级别;
-
-D, --onedir,单目录打包模式;
-
-F, --onefile 单文件打包模式;
--specpath spec文件存储的目录,默认在当前目录
-n, --name 打包应用的名称
--contents-directory 单目录模式的目录名;
-
--add-data source:dest, 将项目中的数据文件打包到根目录下的dest目录;例如.../resources:resources,将上级目录下的resources目录打包到根目录下的resources目录;./db.sqlite:. 将当前目录下的db文件打包到_internal或者_MEIxx目录下(根目录)
-
--add-binary source:dest, 添加二进制文件;
-
-p, --paths DIR,依赖包的搜索目录,等价于spec文件中的pathex
-
--hidden-import, --hiddenimport MODULENAME
-
--collect-submodules MODULENAME
Collect all submodules from the specified package or module. This option can be used multiple times.
-
--collect-data module_name, 从模块中搜索数据文件;
-
--collect-binaries module_name; 从模块中搜索二进制文件;
-
--collect-all module_name,从模块中搜索数据文件、二进制文件、子模块;
-
--copy-metadata packagename,复制包的元数据;
-
--recursive-copy-metadatapackagename
-
--additional-hooks-dir hookspath,搜索钩子的目录;
-
--runtime-hook hook ,运行时的钩子
-
--exclude-module module_name, 排除的模组
-
--splash IMAGE_FILE,启动画面的图片
-
-w 没有控制台窗口;-c 有控制台窗口;
案例:
bash
pyinstaller --noconfirm --log-level=WARN
-F -w
--add-data="README:."
--add-data="image1.png:img"
--add-binary="libfoo.so:lib"
--hidden-import=secret1
--hidden-import=secret2
--icon=..\MLNMFLCN.ICO
myscript.py
# 也可以在python脚本中运行
import PyInstaller.__main__
PyInstaller.__main__.run([
'my_script.py',
'--onefile',
'--windowed'
])
使用spec文件打包
第一次执行pyinstaller main.py时会在当前目录下生成一个.spec文件,编辑该文件后,可以直接使用.spec文件进行打包。
bash
from PyInstaller.utils.hooks import collect_all, collect_data_files
datas = [
('src/README.txt', '.')
]
binaries = =[ ( '/usr/lib/libiodbc.2.dylib', '.' ) ]
hiddenimports = ["pydantic.deprecated.decorator"]
sklearn_data = collect_all("sklearn")
datas += sklearn_data[0]
binaries += sklearn.data[1]
hiddenimports += sklearn.data[2]
a = Analysis(['main.py'],
pathex=['/xxdir'], # 模块的搜索目录
binaries=binaries, # 包含二进制文件
datas=datas , #包含数据文件
hiddenimports=hiddenimports, # 隐式导入
hookspath=None,
runtime_hooks=None,
excludes=None) # 排除的模块,不打包
pyz = PYZ(a.pure)
exe = EXE(pyz,... )
coll = COLLECT(...)
运行时信息
- 区分程序运行时是否为打包,sys.frozen
python
import sys
# 打包
if getattr(sys, 'frozen', False) and hasattr(sys, '_MEIPASS'):
# _MEIPASS 就是_internal根目录或者单文件的解压目录_MEIXXX
# sys.executable 可执行文件的目录
print('running in a PyInstaller bundle')
else:
print('running in a normal Python process')
# 模块的路径
# __file__, sys._MEIPASS + 'mypackage/mymodule.py'
# main.py脚本被打包在sys._MEIPASS根目录下,内部的__file__为sys._MEIPASS + main.py
# 所以打包后读取main.py同级目录下的数据库文件,如下
# db_path = os.path.join(sys._MEIPASS, "db.sqlite")
# 也可以在main.py脚本中通过__file__获取该模块的绝对路径,然后获取根目录,再拼接数据库文件
# --add-data="./db.sqlite:." 打包到sys._MEIPASS根目录中
# --add-data="../path/to/file.dat:." 打包到sys._MEIPASS根目录中
- sys.executable, 可执行文件路径
- 打包前是python解释器的路径;
- 打包后是exe可执行程序的路径;
项目打包案例
软件版本:
python 3.9
pyinstaller 6.16.0
pydantic 2.12.4
目录结构:

命令行切到main.py的同级目录下,执行pyinstaller -D main.py 会在同级目录下生成一个main.spec,编辑该main.spec:
bash
# -*- mode: python ; coding: utf-8 -*-
import sys
from PyInstaller.utils.hooks import collect_all, collect_data_files
sys.setrecursionlimit(5000)
block_cipher = None
# pyinstaller==6.16.0
# pydantic== 2.12.4 防止两者不兼容
pydantic_data = collect_all("pydantic")
datas = [("./users.db", "."), ("../resource", "./resource"), ("./static", "./static")]
binaries = []
hidden_imports = []
datas += pydantic_data[0]
binaries += pydantic_data[1]
hidden_imports += pydantic_data[2]
a = Analysis(
['main.py'],
pathex=["../"],
binaries=binaries,
datas=datas,
hiddenimports=hidden_imports,
hookspath=[],
hooksconfig={},
runtime_hooks=[],
excludes=["PyQt5"], # 如果python环境中既安装了PySide2,又安装了PyQt5,必须排除一个
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=False,
name='lauf_app', # 打包应用的名称
debug=False,
bootloader_ignore_signals=False,
strip=False,
upx=True,
console=True, # 启动时有控制台窗口
disable_windowed_traceback=False,
argv_emulation=False,
target_arch=None,
codesign_identity=None,
entitlements_file=None,
)
coll = COLLECT(
exe,
a.binaries,
a.zipfiles,
a.datas,
strip=False,
upx=True,
upx_exclude=[],
name='lauf_app',
)
常见问题:
- 打包pydantic兼容性报错问题,确保pyinstaller 的版本兼容pydantic的版本
- 如pyinstaller 6.16.0, pydantic 2.12.4
- python环境中同时安装PySide2, PyQt5的问题,必须排除一个,在spec文件中excludes=["PyQt5"]
- 在main.py目录下执行入口脚本,程序可以直接连接当前目录下的users.db;但是打包后的users.db在_internal目录中,exe就无法连接自己当前目录下的users.db,所以数据库连接的路径需要分情况设定:
python
# 固定的db文件
if hasattr(sys, "frozen") and hasattr(sys, "_MEIPASS"):
SQLALCHEMY_DATABASE_URI = f"sqlite:///{sys._MEIPASS}/users.db"
else:
SQLALCHEMY_DATABASE_URI = f"sqlite:///{BASE_DIR}/app/users.db"
engine = create_engine(SQLALCHEMY_DATABASE_URI, connect_args={
"check_same_thread": False
})
打包完成后,在dist目录下会生成一个main目录,其内部结构为:
