如何从 Python 包中读取(静态)文件?

Posted

技术标签:

【中文标题】如何从 Python 包中读取(静态)文件?【英文标题】:How to read a (static) file from inside a Python package? 【发布时间】:2011-08-27 01:06:48 【问题描述】:

您能告诉我如何读取 Python 包中的文件吗?

我的情况

我加载的包有许多我想从程序中加载的模板(用作字符串的文本文件)。但是如何指定此类文件的路径?

假设我想从以下位置读取文件:

package\templates\temp_file

某种路径操纵?包基本路径跟踪?

【问题讨论】:

相关:MANIFEST.in ignored on “python setup.py install” - no data files installed? 【参考方案1】:

包装前奏:

在您甚至可以担心读取资源文件之前,第一步是首先确保将数据文件打包到您的发行版中 - 直接从源代码树中读取它们很容易,但重要的是部分是确保可以从已安装包中的代码访问这些资源文件。

像这样构建您的项目,将数据文件放入包中的子目录

.
├── package
│   ├── __init__.py
│   ├── templates
│   │   └── temp_file
│   ├── mymodule1.py
│   └── mymodule2.py
├── README.rst
├── MANIFEST.in
└── setup.py

您应该在setup() 调用中传递include_package_data=True。仅当您想使用 setuptools/distutils 并构建源代码分发时才需要清单文件。为了确保 templates/temp_file 被打包到这个示例项目结构中,在清单文件中添加这样的一行:

recursive-include package *

历史注释: 现代构建后端不需要使用清单文件,例如 flit、poetry,默认情况下将包含包数据文件。因此,如果您使用的是 pyproject.toml 而您没有 setup.py 文件,那么您可以忽略有关 MANIFEST.in 的所有内容。

现在,包装不碍事,进入阅读部分……

建议:

使用标准库pkgutil API。它在库代码中将如下所示:

# within package/mymodule1.py, for example
import pkgutil

data = pkgutil.get_data(__name__, "templates/temp_file")

它适用于拉链。它适用于 Python 2 和 Python 3。它不需要第三方依赖项。我真的不知道有什么缺点(如果你知道,请评论答案)。

避免的坏方法:

坏方法 #1:使用源文件中的相对路径

这是目前公认的答案。充其量,它看起来像这样:

from pathlib import Path

resource_path = Path(__file__).parent / "templates"
data = resource_path.joinpath("temp_file").read_bytes()

这有什么问题?假设您有可用的文件和子目录是不正确的。如果执行打包在 zip 或 wheel 中的代码,则此方法不起作用,并且无论您的包是否被提取到文件系统,用户都可能完全无法控制。

坏方法 #2:使用 pkg_resources API

这在投票最多的答案中有所描述。它看起来像这样:

from pkg_resources import resource_string

data = resource_string(__name__, "templates/temp_file")

这有什么问题?它添加了对setuptools 的runtime 依赖,它最好只是install 时间依赖。导入和使用pkg_resources 可能会变得非常缓慢,因为代码会构建所有 已安装包的工作集,即使您只对您自己的 包资源感兴趣。这在安装时没什么大不了的(因为安装是一次性的),但在运行时就很难看。

坏方法 #3:使用旧的 importlib.resources API

这是目前投票率最高的答案中的建议。从 Python 3.7 开始,它就在 standard library 中。它看起来像这样:

from importlib.resources import read_binary

data = read_binary("package.templates", "temp_file")

这有什么问题?好吧,不幸的是,该实现留下了一些不足之处,并且很可能在 Python 3.11 中被弃用。使用importlib.resources.read_binaryimportlib.resources.read_text 和朋友将要求您添加一个空文件templates/__init__.py,以便数据文件驻留在子包中而不是子目录中。它还将package/templates 子目录作为可导入的package.templates 子包公开。这不适用于许多已经使用资源子目录而不是资源子包发布的现有包,并且到处添加__init__.py 文件会混淆数据和代码之间的界限,这很不方便。

这种方法已经是deprecated in upstream importlib_resources,预计弃用将出现在版本 3.11 的 CPython 标准库中。 bpo-45514 跟踪弃用,migrating from legacy 提供 _legacy.py 包装器以帮助过渡。

荣誉奖:使用较新的 importlib_resources API

这尚未在任何其他答案中提及,但 importlib_resources 不仅仅是 Python 3.7+ importlib.resources 代码的简单反向移植。它具有可遍历的 API,您可以像这样使用它们:

import importlib_resources

my_resources = importlib_resources.files("package")
data = (my_resources / "templates" / "temp_file").read_bytes()

