Python 包资源读取:模块能从 ZIP 导入,旁边的模板为什么不能当普通路径打开
开发目录里,按模块的 __file__ 拼出模板路径一直可用;把包放进 ZIP 后,模块仍能导入,模板却打不开。原因是导入系统能够理解归档内部位置,而普通文件系统接口未必理解。资源属于包,并不保证它已经是一份可直接打开的磁盘文件。
本文在 CPython 3.12.14 上执行,只在临时目录制作一个自包含的 ZIP 包,不安装依赖,也不读取现有项目。保存为 demo.py,再运行 python demo.py。特意选择标准 ZIP 导入器,避免把“开发机器上恰好是目录”误当成所有部署方式的共同条件。
让读取入口以包为锚点
files 返回支持遍历和读取的 Traversable 对象,调用 joinpath 找到包内模板,然后直接 read_text。它提供资源访问能力,但不是必须转换成 pathlib.Path 的占位符。只需要内容的调用方,拿到字符串或字节通常已够用,可以让加载器处理底层位置。
AI概念示意图:归档盒中的书页可以被直接读取,另一条临时桥将资源交给必须使用真实路径的工具。图片不是文件管理器截图。
import importlib
from importlib.resources import files, as_file
from pathlib import Path
import sys
import tempfile
import zipfile
name = "batch22_resource_demo"
with tempfile.TemporaryDirectory() as folder:
archive = Path(folder) / "bundle.zip"
with zipfile.ZipFile(archive, "w") as bundle:
bundle.writestr(name + "/__init__.py", "")
bundle.writestr(name + "/template.txt", "hello from archive")
sys.path.insert(0, str(archive))
try:
package = importlib.import_module(name)
ordinary = Path(package.__file__).with_name("template.txt")
assert not ordinary.exists()
print("ordinary path:", ordinary.exists())
resource = files(package).joinpath("template.txt")
assert resource.read_text(encoding="utf-8") == "hello from archive"
print("resource:", resource.read_text(encoding="utf-8"))
with as_file(resource) as extracted:
assert extracted.is_file()
assert extracted.read_text(encoding="utf-8") == "hello from archive"
assert not extracted.exists()
print("extracted removed:", not extracted.exists())
finally:
sys.path.remove(str(archive))
sys.modules.pop(name, None)包内位置与磁盘路径分别验收
前两行输出 ordinary path: False 和 resource: hello from archive。通过 __file__ 推出来的位置不是真实文件,但资源读取成功。测试同时检查这两个事实,才能证明代码确实走了归档资源入口,而不是某个同名开发目录抢先被导入。
本例把 ZIP 放到 sys.path 首位,并使用专门的包名;退出时恢复搜索路径并移除自己的模块缓存条目。实际项目应由正常打包和导入流程提供包,不必在业务函数里反复修改 sys.path。资源也必须进入发布产物,读取 API 不会替构建工具补上遗漏的文件。
只有需要文件路径时才展开
有些第三方库只接受路径,不能接受字节或文件对象。这时把资源交给 as_file,在 with 内取得实际 Path。对本例的 ZIP 成员,它会准备临时文件;代码在作用范围内读取成功,退出后断言文件已不存在,最后打印 extracted removed: True。
清理保证针对为资源访问创建的临时产物。如果包本来就位于普通目录,as_file 可能直接提供已有路径,不应期待它退出后删除源文件。也不要把临时 Path 返回给稍后执行的任务;应在作用范围内完成消费,或由调用方明确保存一份自己负责管理的副本。
如果库会延迟打开路径,函数返回不代表资源已经用完。例如先创建一个延迟加载对象,再立即退出 with,真正读取时仍可能失败。应查清消费者何时完成访问,把上下文覆盖到那个时刻;不能只因构造对象没有报错就认为文件可以清理。
部署验收要包含资源本身
建议为发布包保留两种小测试:直接从正常安装目录读取,以及从支持资源协议的归档布局读取,并核对内容。测试只能证明采用的加载器与布局,不能推广到所有自定义加载器。本文演示文件资源;目录展开能力与不同 Python 版本有关,需要另查所用版本的接口。
资源名也应来自明确的内部清单,不把外部输入未经约束地拼进查找路径。包资源接口解决的是定位与读取方式,不负责判断某个名字是否允许被访问。把包装入发布包、加载器能够读取、消费者已经用完三件事分别确认,部署故障才有明确的检查位置。


