你能告诉我如何读取Python包中的文件吗?
我的情况
我加载的包有许多模板(作为字符串使用的文本文件),我想从程序中加载它们。但是我如何指定这样的文件的路径?
假设我想读取一个文件:
package\templates\temp_file
某种路径操作?包基本路径跟踪?
你能告诉我如何读取Python包中的文件吗?
我的情况
我加载的包有许多模板(作为字符串使用的文本文件),我想从程序中加载它们。但是我如何指定这样的文件的路径?
假设我想读取一个文件:
package\templates\temp_file
某种路径操作?包基本路径跟踪?
当前回答
如果你有这样的结构
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在构建时自动将斜杠转换为适当的平台特定的分隔符
如果你想知道文档在哪里:
PEP 0365 https://packaging.python.org/guides/single-sourcing-package-version/
其他回答
如果你有这样的结构
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在构建时自动将斜杠转换为适当的平台特定的分隔符
如果你想知道文档在哪里:
PEP 0365 https://packaging.python.org/guides/single-sourcing-package-version/
假设您正在使用卵文件;不提取:
我在最近的一个项目中“解决”了这个问题,通过使用postinstall脚本,它将我的模板从egg (zip文件)提取到文件系统中的适当目录中。这是我发现的最快,最可靠的解决方案,因为使用__path__[0]有时会出错(我不记得名字了,但我至少看到一个库,它在列表前面添加了一些东西!)
此外,egg文件通常会被提取到一个称为“egg缓存”的临时位置。您可以使用环境变量更改该位置,可以在启动脚本之前或稍后,例如。
os.environ['PYTHON_EGG_CACHE'] = path
但是,有pkg_resources可以正确地完成这项工作。
10.8.中的内容。在Python Cookbook第三版中,David Beazley和Brian K. Jones给出了答案。
我把它放到这里:
假设你有一个包,里面的文件组织如下:
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。
包装前奏:
在您担心读取资源文件之前,第一步是确保数据文件首先被打包到您的发行版中——直接从源树中读取它们很容易,但重要的部分是确保这些资源文件可以从已安装包中的代码中访问。
像这样构建你的项目,把数据文件放在包中的子目录中:
.
├── 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。如果你没有setup.py文件你就可以忽略manifest . toml。
现在,包装结束,进入阅读部分……
推荐:
使用标准库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包或一个轮子中,这种方法是行不通的,而且是否将包提取到文件系统可能完全不在用户的控制范围内。
坏方法2:使用pkg_resources api
这在投票最多的答案中有描述。它看起来是这样的:
from pkg_resources import resource_string
data = resource_string(__name__, "templates/temp_file")
这有什么不对吗?它在setuptools上添加了一个运行时依赖项,这个依赖项最好只是安装时依赖项。导入和使用pkg_resources可能会变得非常慢,因为代码构建了所有已安装包的工作集,尽管您只对自己的包资源感兴趣。这在安装时不是什么大问题(因为安装是一次性关闭的),但在运行时就很难看了。
坏方法3:使用遗留的importlib。资源的api
这是目前投票最多的答案中的建议。它从Python 3.7开始就在标准库中。它是这样的:
from importlib.resources import read_binary
data = read_binary("package.templates", "temp_file")
What's wrong with that? Well, unfortunately, the implementation left some things to be desired and it's likely to be deprecated in Python 3.11. Using importlib.resources.read_binary, importlib.resources.read_text and friends will require you to add an empty file templates/__init__.py so that data files reside within a sub-package rather than in a subdirectory. It will also expose the package/templates subdirectory as an importable package.templates sub-package in its own right. This won't work with many existing packages which are already published using resource subdirectories instead of resource sub-packages, and it's inconvenient to add the __init__.py files everywhere muddying the boundary between data and code.
这种方法在上游的importlib_resources中已经被弃用,预计从3.11版开始的CPython stdlib中也会出现这种弃用。Bpo-45514跟踪弃用和从遗留提供_legacy.py包装器迁移,以帮助进行转换。
值得一提的是:使用更新的importlib_resources api
在其他答案中还没有提到这一点,但importlib_resources不仅仅是Python 3.7+ importlib的一个简单的后移植。资源代码。它有可遍历的api,你可以像这样使用:
import importlib_resources
my_resources = importlib_resources.files("package")
data = (my_resources / "templates" / "temp_file").read_bytes()
This works on Python 2 and 3, it works in zips, and it doesn't require spurious __init__.py files to be added in resource subdirectories. The only downside vs pkgutil that I can see is that these new APIs are only available in the stdlib for Python-3.9+, so there is still a third-party dependency needed to support older Python versions. If you only need to run on Python-3.9+ then use this approach, or you can add a compatibility layer and a conditional dependency on the backport for older Python versions:
# 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。
公认的答案应该是使用importlib.resources。pkgutil。Get_data还要求参数包是非命名空间包(参见pkgutil文档)。因此,包含资源的目录必须有一个__init__.py文件,使其具有与importlib.resources完全相同的限制。如果pkg_resources的开销问题不是问题,这也是一个可以接受的替代方案。
在python -3.3之前,所有包都必须具有__init__.py。在python -3.3之后,文件夹不需要__init__.py作为包。这称为命名空间包。不幸的是,pkgutil不能使用名称空间包(参见pkgutil文档)。
例如,对于包结构:
+-- 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!'