Làm cách nào để đọc một tệp (tĩnh) từ bên trong một gói Python?


106

Bạn có thể cho tôi biết làm thế nào tôi có thể đọc một tệp bên trong gói Python của tôi không?

Hoàn cảnh của tôi

Một gói mà tôi tải có một số mẫu (tệp văn bản được sử dụng làm chuỗi) mà tôi muốn tải từ bên trong chương trình. Nhưng làm cách nào để chỉ định đường dẫn đến tệp đó?

Hãy tưởng tượng tôi muốn đọc một tệp từ:

package\templates\temp_file

Một số loại thao tác đường dẫn? Theo dõi đường dẫn cơ sở gói?



Câu trả lời:


-12

[thêm 2016-06-15: rõ ràng điều này không hoạt động trong mọi tình huống. vui lòng tham khảo các câu trả lời khác]


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

175

TLDR; Sử dụng importlib.resourcesmô-đun của thư viện tiêu chuẩn như được giải thích trong phương pháp số 2 bên dưới.

Các truyền thống pkg_resourcestừsetuptools không được khuyến khích nữa vì phương pháp mới:

  • có hiệu suất cao hơn đáng kể ;
  • an toàn hơn vì việc sử dụng các gói (thay vì cài đặt đường dẫn) làm tăng lỗi thời gian biên dịch;
  • nó trực quan hơn vì bạn không phải "tham gia" các đường dẫn;
  • nó nhanh hơn khi phát triển vì bạn không cần thêm phụ thuộc ( setuptools), mà chỉ dựa vào thư viện chuẩn của Python.

Tôi đã giữ nguyên danh sách truyền thống trước tiên, để giải thích sự khác biệt với phương pháp mới khi chuyển mã hiện có (chuyển cũng được giải thích ở đây ).



Giả sử rằng các mẫu của bạn được đặt trong một thư mục được lồng bên trong gói mô-đun của bạn:

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

Lưu ý 1: Để chắc chắn, chúng ta KHÔNG nên thao túng __file__thuộc tính (ví dụ: mã sẽ bị hỏng khi được phân phát từ mã zip).

Lưu ý 2: Nếu bạn đang xây dựng gói này, hãy nhớ khai báo các tệp dữ liệu của bạn dưới dạng package_datahoặcdata_files trong của bạn setup.py.

1) Sử dụng pkg_resourcestừ setuptools(chậm)

Bạn có thể sử dụng pkg_resourcesgói từ phân phối setuptools , nhưng điều đó đi kèm với chi phí, hiệu suất khôn ngoan :

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)

