PyInstaller 打包 exe 文件

时间:2025-03-27 09:07:21

PyInstaller 是一个第三方库,它能够在Windows、Linux、 Mac OS X 等操作系统下将 Python 源文件打包。通过对源文件打包, Python 程序可以在没有安装 Python 的环境中运行,也可以作为一个 独立文件方便传递和管理。
PyInstaller 支持 Python 2.7 和 Python 3.3+。可以在 Windows、Mac OS X 和 Linux 上使用,但是并不是跨平台的,而是说你要是希望打包成.exe 文件,需要在 Windows 系统上运行 PyInstaller 进行打包工作;打包成 mac app,需要在 Mac OS 上使用。

1. 安装

pip install pyinstaller

2. pyinstaller 打包命令的基本语法

pyinstaller [options] script[.py]
pyinstaller -F -w  -i  --workpath build路径 --distpath exe打包路径 -n exe名字

常用参数

命令	说明
-h,--help 		# 查看该模块的帮助信息
-F,--onefile 	# 将程序打包成一个单独的可执行文件,而不是一个包含多个文件的文件夹。这样可以减小文件体积,但会增加程序的启动时间。
-D,--onedir 	# 产生一个目录(包含多个文件)作为可执行程序
-a,--ascii 	# 不包含 Unicode 字符集支持
-d,--debug 	# 产生 debug 版本的可执行文件
-w,--windowed,--noconsolc 	# 指定程序运行时不显示命令行窗口(仅对 Windows 有效)
-c,--nowindowed,--console 	# 指定使用命令行窗口运行程序(仅对 Windows 有效),拖拽到cmd中可以看到调试信息
-o DIR,--out=DIR 	# 指定 spec 文件的生成目录。如果没有指定,则默认使用当前目录来生成 spec 文件
-p DIR,--path=DIR 	# 设置 Python 导入模块的路径(和设置 PYTHONPATH 环境变量的作用相似)。制定多个目录的时候可以指定多个-p参数来设置,让pyinstaller自己去找程序的资源;也可使用路径分隔符(Windows 使用分号,Linux 使用冒号)来分隔多个路径。
-n NAME,--name=NAME # 指定项目(产生的 spec)名字。如果省略该选项,那么第一个脚本的主文件名将作为 spec 的名字
--hidden-import # 指定库 --hidden-import 
--noconfirm 	# 执行过程中 自动确认
--compress		# 对打包后的文件进行压缩,以减小文件体积。但请注意,压缩可能会影响程序的加载速度。

常用打包命令总结

pyinstaller -F xxx.py 				# 打包一个exe
pyinstaller -F -w xxx.py			# 打包一个不带控制台的exe
pyinstaller -F -c xxx.py			# 打包一个带控制台的exe
pyinstaller -F -i xxx.ico xxx.py	# 打包一个指定图标的exe
pyinstaller -F --additional-hooks-dir .\hooks\ --noconfirm .\yuanchat.py
pyinstaller -c --additional-hooks-dir .\hooks\ --hidden-import passlib.handlers.bcrypt --noconfirm .\yuanchat.py

3. Pyinstaller Hook

hooks 的作用是在某个包被分析时,引入额外的依赖,或者将某些不需要引用的包排除在外。
一个 hook 文件实质上是一个 python 文件,拥有 python 的全部功能。hook 文件的命名规则为 ,其中 modulename 为你引入该模块的完整路径。
例如:在 python 应用的 lib 文件夹里有个 instrument 包,就要这样命名:。
然后运行 *pyinstaller --additional-hooks-dir .\hooks* ,在应用被分析时,就会应用这个 hook 文件。可以将 hooks 路径定义在任何地方

*PyInstaller .spec定义了一些全局变量

hiddenimports = ['module_name'] 	# 额外引入的模块,其中每个字符串元素为引入该模块的完整路径。可以是绝对或相对路径。
excludedimports = ['module_name']   # 排除某些模块的引入。只能是绝对路径。所有在该模块及其子模块中的该引用都将失效(不影响外部模块)。
datas = [ ('/usr/share/icons/education_*.png', 'icons') ] # 前者是文件所在路径,后者是在最后生成的文件夹中的名称。
binaries 							# 同上,引入所需的二进制文件。

4. spec 文件

参考:
/haujet/p/
/HaujetZhao/PyInstaller-Perfect-Build-Method

4.1. 生成 spec 文件

,它的本质是一个 python 文件,pyinstaller 有两种运行模式:

pyinstaller hello.spec 				# 会使用 spec 文件中的配置进行打包
pyinstaller hello.py <other args> 	# 根据命令行参数自动生成 spec 文件,再依据使用 spec 文件中的配置进行打包

pyinstaller 在打包时,实际上是在做了一些准备工作后,直接运行了 spec 文件里的 Python 代码。
相比于给命令行添加参数,直接编辑 spec 文件,在里面保存参数,更优雅,更方便操作。
除了直接打包脚,本文件自动生成 spec 配置,还可以通过执行 pyi-makespec 不打包,只生成 spec 配置

4.2. 解释 spec 文件

# -*- mode: python ; coding: utf-8 -*-

block_cipher = None

