问题:如何从Python包内部读取(静态)文件?
您能告诉我如何读取Python包中的文件吗?
我的情况
我加载的程序包具有许多模板(要用作程序的文本文件),我想从程序中加载它们。但是,如何指定此类文件的路径?
想象一下我想从以下位置读取文件:
package\templates\temp_file
某种路径操纵?包基本路径跟踪?
回答 0
[添加2016-06-15:显然,这并非在所有情况下都有效。请参阅其他答案]
import os, mypackage
template = os.path.join(mypackage.__path__[0], 'templates', 'temp_file')
回答 1
TLDR;使用标准库的importlib.resources
模块,如下面方法2中所述。
不再推荐使用传统的 pkg_resources
fromsetuptools
,因为新方法:
- 它的性能明显更高 ;
- 这样做比较安全,因为使用软件包(而不是路径)会引起编译时错误;
- 它更直观,因为您不必“加入”路径;
- 由于不需要额外的依赖项(
setuptools
),因此开发时速度更快,而仅依赖于Python的标准库。
我将传统列在第一位,以在移植现有代码时解释新方法的区别(此处也解释了移植)。
假设您的模板位于模块包内嵌套的文件夹中:
<your-package>
+--<module-asking-the-file>
+--templates/
+--temp_file <-- We want this file.
注意1:当然,我们不应该摆弄这个
__file__
属性(例如,从zip投放时代码会中断)。注意2:如果您要构建此程序包,请记住将
package_data
或data_files
中的数据文件隐藏起来setup.py
。
1)使用pkg_resources
从setuptools
(慢)
您可以使用setuptools发行版中的pkg_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)
提示:
这将读取的数据,即使您的分布压缩,所以你可以设置
zip_safe=True
你的setup.py
,和/或使用期待已久的zipapp
打包机从Python- 3.5打造自成体系的分布。记住要添加
setuptools
到您的运行时要求中(例如,在install_requires中)。
…,请注意,根据Setuptools / pkg_resources
docs,您不应使用os.path.join
:
基本资源访问
请注意,资源名称必须是-
/
分隔的路径,并且不能是绝对路径(即,没有前导/
)或包含诸如“..
”的相对名称。千万不能使用os.path
程序来操作的资源路径,因为它们不是文件系统路径。
2)Python> = 3.7,或使用反向移植的importlib_resources
库
使用标准库的importlib.resources
模块,该模块比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
不再被一个路径,但资源开放,现有的包内的不仅是文件名; 它可能不包含路径分隔符,并且可能没有子资源(即它不能是目录)。
对于问题中提出的示例,我们现在必须:
<your_package>/templates/
通过__init__.py
在其中创建一个空文件,将其制作成适当的软件包,- 所以现在我们可以使用一个简单的(可能是相对的)
import
语句(不再解析包/模块名称), - 并索要
resource_name = "temp_file"
(没有路径)。
提示:
- 要访问当前模块内部的文件,请将package参数设置为
__package__
,例如pkg_resources.read_text(__package__, 'temp_file')
(感谢@ ben-mares)。- 当事情变得有趣的实际文件名被要求用
path()
的,因为现在用于临时创建的文件(阅读上下文经理这个)。- 添加回迁库,有条件地为老年人Python,用
install_requires=[" importlib_resources ; python_version<'3.7'"]
(检查这个,如果你用打包项目setuptools<36.2.1
)。- 如果从传统方法迁移,请记住
setuptools
从运行时要求中删除库。- 记住要定制
setup.py
或MANIFEST
以包括任何静态文件。- 您也可以
zip_safe=True
在中设置setup.py
。
回答 2
包装前奏:
在甚至不必担心读取资源文件之前,第一步就是要确保首先将数据文件打包到您的发行版中-可以很容易地直接从源代码树中读取它们,但重要的是确保可以从已安装的软件包中的代码访问这些资源文件。
这样构造项目,将数据文件放入包中的子目录中:
.
├── package
│ ├── __init__.py
│ ├── templates
│ │ └── temp_file
│ ├── mymodule1.py
│ └── mymodule2.py
├── README.rst
├── MANIFEST.in
└── setup.py
你应该通过include_package_data=True
在setup()
呼叫。仅当您要使用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上添加了运行时依赖关系,最好仅是安装时间依赖关系。即使代码只对您自己的软件包资源感兴趣,导入和使用也会变得非常缓慢,因为代码会建立所有已安装软件包的工作集。在安装时这没什么大不了的(因为安装是一次性的),但是在运行时却很难看。pkg_resources
坏方法#3:使用importlib.resources API
目前,这是投票最多的答案中的建议。这是最近标准库的新增功能(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
回答 3
如果你有这个结构
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
API
还要注意,如果使用路径,则即使在Windows上,也必须使用正斜杠(/)作为路径分隔符。Setuptools在生成时自动将斜杠转换为适当的特定于平台的分隔符
如果您想知道文档在哪里:
回答 4
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。
回答 5
包中的每个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
回答 6
假设您使用的是鸡蛋文件;未提取:
我通过使用后安装脚本在最近的项目中“解决”了该问题,该脚本将我的模板从egg(zip文件)提取到文件系统中的正确目录。这是我发现的最快,最可靠的解决方案,因为__path__[0]
有时使用会出错(我不记得这个名称了,但是我至少浏览了一个库,在列表的前面增加了一些东西!)。
通常,鸡蛋文件通常也被即时提取到一个称为“鸡蛋缓存”的临时位置。您可以在启动脚本之前甚至以后使用环境变量来更改该位置。
os.environ['PYTHON_EGG_CACHE'] = path
但是,有pkg_resources可能会正确完成此工作。