这适用于 Python 2 和 3,它适用于 zips,并且不需要在资源子目录中添加虚假的 __init__.py 文件。与pkgutil 相比,我能看到的唯一缺点是这些新 API 仅在 Python-3.9+ 的 stdlib 中可用,因此仍然需要第三方依赖项来支持较旧的 Python 版本。如果您只需要在 Python-3.9+ 上运行,请使用此方法,或者您可以在较旧的 Python 版本的 backport 上添加兼容层和 conditional dependency:

# in your library code:
try:
    from importlib.resources import files
except ImportError:
    from importlib_resources import files

# in your setup.py or similar:
from setuptools import setup
setup(
    ...
    install_requires=[
        'importlib_resources; python_version < "3.9"',
    ]
)

示例项目:

我在github 上创建了一个示例项目并上传到PyPI,它演示了上面讨论的所有五种方法。试试看:

$ pip install resources-example
$ resources-example

请参阅https://github.com/wimglenn/resources-example 了解更多信息。

【讨论】:

去年五月编辑过。但我想很容易错过介绍中的解释。尽管如此,你还是建议人们反对这个标准 - 这是一颗难以忍受的子弹:-) @ankostis 让我把问题转到你身上,尽管有所有这些缺点以及已经是 pending deprecation 的不完整 API,你为什么要推荐 importlib.resources?更新不一定更好。告诉我与 stdlib pkgutil 相比,它实际上提供了哪些优势,您的回答没有提及? 亲爱的@wim,Brett Canon's last response 对pkgutil.get_data() 的使用证实了我的直觉 - 这是一个未开发的、即将被弃用的 API。也就是说,我同意你的观点,importlib.resources 并不是一个更好的选择,但在 PY3.10 解决这个问题之前,我支持这个选择,因为了解到这不仅仅是文档推荐的另一个“标准”。跨度> @ankostis 我会对 Brett 的 cmets 持保留态度。 pkgutil 根本没有在 PEP 594 -- Removing dead batteries from the standard library 的弃用计划中提及,并且不太可能在没有充分理由的情况下被删除。它自 Python 2.3 以来一直存在,并在 PEP 302 中指定为加载器协议的一部分。使用“未定义的 API”并不是一个很有说服力的回答,它可以描述 Python 标准库的大部分内容! 让我补充一下:我也希望看到 importlib 资源成功! 我完全支持严格定义的 API。只是在目前的状态下,不能真正推荐它。该 API 仍在发生变化,它不能用于许多现有的包,并且仅在相对较新的 Python 版本中可用。在实践中,它几乎在各个方面都比pkgutil 差。您的“直觉”和appeal to authority 对我来说毫无意义,如果get_data 加载程序有问题,请出示证据和实际示例。【参考方案2】:

TLDR; 使用标准库的 importlib.resources module,如下面方法 2 中所述。

传统的 pkg_resources from setuptools 不再推荐,因为新方法:

是significantly more performant; 更安全,因为使用包(而不是路径字符串)会引发编译时错误; 它更直观,因为您不必“加入”路径; 开发速度更快,因为您不需要额外的依赖项 (setuptools),而只依赖 Python 的标准库。

我把传统的先列出来,说明移植现有代码时与新方法的区别(也移植explained here)。



假设您的模板位于嵌套在模块包内的文件夹中:

  <your-package>
    +--<module-asking-the-file>
    +--templates/
          +--temp_file                         <-- We want this file.

注意 1:当然,我们不应该摆弄__file__ 属性(例如,从 zip 提供代码时会中断)。

注意 2:如果您正在构建此包,请记住在您的 setup.py 中将您的数据文件声明为 package_data or data_files

1) 使用来自setuptoolspkg_resources(慢)

您可以使用 setuptools 发行版中的 pkg_resources 软件包,但 这是有代价的,performance-wise

import pkg_resources

# Could be any dot-separated package/module name or a "Requirement"
resource_package = __name__
resource_path = '/'.join(('templates', 'temp_file'))  # Do not use os.path.join()
template = pkg_resources.resource_string(resource_package, resource_path)
# or for a file-like stream:
template = pkg_resources.resource_stream(resource_package, resource_path)

提示:

即使您的发行版已压缩,这也会读取数据,因此您可以在您的 setup.py 中设置 zip_safe=True,和/或使用期待已久的 zipapp packer 从 python-3.5 到创建独立的发行版。

记得将setuptools 添加到您的运行时要求中(例如在 install_requires 中)。

...请注意,根据 Setuptools/pkg_resources 文档,您不应使用 os.path.join

Basic Resource Access

请注意,资源名称必须是 / 分隔的路径,并且不能是绝对的(即没有前导 /)或包含诸如“..”之类的相对名称。不要使用os.path 例程来操作资源路径,因为它们不是文件系统路径。