Lời khuyên:

  • Điều này sẽ đọc dữ liệu ngay cả khi bản phân phối của bạn được nén, vì vậy bạn có thể đặt zip_safe=Truetrong của mình setup.pyvà / hoặc sử dụng trình zipappđóng gói đã chờ đợi từ lâu từ python-3.5 để tạo các bản phân phối độc lập.

  • Hãy nhớ thêm setuptoolsvào yêu cầu thời gian chạy của bạn (ví dụ: trong install_requires`).

... và lưu ý rằng theo Setuptools / pkg_resourcesdocs, bạn không nên sử dụng os.path.join:

Quyền truy cập tài nguyên cơ bản

Lưu ý rằng tên tài nguyên phải là /các đường dẫn-phân tách và không được là tuyệt đối (tức là không có hàng đầu /) hoặc chứa các tên tương đối như " ..". Đừng không sử dụng os.paththói quen để thao tác đường dẫn tài nguyên, vì họ là không đường dẫn hệ thống tập tin.

2) Python> = 3.7 hoặc sử dụng importlib_resourcesthư viện được backported

Sử dụng importlib.resourcesmô-đun của thư viện tiêu chuẩn hiệu quả hơn setuptoolsở trên:

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')

Chú ý:

Về chức năng read_text(package, resource):

  • packagethể là một chuỗi hoặc một mô-đun.
  • Các resourceKHÔNG phải là một con đường nữa, nhưng chỉ là tên tập tin của tài nguyên để mở, trong một gói phần mềm hiện có; nó có thể không chứa dấu phân cách đường dẫn và nó có thể không có tài nguyên con (tức là nó không thể là một thư mục).

Đối với ví dụ được hỏi trong câu hỏi, bây giờ chúng ta phải:

  • tạo <your_package>/templates/ thành một gói thích hợp, bằng cách tạo một __init__.pytệp trống trong đó,
  • vì vậy bây giờ chúng ta có thể sử dụng một importcâu lệnh đơn giản (có thể tương đối) (không cần phân tích cú pháp tên gói / mô-đun nữa),
  • và chỉ cần yêu cầu resource_name = "temp_file"(không có đường dẫn).

Lời khuyên:

  • Để truy cập tệp bên trong mô-đun hiện tại, hãy đặt đối số gói thành __package__, ví dụ: pkg_resources.read_text(__package__, 'temp_file')(thanks to @ ben-mares).
  • Mọi thứ trở nên thú vị khi một tên tệp thực tế được hỏi path(), vì bây giờ trình quản lý ngữ cảnh được sử dụng cho các tệp được tạo tạm thời (đọc phần này ).
  • Thêm thư viện backported, có điều kiện cho Pythons cũ hơn, với install_requires=[" importlib_resources ; python_version<'3.7'"](kiểm tra điều này nếu bạn đóng gói dự án của mình với setuptools<36.2.1).
  • Hãy nhớ xóa setuptoolsthư viện khỏi yêu cầu thời gian chạy của bạn , nếu bạn đã di chuyển từ phương pháp truyền thống.
  • Hãy nhớ để tùy chỉnh setup.pyhoặc MANIFESTđể bao gồm bất kỳ tập tin tĩnh .
  • Bạn cũng có thể đặt zip_safe=Truetrong của bạn setup.py.

1
str.join lấy chuỗi resource_path = '/'.join(('templates', 'temp_file'))
Alex Punnen

Tôi tiếp tục nhận được NotImplementedError: Can't perform this operation for loaders without 'get_data()'bất kỳ ý tưởng?
leoschet

Lưu ý rằng importlib.resourcespkg_resourceskhông nhất thiết phải tương thích . importlib.resourceshoạt động với các tệp zip được thêm vào sys.path, các pkg_resourcescông cụ thiết lập và làm việc với các tệp trứng, là các tệp zip được lưu trữ trong một thư mục mà chính nó được thêm vào sys.path. Ví dụ: với sys.path = [..., '.../foo', '.../bar.zip'], trứng được nhập vào .../foo, nhưng các gói trong bar.zipcũng có thể được nhập. Bạn không thể sử dụng pkg_resourcesđể trích xuất dữ liệu từ các gói trong bar.zip. Tôi chưa kiểm tra xem setuptools có đăng ký trình tải cần thiết importlib.resourcesđể hoạt động với trứng hay không.
Martijn Pieters

Có cần thêm cấu hình setup.py nếu lỗi Package has no locationxuất hiện không?
zygimantus

1
Trong trường hợp bạn muốn truy cập tệp bên trong mô-đun hiện tại (chứ không phải mô-đun con như templatestrong ví dụ), thì bạn có thể đặt packageđối số thành __package__, ví dụpkg_resources.read_text(__package__, 'temp_file')
Ben Mares

42

Một khúc dạo đầu:

Trước khi bạn có thể lo lắng về việc đọc các tệp tài nguyên, bước đầu tiên là đảm bảo rằng các tệp dữ liệu đang được đóng gói vào bản phân phối của bạn ngay từ đầu - thật dễ dàng để đọc chúng trực tiếp từ cây nguồn, nhưng phần quan trọng là làm đảm bảo các tệp tài nguyên này có thể truy cập được từ mã trong gói đã cài đặt .

Cấu trúc dự án của bạn như thế này, đặt các tệp dữ liệu vào một thư mục con trong gói:

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

Bạn nên vượt qua include_package_data=Truetrong setup()cuộc gọi. Tệp kê khai chỉ cần thiết nếu bạn muốn sử dụng setuptools / distutils và xây dựng các bản phân phối nguồn. Để đảm bảo templates/temp_filecấu trúc dự án mẫu này được đóng gói, hãy thêm một dòng như thế này vào tệp kê khai:

recursive-include package *

Lưu ý cơ bản về lịch sử: Không cần sử dụng tệp kê khai cho các phần mềm phụ trợ xây dựng hiện đại như flit, thơ, tệp này sẽ bao gồm các tệp dữ liệu gói theo mặc định. Vì vậy, nếu bạn đang sử dụng pyproject.tomlvà bạn không có setup.pytệp thì bạn có thể bỏ qua tất cả nội dung về MANIFEST.in.

Bây giờ, với cách đóng gói, chuyển sang phần đọc ...

Sự giới thiệu:

Sử dụng các pkgutilAPI thư viện tiêu chuẩn . Nó sẽ trông như thế này trong mã thư viện:

# 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))

Nó hoạt động trong khóa kéo. Nó hoạt động trên Python 2 và Python 3. Nó không yêu cầu phụ thuộc của bên thứ ba. Tôi thực sự không biết về bất kỳ nhược điểm nào (nếu bạn có, hãy bình luận về câu trả lời).

Những cách xấu để tránh:

Cách xấu # 1: sử dụng đường dẫn tương đối từ tệp nguồn

Đây hiện là câu trả lời được chấp nhận. Tốt nhất, nó trông giống như sau:

from pathlib import Path

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

Có gì sai với điều đó? Giả định rằng bạn có sẵn tệp và thư mục con là không đúng. Cách tiếp cận này không hoạt động nếu thực thi mã được đóng gói trong zip hoặc bánh xe và nó có thể nằm ngoài tầm kiểm soát của người dùng cho dù gói của bạn có được giải nén vào hệ thống tệp hay không.

Cách kém # 2: sử dụng API pkg_resources

Điều này được mô tả trong câu trả lời được bình chọn nhiều nhất. Nó trông giống như sau:

from pkg_resources import resource_string

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

Có gì sai với điều đó? Nó cho biết thêm một thời gian chạy phụ thuộc vào setuptools , mà tốt nhất là nên có một cài đặt phụ thuộc thời gian mà thôi. Việc nhập và sử dụng pkg_resourcescó thể trở nên thực sự chậm, vì mã tạo ra một tập hợp hoạt động của tất cả các gói đã cài đặt, mặc dù bạn chỉ quan tâm đến tài nguyên gói của riêng mình . Đó không phải là vấn đề lớn tại thời điểm cài đặt (vì quá trình cài đặt chỉ diễn ra một lần), nhưng nó rất tệ khi chạy.

Cách tồi # 3: sử dụng API importlib.resources

Đây hiện là đề xuất trong câu trả lời được bình chọn nhiều nhất. Đó là một bổ sung thư viện tiêu chuẩn gần đây ( mới trong Python 3.7 ), nhưng cũng có sẵn một cổng hỗ trợ. Nó trông như thế này:

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))

Có gì sai với điều đó? Chà, thật không may, nó vẫn chưa hoạt động ... Đây vẫn là một API chưa hoàn chỉnh, việc sử dụng importlib.resourcessẽ yêu cầu bạn thêm một tệp trống templates/__init__.pyđể các tệp dữ liệu sẽ nằm trong một gói con chứ không phải trong một thư mục con. Nó cũng sẽ hiển thị package/templatesthư mục con như một package.templatesgói con có thể nhập theo đúng nghĩa của nó. Nếu đó không phải là vấn đề lớn và nó không làm phiền bạn, thì bạn có thể tiếp tục và thêm __init__.pytệp vào đó và sử dụng hệ thống nhập để truy cập tài nguyên. Tuy nhiên, trong khi ở đó, bạn cũng có thể tạo nó thành một my_resources.pytệp thay thế và chỉ cần xác định một số byte hoặc biến chuỗi trong mô-đun, sau đó nhập chúng bằng mã Python. Đó là hệ thống nhập khẩu đang thực hiện công việc nặng nhọc ở đây.

Dự án ví dụ:

Tôi đã tạo một dự án ví dụ trên github và tải lên trên PyPI , trình bày tất cả bốn cách tiếp cận được thảo luận ở trên. Hãy dùng thử với:

$ pip install resources-example
$ resources-example

Xem https://github.com/wimglenn/resources-example để biết thêm thông tin.


1
Nó đã được chỉnh sửa vào tháng 5 năm ngoái. Nhưng tôi đoán rất dễ bỏ lỡ những giải thích ở phần giới thiệu. Tuy nhiên, bạn khuyên mọi người đi ngược lại tiêu chuẩn - đó là một viên đạn khó cắn :-)
ankostis

1
@ankostis Thay vào đó, hãy để tôi chuyển câu hỏi cho bạn, tại sao bạn lại đề xuất importlib.resourcesbất chấp tất cả những thiếu sót này với một API chưa hoàn thiện đang chờ xử lý ? Mới hơn chưa chắc đã tốt hơn. Hãy cho tôi biết nó thực sự cung cấp những ưu điểm gì so với stdlib pkgutil, mà câu trả lời của bạn không đề cập đến?
wim

1
@Wim thân mến, phản hồi cuối cùng của Brett Canon về việc sử dụng pkgutil.get_data()cảm giác ruột của tôi đã xác nhận - đó là một API kém phát triển, sẽ không còn được dùng nữa. Điều đó nói rằng, tôi đồng ý với bạn, importlib.resourceskhông phải là một lựa chọn thay thế tốt hơn nhiều, nhưng cho đến khi PY3.10 giải quyết vấn đề này, tôi vẫn đứng trước sự lựa chọn này, heving đã học được rằng đó không chỉ là một "tiêu chuẩn" khác được các tài liệu khuyến nghị.
ankostis

1
@ankostis Tôi muốn nhận xét của Brett bằng một hạt muối. pkgutilhoàn toàn không được đề cập đến trong lịch trình ngừng sử dụng của PEP 594 - Xóa pin đã chết khỏi thư viện tiêu chuẩn và không có khả năng bị xóa mà không có lý do chính đáng. Nó đã xuất hiện từ Python 2.3 và được chỉ định là một phần của giao thức trình tải trong PEP 302 . Sử dụng "API chưa được xác định" không phải là một câu trả lời thuyết phục lắm, điều đó có thể mô tả phần lớn thư viện chuẩn Python!
wim

2
Hãy để tôi nói thêm: Tôi cũng muốn thấy tài nguyên importlib thành công! Tôi là tất cả cho các API được xác định chặt chẽ. Chỉ là trong tình trạng hiện tại, nó không thực sự được khuyến khích. API vẫn đang được thay đổi, nó không thể sử dụng được cho nhiều gói hiện có và chỉ khả dụng trong các bản phát hành Python tương đối gần đây. Trong thực tế, điều đó còn tồi tệ hơn pkgutilvề mọi mặt. "Cảm giác ruột" của bạn và khiếu nại đối với thẩm quyền là vô nghĩa đối với tôi, nếu có vấn đề với get_databộ nạp thì hãy đưa ra bằng chứng và ví dụ thực tế.
wim

15

Trong trường hợp bạn có cấu trúc này

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

bạn cần mã này:

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)

Phần kỳ lạ "luôn sử dụng dấu gạch chéo" đến từ setuptoolscác API

Cũng lưu ý rằng nếu bạn sử dụng đường dẫn, bạn phải sử dụng dấu gạch chéo lên (/) làm dấu phân cách đường dẫn, ngay cả khi bạn đang sử dụng Windows. Setuptools tự động chuyển đổi các dấu gạch chéo sang các dấu phân tách nền tảng cụ thể thích hợp tại thời điểm xây dựng

Trong trường hợp bạn thắc mắc tài liệu ở đâu:


Cảm ơn bạn vì câu trả lời ngắn gọn của bạn
Paolo

8

Nội dung trong "10.8. Đọc tệp dữ liệu trong một gói" của Sách dạy nấu ăn Python, Ấn bản thứ ba của David Beazley và Brian K. Jones đưa ra câu trả lời.

Tôi sẽ chỉ đưa nó đến đây:

Giả sử bạn có một gói với các tệp được tổ chức như sau:

mypackage/
    __init__.py
    somedata.dat
    spam.py

Bây giờ, giả sử tệp spam.py muốn đọc nội dung của tệp somedata.dat. Để làm điều đó, hãy sử dụng mã sau:

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

Dữ liệu biến kết quả sẽ là một chuỗi byte chứa nội dung thô của tệp.

Đối số đầu tiên của get_data () là một chuỗi chứa tên gói. Bạn có thể cung cấp trực tiếp hoặc sử dụng một biến đặc biệt, chẳng hạn như __package__. Đối số thứ hai là tên tương đối của tệp trong gói. Nếu cần, bạn có thể điều hướng vào các thư mục khác nhau bằng cách sử dụng các quy ước tên tệp Unix tiêu chuẩn miễn là thư mục cuối cùng vẫn nằm trong gói.

Bằng cách này, gói có thể được cài đặt dưới dạng thư mục, .zip hoặc .egg.



-2

giả sử bạn đang sử dụng tệp trứng; không được trích xuất:

Tôi đã "giải quyết" điều này trong một dự án gần đây, bằng cách sử dụng tập lệnh postinstall, trích xuất các mẫu của tôi từ trứng (tệp zip) vào thư mục thích hợp trong hệ thống tệp. Đó là giải pháp nhanh nhất, đáng tin cậy nhất mà tôi tìm thấy, vì làm việc với __path__[0]đôi khi có thể gặp sai sót (tôi không nhớ tên, nhưng tôi đã truy cập ít nhất một thư viện, đã thêm một thứ gì đó vào trước danh sách đó!).

Ngoài ra, các tệp trứng thường được trích xuất nhanh chóng đến một vị trí tạm thời được gọi là "bộ nhớ cache của trứng". Bạn có thể thay đổi vị trí đó bằng cách sử dụng một biến môi trường, trước khi bắt đầu tập lệnh của bạn hoặc thậm chí sau đó, ví dụ:

os.environ['PYTHON_EGG_CACHE'] = path

Tuy nhiên, có pkg_resources có thể thực hiện công việc đúng cách.

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.