# 这一部分负责收集你的脚本需要的所有模块和文件。的;hiddenimports 参数可以指定一些 PyInstaller 无法自动检测到的模块。
a = Analysis(
    [''],       # 指定要打包的 Python 脚本的路径(可以是相对路径)
    pathex=[],          # 用来指定模块搜索路径
    binaries=[],        # 包含了动态链接库或共享对象文件,会在运行之后自动更新,加入依赖的二进制文件
    datas=[],           # 列表,用于指定需要包含的额外文件。每个元素都是一个元组:(文件的源路径, 在打包文件中的路径)
    hiddenimports=[],   # 用于指定一些 PyInstaller 无法自动检测到的模块
    hookspath=[],       # 指定查找 PyInstaller 钩子的路径
    hooksconfig={},     # 自定义 hook 配置,这是一个字典,一行注释写不下,此处先不讲
    runtime_hooks=[],   # 指定运行时 hook,本质是一个 Python 脚本,hook 会在你的脚本运行前运行,可用于准备环境
    excludes=[],        # 用于指定需要排除的模块
    win_no_prefer_redirects=False,
    win_private_assemblies=False,
    cipher=block_cipher,
    noarchive=False,
)
# 除此之外,a 还有一些没有列出的属性:
#  pure 是一个列表,包含了所有纯 Python 模块的信息,每个元素是一个元组,包含了:模块名, pyc路径, py 路径,这些模块会被打包到一个 .pyz 文件中。
#  scripts 是一个列表,包含了你的 Python 脚本的信息。每个元素是一个元组,其中包含了脚本的内部名,脚本的源路径,以及一些元数据。这些脚本会被打包到一个可执行文件中。
# pyz 是指生成的可执行文件的名称。它是由 PyInstaller 用来打包 Python 程序和依赖项的主要文件。

# 创建 pyz 文件,它在运行时会被解压缩到临时目录中,然后被加载和执行。它会被打包进 exe 文件
pyz = PYZ(a.pure, a.zipped_data, cipher=block_cipher)

# 创建 exe 文件
exe = EXE(
    pyz,            # 包含了所有纯 Python 模块
    a.scripts,      # 包含了主脚本及其依赖
    [],             # 所有需要打包到 exe 文件内的二进制文件
    exclude_binaries=True,  # 若为 True,所有的二进制文件将被排除在 exe 之外,转而被 COLLECT 函数收集
    name='hello',   # 生成的 exe 文件的名字。
    debug=False,    # 打包过程中是否打印调试信息?
    bootloader_ignore_signals=False,
    strip=False,    # 是否移除所有的符号信息,使打包出的 exe 文件更小
    upx=True,       # 是否用 upx 压缩 exe 文件
    console=True,   # 若为 True 则在控制台窗口中运行,否则作为后台进程运行
    disable_windowed_traceback=False,
    argv_emulation=False,
    target_arch=None,
    codesign_identity=None,
    entitlements_file=None,
)

# 这个对象包含了所有需要分发的文件
# 包括 EXE 函数创建的 exe 文件、所有的二进制文件、zip 文件(如果有的话)和数据文件
coll = COLLECT(
    exe,
    a.binaries,
    a.zipfiles,
    a.datas,
    strip=False,
    upx=True,
    upx_exclude=[],
    name='hello',   # 生成的文件夹的名字
)

5. PyInstaller 打包遇到的问题

参考:
/p/37764866
/zy1620454507/article/details/130620756
/qq_41185868/article/details/82913879

5.1. “ModuleNotFoundError * yapf_third_party”

参考:/only_coding/article/details/132103669
错误信息:

FileNotFoundError: [Errno 2] No such file or directory: 'C:\\Users\\XXX\\AppData\\Local\\Temp\\_MEI101202\\yapf_third_party\\_ylib2to3\\'

原因是 pyinstaller 没有自带该第三方库文件的 hook,就会导致这个包文件不被打包进来。
解决办法1
最简单的方法是打包完以后直接把这个文件按照原始路径复制进去。
比如在conda环境中找到 yapf_third_party, 如:C:\Users\\envs\python310\Lib\site-packages\yapf_third_party
复制到 *\dist\xxxxx_internal\ 目录中。
参考:/qq_41185868/article/details/82913879
解决办法2
写个 hook,然后放进 pyinstaller 的 hooks\ 文件夹里面(可以自己创建文件夹),hook 文件命名为: hook-yapf_third_party.py

from PyInstaller.utils.hooks import collect_data_files
datas = collect_data_files("yapf_third_party")
# 其他参考
# datas = collect_data_files("passlib")
# hiddenimports = collect_submodules('passlib')

5.2. ImportError *.json

使用 openpyxl 这个包,会发现在使用 PyInstaller 打包后,在运行时会出现以下错误:

ImportError: cannot import name __version__

打开这个 python 包所在的路径会发现,init.py 引入了 . 这个文件,而该文件中记载了包括 version 在内的一系列常数:

try:
    here = os.path.abspath(os.path.dirname(__file__))
    src_file = os.path.join(here, ".")
    with open(src_file) as src:
        constants = json.load(src)
        __author__ = constants['__author__']
        __author_email__ = constants["__author_email__"]
        __license__ = constants["__license__"]
        __maintainer_email__ = constants["__maintainer_email__"]
        __url__ = constants["__url__"]
        __version__ = constants["__version__"]
except IOError:
    # packaged
    pass

由于在 PyInstaller 打包时,不会将 json 文件打包进来,由于找不到这个文件,产生了 IOError,这样 version 就没有被成功定义,因而产生了上述的 ImportError。
解决方案:
里写:

datas = [('.', '.')]

这样就可以把这个文件打包进去。
等价于

from PyInstaller.utils.hooks import collect_data_files
datas = collect_data_files('openpyxl')