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

ron*_*zon 75 python file package

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

我的情况

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

想象一下,我想从以下位置读取文件:

package\templates\temp_file
Run Code Online (Sandbox Code Playgroud)

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

ank*_*tis 133

假设您的模板位于此路径的模块包中:

  <your-package>
    +--<module-asking-the-file>
    +--templates/
          +--temp_file                         <-- We want this file.
Run Code Online (Sandbox Code Playgroud)

读取模板的正确方法是使用setuptools发行版中的importlib.resources包:

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)
Run Code Online (Sandbox Code Playgroud)

提示:
这会读,即使你的分布是压缩数据,所以你可以设置 pkg_resources你的setuptools,和/或使用期待已久的__file__打包机蟒蛇- 3.5打造自成体系的分布.

根据Setuptools/package_datadocs,不要使用data_files:

基本资源访问

请注意,资源名称必须是setup.py分隔路径,不能是绝对路径(即无引导路径pkg_resources)或包含相对名称,如" setuptools".千万不能使用pkg_resources程序来操作的资源路径,因为它们不是文件系统路径.

  • 如果您想访问当前模块内的文件(而不是像示例中的 `templates` 这样的子模块),那么您可以将 `package` 参数设置为 `__package__`,例如 `pkg_resources.read_text(__package__, '临时文件')` (7认同)
  • 我不断收到 `NotImplementedError: Can't perform this operation for loader without 'get_data()'` 有什么想法吗? (2认同)
  • 请注意,“importlib.resources”和“pkg_resources”*不一定兼容*。“importlib.resources”适用于添加到“sys.path”的 zip 文件,setuptools 和“pkg_resources”适用于 Egg 文件,这些文件是存储在本身添加到“sys.path”的目录中的 zip 文件。例如,使用 `sys.path = [..., '.../foo', '.../bar.zip']`,鸡蛋放在 `.../foo` 中,但包放在 `bar.zip 中` 也可以导入。您无法使用“pkg_resources”从“bar.zip”中的包中提取数据。我还没有检查 setuptools 是否为“importlib.resources”注册了必要的加载器来处理 Eggs。 (2认同)

wim*_*wim 116

包装前奏:

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

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

.
??? package
?   ??? __init__.py
?   ??? templates
?   ?   ??? temp_file
?   ??? mymodule1.py
?   ??? mymodule2.py
??? README.rst
??? MANIFEST.in
??? setup.py
Run Code Online (Sandbox Code Playgroud)

你应该通过include_package_data=Truesetup()呼叫。仅当您想使用 setuptools/distutils 和构建源分发时才需要清单文件。要确保templates/temp_file为此示例项目结构打包,请在清单文件中添加如下一行:

recursive-include package *
Run Code Online (Sandbox Code Playgroud)

历史遗留注释: 现代构建后端(例如 flit、poetry)不需要使用清单文件,默认情况下这些后端将包含包数据文件。因此,如果您正在使用pyproject.toml并且没有setup.py文件,那么您可以忽略所有关于MANIFEST.in.

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

推荐:

使用标准库pkgutilAPI。它在库代码中看起来像这样:

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

data = pkgutil.get_data(__name__, "templates/temp_file")
Run Code Online (Sandbox Code Playgroud)

它适用于拉链。它适用于 Python 2 和 Python 3。它不需要第三方依赖项。我并没有真正意识到任何缺点(如果你是,那么请对答案发表评论)。

避免的坏方法:

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

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

from pathlib import Path

resource_path = Path(__file__).parent / "templates"
data = resource_path.joinpath("temp_file").read_bytes()
Run Code Online (Sandbox Code Playgroud)

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

坏方法#2:使用 pkg_resources API

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

from pkg_resources import resource_string

data = resource_string(__name__, "templates/temp_file")
Run Code Online (Sandbox Code Playgroud)

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

糟糕的方式 #3:使用 importlib.resources API

这是目前最高投票答案中的建议。这是最近添加的标准库Python 3.7 中的新内容)。它看起来像这样:

from importlib.resources import read_binary

data = read_binary("package.templates", "temp_file")
Run Code Online (Sandbox Code Playgroud)

那有什么问题?好吧,不幸的是,它不起作用......还没有。这仍然是一个不完整的 API,使用importlib.resources将要求您添加一个空文件templates/__init__.py,以便数据文件驻留在子包中而不是子目录中。它还会将package/templates子目录本身公开为可导入的package.templates子包。如果这不是什么大不了的事情并且不会打扰您,那么您可以继续将__init__.py文件添加到那里并使用导入系统访问资源。但是,当您在使用它时,您也可以将其改为my_resources.py文件,只需在模块中定义一些字节或字符串变量,然后在 Python 代码中导入它们。无论哪种方式,都是进口系统在这里承担重任。