2) Python >= 3.7,或使用向后移植的 importlib_resources

使用标准库的importlib.resources module,比上面的setuptools效率更高:

try:
    import importlib.resources as pkg_resources
except ImportError:
    # Try backported to PY<37 `importlib_resources`.
    import importlib_resources as pkg_resources

from . import templates  # relative-import the *package* containing the templates

template = pkg_resources.read_text(templates, 'temp_file')
# or for a file-like stream:
template = pkg_resources.open_text(templates, 'temp_file')

注意:

关于函数read_text(package, resource)

package 可以是字符串或模块。 resource 不再是路径,而只是现有包中要打开的资源的文件名;它可能不包含路径分隔符,也可能没有子资源(即它不能是目录)。

对于问题中提出的示例,我们现在必须:

通过在其中创建一个空的__init__.py 文件,将&lt;your_package&gt;/templates/ 放入适当的包中, 所以现在我们可以使用一个简单的(可能是相对的)import 语句(不再解析包/模块名称), 只需询问resource_name = "temp_file"(无路径)。

提示:

要访问当前模块内的文件,请将包参数设置为__package__,例如pkg_resources.read_text(__package__, 'temp_file')(感谢@ben-mares)。 当使用path() 询问实际文件名 时事情会变得有趣,因为现在上下文管理器用于临时创建的文件(阅读this)。 使用install_requires=[" importlib_resources ; python_version&lt;'3.7'"] 添加向后移植的库,有条件地适用于较旧的Python(如果您使用setuptools&lt;36.2.1 打包项目,请检查this)。 如果您从传统方法迁移,请记住从 runtime-requirements 中删除 setuptools 库。 记得自定义setup.pyMANIFEST为include any static files。 您也可以在setup.py 中设置zip_safe=True

【讨论】:

str.join 需要序列 resource_path = '/'.join(('templates', 'temp_file')) 我不断收到NotImplementedError: Can't perform this operation for loaders without 'get_data()' 任何想法? 请注意,importlib.resourcespkg_resources不一定兼容importlib.resources 与添加到sys.path 的压缩文件一起使用,setuptools 和pkg_resources 与egg 文件一起使用,这些文件是存储在本身添加到sys.path 的目录中的压缩文件。例如。使用sys.path = [..., '.../foo', '.../bar.zip'],鸡蛋进入.../foo,但也可以导入bar.zip 中的包。您不能使用pkg_resourcesbar.zip 中的包中提取数据。我还没有检查 setuptools 是否为 importlib.resources 注册了必要的加载器来处理鸡蛋。 如果出现错误Package has no location,是否需要额外的setup.py配置? 如果您想访问当前模块中的文件(而不是像示例中的templates 这样的子模块),那么您可以将package 参数设置为__package__,例如pkg_resources.read_text(__package__, 'temp_file')【参考方案3】:

接受的答案应该是使用importlib.resourcespkgutil.get_data 还要求参数 package 是一个非命名空间包 (see pkgutil docs)。因此,包含资源的目录必须有一个__init__.py 文件,使其具有与importlib.resources 完全相同的限制。如果pkg_resources 的开销问题不是问题,这也是一个可以接受的替代方案。

Pre-Python-3.3,所有包都必须有一个__init__.pyPost-Python-3.3,文件夹不需要 __init__.py 成为一个包。这称为namespace package。不幸的是,pkgutil 不适用于 namespace packages (see pkgutil docs)。

以包结构为例:

+-- foo/
|   +-- __init__.py
|   +-- bar/
|   |   +-- hi.txt

hi.txt 只有Hi!,你会得到以下结果

>>> import pkgutil
>>> rsrc = pkgutil.get_data("foo.bar", "hi.txt")
>>> print(rsrc)
None

但是,在bar 中使用__init__.py,您会得到

>>> import pkgutil
>>> rsrc = pkgutil.get_data("foo.bar", "hi.txt")
>>> print(rsrc)
b'Hi!'

【讨论】:

这个答案是错误的——包含资源的目录不需要是一个包。它可以是包的子目录。 importlib.resources 的限制是 pkgutil 没有的,即包含资源的目录本身也需要有一个__init__.py,即它必须是一个子包。这与命名空间包问题无关,它关注的是***目录中是否存在__init__.py,而不是包内的数据子目录中。 @wim 对不起,但我相信你弄错了。 pre-Python 3.3+,所有包都必须有一个 __init__.py 才能加载。在 3.3 之后,包不需要它们。没有__init__.py 的包是namespace packages。根据pkgutil 文档,如果您尝试从命名空间包中加载资源,您将获得None。请查看我更新后的编辑答案。 您错误地使用了pkgutil。试试pkgutil.get_data("foo", "bar/hi.txt")【参考方案4】:

