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

时间:2011-05-17 08:09:46

标签: python file package

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

我的情况

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

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

package\templates\temp_file

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

10 个答案:

答案 0 :(得分:103)

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

由于pkg_resources from setuptools,不推荐传统 performance reasons
我保留了传统列表的第一个,解释了移植现有代码时与新方法的差异(同时移植explained here)。


我们假设您的模板位于模块的包中:

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

注意:当然,我们不应该使用__file__属性(例如代码会在拉链服务时中断)。

1)使用pkg_resources(慢)

中的setuptools

您可以使用 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)
  

<强>提示:

     
      
  • 即使您的发布内容已压缩,也会读取数据,因此您可以在zip_safe=True中设置setup.py,和/或使用期待已久的zipapp packer来自 python-3.5 创建自包含的发行版。

  •   
  • 请务必在运行时要求中添加setuptools(例如,在install_requires`中)。

  •   

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

  

Basic Resource Access

     

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

2)Python&gt; = 3.7,或使用反向移植的importlib_resources

使用比setuptools更高效的标准库importlib.resources module

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  # the package containing the file

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不再是路径,而只是在现有包内打开的资源的文件名;它可能不包含路径分隔符,也可能没有子资源(即它不能是目录)。
  •   

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

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

<强>提示:

     
      
  • path()询问实际文件名时,事情变得有趣,因为现在上下文管理器用于临时创建的文件(阅读this)。
  •   
  • 使用install_requires=[" importlib_resources ; python_version<'3.7'"]有条件地为旧版Pythons添加backported库(如果您使用setuptools<36.2.1打包项目,请选中this)。
  •   
  • 如果您从传统方法迁移,请记住从 runtime-requirements 中删除setuptools库。
  •   
  • 您也可以在zip_safe=True
  • 中设置setup.py   

答案 1 :(得分:11)

如果你有这个结构

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
│   │   │   ├── REAMDE.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/REAMDE.md'  # always use slash
filepath = pkg_resources.resource_filename(__name__, path)

我不太确定“总是使用斜线”部分。它可能来自setuptools

  

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

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

答案 2 :(得分:6)

包装前奏:

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

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

.
├── 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")
print("data:", repr(data))
text = pkgutil.get_data(__name__, "templates/temp_file").decode()
print("text:", repr(text))

它可以使用拉链。它适用于Python 2和Python3。它不需要第三方依赖项。我真的不知道有什么弊端(如果您愿意,请对答案发表评论)。

避免的坏方法:

错误的方式1:使用源文件中的相对路径

这是当前接受的答案。充其量看起来像这样:

from pathlib import Path

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

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

错误的方式2:使用pkg_resources API

这是目前投票最多的答案。看起来像这样:

from pkg_resources import resource_string

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

这是怎么了?它在setuptools上添加了 runtime 依赖关系,最好仅是 install 时间依赖关系。导入和使用pkg_resources可能会变得非常缓慢,因为该代码会建立一个 all 安装软件包的工作集,即使您只对您自己的 软件包感兴趣资源。在安装时这没什么大不了的(因为安装是一次性的),但是在运行时却很难看。

坏方法3:使用importlib.resources API

这是最近standard library的新增内容(Python 3.7中的新功能),但是也有一个备用端口。看起来像这样:

try:
    from importlib.resources import read_binary
    from importlib.resources import read_text
except ImportError:
    # Python 2.x backport
    from importlib_resources import read_binary
    from importlib_resources import read_text

data = read_binary("package.templates", "temp_file")
print("data", repr(data))
text = read_text("package.templates", "temp_file")
print("text", repr(text))

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

示例项目:

我已经在github上创建了一个示例项目,并将其上传到PyPI上,该项目演示了上面讨论的所有四种方法。试试:

$ pip install resources-example
$ resources-example

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

答案 3 :(得分:5)

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

我就到这里:

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

mypackage/
    __init__.py
    somedata.dat
    spam.py

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

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

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

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

通过这种方式,该软件包可以安装为目录,.zip或.egg。

答案 4 :(得分:3)

包中的每个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

答案 5 :(得分:0)

假设您正在使用鸡蛋文件;未提取:

我在最近的一个项目中通过使用postinstall脚本解决了这个问题,该脚本将我的模板从egg(zip文件)中提取到文件系统中的正确目录。这是我发现的最快,最可靠的解决方案,因为与__path__[0]一起工作有时可能会出错(我不记得这个名字,但我至少看过一个图书馆,在这个列表前添加了一些东西! )。

鸡蛋文件通常也会被动态提取到称为“鸡蛋缓存”的临时位置。您可以在启动脚本之前或之后使用环境变量更改该位置,例如

os.environ['PYTHON_EGG_CACHE'] = path

但是pkg_resources可以正常完成工作。

答案 6 :(得分:0)

可接受的答案应该是使用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。不幸的是,pkgutilnamespace packagessee pkgutil docs)不兼容。

例如,具有包装结构:

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

其中hi.txt只有Hi!,您将得到以下内容

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

但是,__init__.py中有一个bar,您会得到

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

答案 7 :(得分:-1)

答案 8 :(得分:-4)

您应该可以使用以下内容导入部分包名称空间:

from my_package import my_stuff

...如果这是一个正确构造的Python包(通常是抽象的),你不需要指定任何看起来像文件名的东西。

答案 9 :(得分:-7)

[补充2016-06-15:显然这在所有情况下都不起作用。请参考其他答案]


import os, mypackage
template = os.path.join(mypackage.__path__[0], 'templates', 'temp_file')