荣誉提名:使用较新的 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()
Run Code Online (Sandbox Code Playgroud)

这适用于 Python 2 和 3,它适用于 zip,并且不需要__init__.py在资源子目录中添加虚假文件。pkgutil我能看到的唯一缺点是这些新的 API 尚未出现在 stdlib 中,因此仍然存在第三方依赖性。较新的 APIimportlib_resources应该importlib.resources在 Python 3.9 中到达 stdlib 。

示例项目:

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

.
??? package
?   ??? __init__.py
?   ??? templates
?   ?   ??? temp_file
?   ??? mymodule1.py
?   ??? mymodule2.py
??? README.rst
??? MANIFEST.in
??? setup.py
Run Code Online (Sandbox Code Playgroud)

有关更多信息,请参阅https://github.com/wimglenn/resources-example

  • 去年5月已经编辑过。但我想很容易错过简介中的解释。尽管如此,你还是建议人们反对这个标准——这是一颗很难咬的子弹:-) (4认同)
  • @ankostis 让我把问题转向你,为什么你会推荐 `importlib.resources` 尽管存在所有这些缺点,而且 API 不完整,而且已经[待弃用](https://gitlab.com/python-devs/importlib_resources/issues /80)?越新并不一定越好。告诉我 **它实际上比 stdlib pkgutil 提供什么优势**,你的答案没有提到这一点? (3认同)
  • 让我补充一下:*我也希望看到 importlib 资源成功!*我完全支持严格定义的 API。只是就目前的状态来说,实在不值得推荐。该 API 仍在发生变化,许多现有包无法使用,并且仅在相对较新的 Python 版本中可用。实际上,它在几乎所有方面都比“pkgutil”更糟糕。你的“直觉”和[诉诸权威](https://www.logiclyfallacious.com/ologicalfallacies/Appeal-to-Authority)对我来说毫无意义,如果“get_data”加载器存在问题,那么请展示证据和实际例子。 (3认同)
  • 这对于获取文件内容很有效,但我需要文件名或类似文件的对象。我正在尝试这样做:“logging.config.fileConfig(filename)”。 (2认同)
  • @ankostis 我对布雷特的评论持保留态度。*[PEP 594 -- 从标准库中删除没电的电池](https://www.python.org/dev/peps/pep-0594/)* 的弃用时间表中根本没有提到 `pkgutil`,并且如果没有充分的理由,不太可能被删除。它自 Python 2.3 以来就已存在,并在 [PEP 302](https://www.python.org/dev/peps/pep-0302/#optical-extensions-to-the-importer- 中指定为加载程序协议的一部分协议)。使用“未定义的 API”并不是一个很有说服力的答案,它可以描述大部分 Python 标准库! (2认同)

Mar*_*oma 14

如果你有这种结构

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
Run Code Online (Sandbox Code Playgroud)

你需要这个代码:

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)
Run Code Online (Sandbox Code Playgroud)

我不太确定"总是使用斜线"部分.它可能来自setuptools

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

如果您想知道文档的位置:


cha*_*ang 7

David Beazley和Brian K. Jones撰写的Python Cookbook第三版“ 10.8。读取包中的数据文件”中的内容给出了答案。

我将它送到这里:

假设您有一个软件包,其文件组织如下:

mypackage/
    __init__.py
    somedata.dat
    spam.py
Run Code Online (Sandbox Code Playgroud)

现在假设文件spam.py要读取文件somedata.dat的内容。为此,请使用以下代码:

import pkgutil
data = pkgutil.get_data(__package__, 'somedata.dat')
Run Code Online (Sandbox Code Playgroud)

结果变量数据将是一个字节字符串,其中包含文件的原始内容。

get_data()的第一个参数是包含程序包名称的字符串。您可以直接提供它,也可以使用特殊变量,例如__package__。第二个参数是包中文件的相对名称。如有必要,只要最终目录仍位于软件包中,就可以使用标准Unix文件名约定导航到其他目录。

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

  • 如果文件是 .csv,我想将其读入 pandas 数据帧怎么办? (4认同)

Flo*_*ian -3

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

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

此外,egg 文件通常会即时提取到称为“egg 缓存”的临时位置。您可以在启动脚本之前甚至稍后使用环境变量更改该位置,例如。

os.environ['PYTHON_EGG_CACHE'] = path
Run Code Online (Sandbox Code Playgroud)

然而,有pkg​​_resources可以正确完成这项工作。