如果你有这个结构

lidtk
├── bin
│   └── lidtk
├── lidtk
│   ├── analysis
│   │   ├── char_distribution.py
│   │   └── create_cm.py
│   ├── classifiers
│   │   ├── char_dist_metric_train_test.py
│   │   ├── char_features.py
│   │   ├── cld2
│   │   │   ├── cld2_preds.txt
│   │   │   └── cld2wili.py
│   │   ├── get_cld2.py
│   │   ├── text_cat
│   │   │   ├── __init__.py
│   │   │   ├── README.md   <---------- say you want to get this
│   │   │   └── textcat_ngram.py
│   │   └── tfidf_features.py
│   ├── data
│   │   ├── __init__.py
│   │   ├── create_ml_dataset.py
│   │   ├── download_documents.py
│   │   ├── language_utils.py
│   │   ├── pickle_to_txt.py
│   │   └── wili.py
│   ├── __init__.py
│   ├── get_predictions.py
│   ├── languages.csv
│   └── utils.py
├── README.md
├── setup.cfg
└── setup.py

你需要这个代码:

import pkg_resources

# __name__ in case you're within the package
# - otherwise it would be 'lidtk' in this example as it is the package name
path = 'classifiers/text_cat/README.md'  # always use slash
filepath = pkg_resources.resource_filename(__name__, path)

奇怪的“总是使用斜线”部分来自setuptools APIs

另外请注意,如果您使用路径,则必须使用正斜杠 (/) 作为路径分隔符,即使您在 Windows 上也是如此。 Setuptools 在构建时自动将斜杠转换为适当的特定于平台的分隔符

如果您想知道文档在哪里:

PEP 0365 https://packaging.python.org/guides/single-sourcing-package-version/

【讨论】:

pkg_resources 具有 pkgutil 克服的开销。此外,如果提供的代码作为入口点运行,__name__ 将评估为__main__,而不是包名称。【参考方案5】:

David Beazley 和 Brian K. Jones 的 Python Cookbook 第三版“10.8.Reading Datafiles Within a Package”中的内容给出了答案。

我就到这里来:

假设你有一个包,其中的文件组织如下:

mypackage/
    __init__.py
    somedata.dat
    spam.py

现在假设文件 spam.py 想要读取文件 somedata.dat 的内容。去做 它,使用以下代码:

import pkgutil
data = pkgutil.get_data(__package__, 'somedata.dat')

生成的变量数据将是一个包含文件原始内容的字节字符串。

get_data() 的第一个参数是一个包含包名的字符串。你可以 要么直接提供它,要么使用特殊变量,例如__package__。第二 参数是包中文件的相对名称。如有需要,您可以导航 使用标准 Unix 文件名约定进入不同的目录,只要 final 目录仍然位于包中。

这样,包可以安装为目录,.zip 或 .egg。

【讨论】:

我喜欢你引用了食谱!【参考方案6】:

包中的每个 python 模块都有一个__file__ 属性

您可以将其用作:

import os 
from mypackage

templates_dir = os.path.join(os.path.dirname(mypackage.__file__), 'templates')
template_file = os.path.join(templates_dir, 'template.txt')

鸡蛋资源见:http://peak.telecommunity.com/DevCenter/PythonEggs#accessing-package-resources

【讨论】:

这不适用于 zip 文件中包含的源代码。 这不是一个可靠的方法。它会引入脆弱的代码。【参考方案7】:

假设您使用的是 egg 文件;未提取:

我在最近的一个项目中“解决”了这个问题,通过使用安装后脚本,该脚本将我的模板从 egg(zip 文件)提取到文件系统中的正确目录。这是我找到的最快、最可靠的解决方案,因为使用__path__[0] 有时会出错(我不记得名字了,但我至少看到了一个库,它在该列表前面添加了一些东西!)。

此外,egg 文件通常会在运行中被提取到一个名为“egg cache”的临时位置。您可以在启动脚本之前甚至之后使用环境变量更改该位置,例如。

os.environ['PYTHON_EGG_CACHE'] = path

但是有 pkg_resources 可能会正确完成这项工作。

【讨论】:

以上是关于如何从 Python 包中读取(静态)文件?的主要内容,如果未能解决你的问题,请参考以下文章

如何使用静态库传输资源文件(如何将资源包装在包中)?

java如何读取压缩包中的文本文件

你如何告诉 CMake 静态链接到使用 find_package 找到的包中的库?

如何在 PIG 脚本中从数据包中读取数据

jar包里面的代码如何读取jar包中的配置文件?

如何从 PyPi 包中提取依赖项