Python打包时怎么包含图片、配置文件等资源文件
详细解析Python在两种主流打包场景(分发Wheel包与生成PyInstaller独立可执行程序)下如何正确包含图片、配置文件等非代码资源,以及运行期的标准读取方法与避坑技巧。
在 Python 开发中,包含图片、YAML/JSON 配置、图标、HTML 模板等非代码资源文件,核心在于明确目标打包场景:一种是将项目打包为供他人 pip install 的模块或分发库(Wheel/Sdist);另一种是将程序打包为无需 Python 环境即可独立运行的可执行程序(如 PyInstaller 生成的 .exe 或二进制单文件)。两种场景所依赖的构建配置机制与运行期路径解析完全不同,本文将分别介绍它们的标准处理方式与正确访问方法。
一、制作 Python 分发包(Wheel)时包含资源文件
当需要把项目发布给其他开发者安装使用时,推荐遵循现代化 PEP 621 标准,在 pyproject.toml 中统一声明构建元数据与数据文件规则。
1. 规范的项目目录布局
资源文件必须位于 Python 包目录内部(即包含 __init__.py 的文件夹内),而非与包平级的根目录下。如果资源处于包外部,标准构建工具默认不会将其归入轮子包内。
my_app/
├── pyproject.toml
├── README.md
└── my_app/
├── __init__.py
├── core.py
└── assets/
├── __init__.py # 建议保留,便于按子包定位
├── config.yaml # 配置文件
└── logo.png # 图片资源
2. 在 pyproject.toml 中声明包数据
以最通用的 setuptools 构建后端为例,配置 [tool.setuptools.package-data] 明确告诉构建系统需要打包哪些文件类型:
[build-system]
requires = ["setuptools>=61.0"]
build-backend = "setuptools.build_meta"
[project]
name = "my_app"
version = "0.1.0"
description = "包含静态资源示例应用"
readme = "README.md"
requires-python = ">=3.9"
[tool.setuptools.packages.find]
where = ["."]
# 关键配置:指定包内部需要额外打包的静态文件
[tool.setuptools.package-data]
"my_app.assets" = ["*.yaml", "*.png", "*.json"]
"my_app.assets" = ["*"] 包含该目录下的所有文件。若使用 hatchling 作为构建后端,可在 [tool.hatch.build.targets.wheel] 下配置 packages = ["my_app"],hatchling 默认会包含包目录下的所有非 Python 文件。
3. 正确读取已安装包中的资源文件
初学者容易写出类似 open("assets/config.yaml") 或根据 os.getcwd() 拼路径的代码。一旦其他人通过 pip 安装,并在任意工作目录下调用时,相对路径就会抛出 FileNotFoundError。
在 Python 3.9+ 中,推荐使用内置的 importlib.resources 模块进行资源访问:
from importlib import resources
import yaml
def load_app_config():
# 获取资源所在路径对象(Traversable 对象)
config_ref = resources.files("my_app.assets").joinpath("config.yaml")
# 直接以文本方式读取内容
content = config_ref.read_text(encoding="utf-8")
return yaml.safe_load(content)
def get_logo_bytes():
# 直接以二进制方式读取图片数据
image_ref = resources.files("my_app.assets").joinpath("logo.png")
return image_ref.read_bytes()
二、使用 PyInstaller 打包独立程序时包含资源
当使用 PyInstaller 将 Python 脚本打包为单文件可执行程序(--onefile)时,PyInstaller 只会自动解析 Python 源码中的 import 引用,不会主动捕获文本和图片资源,必须通过显式规则打包。
1. 命令行 --add-data 参数说明
--add-data 的参数格式为 源文件路径:目标子目录。需要特别注意操作系统的语法差异:
- Linux 与 macOS(POSIX 环境):使用半角冒号
:分隔。 - Windows(CMD / PowerShell 环境):使用分号
;分隔。
# Linux / macOS 终端执行命令:
pyinstaller --noconfirm --onedir --windowed \
--add-data "my_app/assets:my_app/assets" \
main.py
# Windows 终端执行命令(注意是分号):
pyinstaller --noconfirm --onedir --windowed ^
--add-data "my_app/assets;my_app/assets" ^
main.py
2. 在 .spec 文件中持久化配置
若配置项较多,推荐使用 .spec 规格文件管理。在 Analysis 对象的 datas 列表中增加元组定义:
# main.spec
a = Analysis(
['main.py'],
pathex=[],
binaries=[],
# datas 列表中每一项为 (本地物理源路径, 打包后的内部目标文件夹)
datas=[
('my_app/assets', 'my_app/assets')
],
hiddenimports=[],
...
)
3. PyInstaller 单文件运行原理与路径定位兼容函数
在单文件(--onefile)模式下,程序启动时,PyInstaller 会在操作系统的临时文件夹(如 /tmp 或 C:\Users\...\AppData\Local\Temp)下创建一个名为 _MEIxxxxxx 的临时文件夹,并将所有内嵌资源解压进去。此时:
sys._MEIPASS属性指向该临时解压目录。- 在未打包的纯 Python 开发环境下,
sys._MEIPASS并不存在。
因此,项目中的通用路径解析函数必须具备动态自适应能力:
import sys
from pathlib import Path
def get_resource_path(relative_path: str) -> Path:
"""
获取资源文件的绝对路径。
自动兼容:
1. PyInstaller 打包后的运行环境(解压在 sys._MEIPASS)
2. 本地开发调试阶段的 Python 环境
"""
if hasattr(sys, "_MEIPASS"):
# 打包后运行:指向临时解压目录
base_path = Path(sys._MEIPASS)
else:
# 开发模式:以当前源码根目录或文件所在目录为基准
base_path = Path(__file__).resolve().parent
return base_path / relative_path
# 业务使用示例:
config_path = get_resource_path("my_app/assets/config.yaml")
with open(config_path, "r", encoding="utf-8") as f:
print("成功读取配置:", f.read())
三、常见打包踩坑与排查指南
| 现象 / 报错 | 根本原因 | 对应解决方案 |
|---|---|---|
打包为 Wheel 后,用户 pip install 报 ModuleNotFoundError 或找不到静态资源 |
静态资源放在了顶层项目根目录下,或者 pyproject.toml 未包含对应的子路径规则。 |
将静态资源统一移入包目录(如 my_app/assets/),并在 package-data 中精准配置通配符。 |
Windows 下执行 PyInstaller 报 invalid syntax 或 add-data 语法错误 |
Windows 控制台使用了冒号 : 作为分隔符,而 Windows 盘符自带冒号导致冲突。 |
在 Windows 系统中将 --add-data 的分隔符改为分号 ;。 |
| PyInstaller 单文件程序运行时修改配置文件,程序重启后修改丢失 | 单文件模式每次运行解压在随机的临时目录 _MEIxxxx 中,程序退出后临时目录会被自动销毁。 |
打包的资源文件应严格视为只读资产。用户自定义可写配置应存放于系统的用户家目录(如 ~/.config/my_app/ 或 platformdirs 获取的路径)。 |
在子函数中直接调用 open("assets/logo.png") 偶发性报错 |
直接写相对路径依赖当前进程的当前工作目录(CWD),若用户从其他路径启动程序则无法定位。 | 始终基于 importlib.resources 或 get_resource_path() 生成绝对路径后再执行文件操作。 |
四、总结与最佳实践
处理 Python 资源文件打包时,牢记两条核心准则:
- 构建期明确定位:制作 Wheel 时,在
pyproject.toml的package-data中声明,并保证资源文件位于 Python 包体内部;制作独立可执行程序时,使用--add-data(注意不同系统的分隔符)将资源打入单文件内。 - 运行期规范读取:摒弃写死相对路径的习惯。库项目使用标准的
importlib.resources.files()访问;独立程序则使用基于sys._MEIPASS的自适应绝对路径解析函数,并将打包资源视为只读文件,确保程序在任何机器与工作目录下都能稳定运行。
免责声明:本文内容仅供参考,不代表本站观点。转载或引用请注明出处。









