PyInstaller 飞速入门

PyInstaller 把 python 项目打包成可执行文件供传播。

安装

PyInstaller 自己也是一个 pip 模块,使用 pip 去安装:

1
2
3
pip install pyinstaller

pyinstaller -v # 检查安装成功

使用

pyinstaller 只需要传给它项目的入口文件,

1
pyinstaller main.py

pyinstaller 自己会去解析代码中的依赖并打包进去。这里有一个很显然的地方——动态 import 的模块会识别不到。这个后面再提。

pyinstaller 默认会把可执行文件打到 dist 目录下,同时包含一个_internal目录包含依赖等,这是--onedir的行为。

使用--onefile参数打包的话,则只会包含一个可执行文件。

打包成目录虽然分发稍显困难且大小更大(压缩后大小和单文件一致),但启动会更快,打包成单文件的话分发方便,但启动会更慢

执行打包后的可执行文件时,工作目录将是可执行文件的所属目录

这个可执行文件可以直接在界面上双击打开或者通过控制台执行,此时可以接受参数。

打包只会涉及到代码依赖。对于代码中引用的静态资源?见下。

关于 —onefile 和 —onedir

两者本质是一样的,只是前者将所有依赖也打到 exe 中,后者把依赖仍旧独立放置;前者每次执行时,都会把所有依赖都解压到一个临时文件夹中再执行,这会带来一些延时。

sys._MEIPASS,该字段在执行打包后可执行文件时存在,表示依赖所处路径,这个路径对--onedir来说总是 exe 本路径下的_internal文件夹,对--onefile来说每次都会是一个新的临时文件夹。

关于 spec

每次执行 pyinstaller 时,都会根据当前配置去生成 spec 文件,该文件表示编译配置,后续不修改配置,重新编译时,只需要直接传入该 spec 文件给 pyinstaller 即可:

1
pyinstaller main.spec

FAQ

如何打包静态文件资源 Ver.1

这里分为两部分——标识静态文件资源,以及访问静态文件资源。

静态文件资源不会被识别出来,需要在编译时手动指定静态文件资源,同时指定它将要存放的位置,比如下面将项目中的asset/some.json资源文件放置到资源文件夹(即上面的sys._MEIPASS)下的asset目录下:

1
pyinstaller main.py --add-data 'asset/some.json:asset'

然后访问,在代码中,不能使用相对路径去访问静态文件资源了,因为此时行为在直接执行和打包后执行时变得不同。需要定义一个函数去处理此差异:

1
2
3
4
5
6
7
def resolve_resource_path(resource: str | Path):
if hasattr(sys, '_MEIPASS'):
# 打包环境
return Path(sys._MEIPASS) / resource
# 开发环境,以当前 py 文件(或者其他锚点)作为起始
# 如果是实际项目的话,这里应该是项目根路径之类的
return Path(__file__).parent / resource

DeepSeek 警告说,sys._MEIPASS 是 pyinstaller 的实现细节,依赖它是不优雅的。pyinstaller 同样重写了__file__,顶层模块的__file__将为_internal/xxx.pyc,使用该__file__也能够去获取资源文件。

如何导入动态加载的模块?

倘若我不加相关配置,下面的代码会报错:

1
2
from importlib import import_module
import_module('requests')

因为 pyinstaller 没有识别到 requests 依赖。

使用--hidden-import参数手动引入模块:

1
pyinstaller main.py --hidden-import requests

还有--collect-all这样的参数,这里不表,真出了问题再查。

exe 启动时会打开一个控制台,如何修改此行为?

直接在文件管理器中打开打包后的 exe,会同时打开一个控制台。

使用--noconsole参数避免此行为。

1
pyinstaller main.py --noconsole

关于日志,没有控制台的话 print 就无处可去了,应当使用 logging 模块去打印日志,虽然我还没学过。

未捕获的异常我希望弹窗提醒或者日志!

这个和 pyinstaller 无关了,需重写异常处理的 hook,直接贴上 GPT 的建议(需要配置 logging):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
import sys
import traceback
import logging
import tkinter as tk
from tkinter import messagebox

# 自定义异常处理函数
def handle_exception(exc_type, exc_value, exc_traceback):
# 忽略键盘中断
if issubclass(exc_type, KeyboardInterrupt):
sys.__excepthook__(exc_type, exc_value, exc_traceback)
return

# 格式化错误信息
error_message = "".join(traceback.format_exception(exc_type, exc_value, exc_traceback))

# 打到日志
logging.error("未捕获异常:\n%s", error_message)

# 弹窗提醒
try:
root = tk.Tk()
root.withdraw() # 隐藏主窗口
messagebox.showerror("程序崩溃", "程序发生了致命错误,详细信息请查看 error.log 日志文件。")
root.destroy()
except:
pass # 若在无图形环境运行,不弹窗

# 替换默认的未捕获异常处理
sys.excepthook = handle_exception

如何更改使用的 python 的版本?

做不到,pyinstaller 只会使用安装 pyinstaller 时的那个 python 环境,要做到这个,只能使用指定版本的 python 创建虚拟环境然后再安装 pyinstaller,这样便能够安装该版本的 pyinstaller。

或者,使用全局环境的话,使用指定版本的 python 的-m命令去执行 pyinstaller,如:

1
python3.10 -m PyInstaller ...

如何跨平台打包?

做不到,真想干建议上 docker 去进行编译。


本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 协议 ,转载请注明出处!