Wie lese ich eine (statische) Datei aus einem Python-Paket?

106

Können Sie mir sagen, wie ich eine Datei lesen kann, die sich in meinem Python-Paket befindet?

Meine Situation

Ein Paket, das ich lade, enthält eine Reihe von Vorlagen (Textdateien, die als Zeichenfolgen verwendet werden), die ich aus dem Programm heraus laden möchte. Aber wie gebe ich den Pfad zu einer solchen Datei an?

Stellen Sie sich vor, ich möchte eine Datei lesen aus:

package\templates\temp_file

Eine Art Pfadmanipulation? Paketbasispfadverfolgung?

Ronszon
quelle
1
Mögliches Duplikat von Suchen einer Datei in einer Python-Modul-Distribution
Andreas Jung
Mögliches Duplikat der Python-Zugriffsdaten im Paket-
Unterverzeichnis

Antworten:

-12

[hinzugefügt am 15.06.2016: Anscheinend funktioniert dies nicht in allen Situationen. Bitte beachten Sie die anderen Antworten]


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

TLDR; Verwenden Sie das importlib.resourcesModul der Standardbibliothek, wie in der folgenden Methode Nr. 2 erläutert.

Das traditionelle pkg_resourcesvonsetuptools wird nicht mehr empfohlen, da die neue Methode:

  • es ist wesentlich performanter ;
  • Dies ist sicherer, da die Verwendung von Paketen (anstelle von Pfadstichen) Fehler bei der Kompilierung verursacht.
  • Es ist intuitiver, da Sie keine Pfade "verbinden" müssen.
  • Bei der Entwicklung ist es schneller, da Sie keine zusätzliche Abhängigkeit ( setuptools) benötigen , sondern sich nur auf die Standardbibliothek von Python verlassen.

Ich habe das traditionelle zuerst aufgelistet, um die Unterschiede mit der neuen Methode beim Portieren von vorhandenem Code zu erklären (Portierung auch hier erklärt ).



Angenommen, Ihre Vorlagen befinden sich in einem Ordner, der im Paket Ihres Moduls verschachtelt ist:

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

Hinweis 1: Auf jeden Fall sollten wir NICHT mit dem __file__Attribut herumspielen (z. B. wird der Code beim Servieren von einer Zip-Datei unterbrochen).

Hinweis 2: Wenn Sie dieses Paket erstellen, denken Sie daran, Ihre Datendateien als package_dataoderdata_files in Ihrem zu deklarieren setup.py.

1) Verwenden von pkg_resourcesfrom setuptools(langsam)

Sie können ein pkg_resourcesPaket aus der Setuptools- Distribution verwenden, dies ist jedoch in Bezug auf die Leistung mit Kosten verbunden :

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)

Tipps:

  • Dadurch werden Daten auch dann gelesen, wenn Ihre Distribution komprimiert ist. Sie können also zip_safe=TrueIhre festlegen setup.pyund / oder den lang erwarteten zipappPacker von Python-3.5 verwenden , um eigenständige Distributionen zu erstellen.

  • Denken Sie daran, setuptoolsIhre Laufzeitanforderungen zu ergänzen (z. B. in install_requires`).

... und beachten Sie, dass Sie laut Setuptools / pkg_resourcesdocs nicht verwenden sollten os.path.join:

Grundlegender Ressourcenzugriff

Beachten Sie, dass Ressourcennamen /getrennte Pfade sein müssen und nicht absolut (dh nicht führend /) sein dürfen oder relative Namen wie " .." enthalten dürfen. Verwenden Sie keineos.path Routinen zum Bearbeiten von Ressourcenpfaden, da es sich nicht um Dateisystempfade handelt.

2) Python> = 3.7 oder Verwendung der zurückportierten importlib_resourcesBibliothek

Verwenden Sie das importlib.resourcesModul der Standardbibliothek, das effizienter ist als setuptoolsoben:

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

Beachtung:

In Bezug auf die Funktion read_text(package, resource):

  • Das packagekann entweder ein String oder ein Modul sein.
  • Das resourceist kein Weg mehr, sondern nur der Dateiname der Ressource zu öffnen, innerhalb eines bestehenden Pakets; Es enthält möglicherweise keine Pfadtrennzeichen und keine Unterressourcen (dh es kann kein Verzeichnis sein).

Für das in der Frage gestellte Beispiel müssen wir jetzt:

  • Machen Sie das <your_package>/templates/ zu einem richtigen Paket, indem Sie eine leere __init__.pyDatei darin erstellen.
  • Jetzt können wir eine einfache (möglicherweise relative) importAnweisung verwenden (keine Analyse von Paket- / Modulnamen mehr).
  • und einfach nachfragen resource_name = "temp_file"(kein Weg).

Tipps:

  • Um auf eine Datei innerhalb des aktuellen Moduls zuzugreifen, setzen Sie das Paketargument auf __package__zB pkg_resources.read_text(__package__, 'temp_file')(dank @ ben-mares).
  • Interessant wird es, wenn nach einem tatsächlichen Dateinamen gefragt wird path(), da jetzt Kontextmanager für vorübergehend erstellte Dateien verwendet werden (lesen Sie dies ).
  • Fügen Sie die zurückportierte Bibliothek, bedingt für ältere Pythons, mit hinzu install_requires=[" importlib_resources ; python_version<'3.7'"](überprüfen Sie dies, wenn Sie Ihr Projekt mit verpacken setuptools<36.2.1).
  • Denken Sie daran, die setuptoolsBibliothek aus Ihren Laufzeitanforderungen zu entfernen , wenn Sie von der herkömmlichen Methode migriert haben.
  • Denken Sie daran anpassen setup.pyoder MANIFESTauf alle statischen Dateien enthalten .
  • Sie können auch zip_safe=Truein Ihrem einstellen setup.py.
Ankostis
quelle
1
str.join nimmt die Sequenz resource_path = '/'.join(('t'tates', 'temp_file'))
Alex Punnen
Ich bekomme immer wieder NotImplementedError: Can't perform this operation for loaders without 'get_data()'Ideen?
Leoschet
Beachten Sie, dass importlib.resourcesund pkg_resourcessind nicht unbedingt kompatibel . importlib.resourcesfunktioniert mit hinzugefügten sys.pathZip- pkg_resourcesDateien , Setuptools und mit Egg-Dateien, die Zip-Dateien sind, die in einem Verzeichnis gespeichert sind, das selbst hinzugefügt wird sys.path. ZB mit sys.path = [..., '.../foo', '.../bar.zip']gehen Eier rein .../foo, aber Pakete in bar.zipkönnen auch importiert werden. Sie können nicht verwenden pkg_resources, um Daten aus Paketen in zu extrahieren bar.zip. Ich habe nicht überprüft, ob setuptools den erforderlichen Lader für importlib.resourcesdie Arbeit mit Eiern registriert .
Martijn Pieters
Ist eine zusätzliche Konfiguration von setup.py erforderlich, wenn ein Fehler Package has no locationauftritt?
Zygimantus
1
Falls Sie eine Datei innerhalb des aktuellen Moduls zuzugreifen (und kein Submodul wie templatesnach dem Beispiel), dann können Sie das Set packageArgument __package__, zBpkg_resources.read_text(__package__, 'temp_file')
Ben Mares
42

Ein Verpackungsvorspiel:

Bevor Sie sich überhaupt um das Lesen von Ressourcendateien kümmern können, müssen Sie zunächst sicherstellen, dass die Datendateien überhaupt in Ihre Distribution gepackt werden. Es ist einfach, sie direkt aus dem Quellbaum zu lesen, aber der wichtige Teil ist das Erstellen Stellen Sie sicher, dass auf diese Ressourcendateien über Code in einem installierten Paket zugegriffen werden kann.

Strukturieren Sie Ihr Projekt folgendermaßen und fügen Sie Datendateien in ein Unterverzeichnis innerhalb des Pakets ein:

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

Sie sollten include_package_data=Trueden setup()Anruf weiterleiten. Die Manifestdatei wird nur benötigt, wenn Sie setuptools / distutils verwenden und Quelldistributionen erstellen möchten. templates/temp_fileFügen Sie der Manifestdatei eine Zeile wie diese hinzu, um sicherzustellen, dass das Paket für diese Beispielprojektstruktur gepackt wird:

recursive-include package *

Historischer Cruft-Hinweis: Die Verwendung einer Manifestdatei ist für moderne Build-Backends wie Flit oder Poetry nicht erforderlich , die standardmäßig die Paketdatendateien enthalten. Wenn Sie pyproject.tomlalso eine setup.pyDatei verwenden und keine haben , können Sie alles ignorieren MANIFEST.in.

Nun, mit der Verpackung aus dem Weg, auf den Leseteil ...

Empfehlung:

Verwenden Sie Standard-Bibliotheks- pkgutilAPIs. Im Bibliothekscode wird es so aussehen:

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

Es funktioniert in Reißverschlüssen. Es funktioniert unter Python 2 und Python 3. Es sind keine Abhängigkeiten von Drittanbietern erforderlich. Ich bin mir keiner Nachteile bewusst (wenn ja, dann kommentieren Sie bitte die Antwort).

Schlechte Möglichkeiten zu vermeiden:

Schlechter Weg Nr. 1: Verwenden relativer Pfade aus einer Quelldatei

Dies ist derzeit die akzeptierte Antwort. Bestenfalls sieht es ungefähr so ​​aus:

from pathlib import Path

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

Was stimmt damit nicht? Die Annahme, dass Dateien und Unterverzeichnisse verfügbar sind, ist nicht korrekt. Dieser Ansatz funktioniert nicht, wenn Code ausgeführt wird, der in einer Zip-Datei oder einem Rad gepackt ist, und es liegt möglicherweise völlig außerhalb der Kontrolle des Benutzers, ob Ihr Paket überhaupt in ein Dateisystem extrahiert wird oder nicht.

Schlechter Weg # 2: Verwenden von pkg_resources-APIs

Dies ist in der am besten bewerteten Antwort beschrieben. Es sieht ungefähr so ​​aus:

from pkg_resources import resource_string

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

Was stimmt damit nicht? Es wird eine Laufzeitabhängigkeit von setuptools hinzugefügt , die vorzugsweise nur eine Installationszeitabhängigkeit sein sollte. Das Importieren und Verwenden pkg_resourceskann sehr langsam werden, da der Code einen funktionierenden Satz aller installierten Pakete erstellt, obwohl Sie nur an Ihren eigenen Paketressourcen interessiert waren . Das ist zur Installationszeit keine große Sache (da die Installation einmalig ist), aber zur Laufzeit ist es hässlich.

Schlechter Weg Nr. 3: Verwenden der APIs importlib.resources

Dies ist derzeit die Empfehlung in der am besten bewerteten Antwort. Es handelt sich um eine neue Standardbibliothek ( neu in Python 3.7 ), es ist jedoch auch ein Backport verfügbar. Es sieht aus wie das:

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

Was stimmt damit nicht? Nun, leider funktioniert es nicht ... noch nicht. Dies ist immer noch eine unvollständige API. importlib.resourcesWenn Sie diese verwenden, müssen Sie eine leere Datei hinzufügen templates/__init__.py, damit sich die Datendateien in einem Unterpaket und nicht in einem Unterverzeichnis befinden. Das package/templatesUnterverzeichnis wird auch als package.templateseigenständiges importierbares Unterpaket verfügbar gemacht . Wenn das keine große Sache ist und Sie nicht stört, können Sie die __init__.pyDatei dort hinzufügen und über das Importsystem auf Ressourcen zugreifen. Wenn Sie schon dabei sind, können Sie es auch zu einer my_resources.pyDatei machen und einfach einige Bytes oder String-Variablen im Modul definieren und sie dann in Python-Code importieren. Es ist das Importsystem, das hier so oder so das schwere Heben erledigt.

Beispielprojekt:

Ich habe ein Beispielprojekt auf erstellt Github und hochgeladen auf PyPI , die alle vier demonstriert oben Ansätze diskutiert. Probieren Sie es aus mit:

$ pip install resources-example
$ resources-example

Weitere Informationen finden Sie unter https://github.com/wimglenn/resources-example .

wim
quelle
1
Es wurde im letzten Mai bearbeitet. Aber ich denke, es ist leicht, die Erklärungen im Intro zu übersehen. Trotzdem raten Sie den Leuten vom Standard ab - das ist schwer zu beißen :-)
ankostis
1
@ankostis Lassen Sie mich stattdessen die Frage auf Sie richten, warum würden Sie importlib.resourcestrotz all dieser Mängel eine unvollständige API empfehlen, deren Verfall bereits aussteht ? Neu ist nicht unbedingt besser. Sagen Sie mir, welche Vorteile es tatsächlich gegenüber dem stdlib pkgutil bietet, über das Ihre Antwort keine Erwähnung findet?
wim
1
Lieber @wim, Brett Canons letzte Antwort auf die Verwendung von hat pkgutil.get_data()mein Bauchgefühl bestätigt - es ist eine unterentwickelte, veraltete API. Das heißt, ich stimme Ihnen zu, importlib.resourcesist keine viel bessere Alternative, aber bis PY3.10 dies löst, stehe ich zu dieser Wahl und habe erfahren, dass es nicht nur ein weiterer "Standard" ist, der von den Dokumenten empfohlen wird.
Ankostis
1
@ankostis Ich würde Bretts Kommentare mit einem Körnchen Salz nehmen. pkgutilwird im Verfallsplan von PEP 594 - Entfernen leerer Batterien aus der Standardbibliothek überhaupt nicht erwähnt und wird wahrscheinlich nicht ohne guten Grund entfernt. Es gibt es seit Python 2.3 und es wurde als Teil des Loader-Protokolls in PEP 302 angegeben . Die Verwendung einer "unterdefinierten API" ist keine sehr überzeugende Antwort, die den Großteil der Python-Standardbibliothek beschreiben könnte!
wim
2
Lassen Sie mich hinzufügen: Ich möchte, dass importlib-Ressourcen auch erfolgreich sind! Ich bin alles für streng definierte APIs. Es ist nur so, dass es in seinem gegenwärtigen Zustand nicht wirklich empfohlen werden kann. Die API wird noch geändert, ist für viele vorhandene Pakete unbrauchbar und nur in relativ neuen Python-Versionen verfügbar. In der Praxis ist es schlimmer als pkgutilin jeder Hinsicht. Ihr "Bauchgefühl" und Ihr Appell an die Autorität sind für mich bedeutungslos. Wenn es Probleme mit get_dataLadern gibt, zeigen Sie Beweise und praktische Beispiele.
wim
15

Falls Sie diese Struktur haben

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

Sie benötigen diesen Code:

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)

Der seltsame Teil "Schrägstrich immer verwenden" stammt von setuptoolsAPIs

Beachten Sie außerdem, dass Sie bei Verwendung von Pfaden einen Schrägstrich (/) als Pfadtrennzeichen verwenden müssen, auch wenn Sie unter Windows arbeiten. Setuptools konvertiert Schrägstriche beim Erstellen automatisch in geeignete plattformspezifische Trennzeichen

Falls Sie sich fragen, wo sich die Dokumentation befindet:

Martin Thoma
quelle
Vielen Dank für Ihre kurze Antwort
Paolo
8

Der Inhalt in "10.8. Lesen von Datendateien in einem Paket" des Python-Kochbuchs, dritte Ausgabe von David Beazley und Brian K. Jones mit den Antworten.

Ich werde es einfach hierher bringen:

Angenommen, Sie haben ein Paket mit Dateien, die wie folgt organisiert sind:

mypackage/
    __init__.py
    somedata.dat
    spam.py

Angenommen, die Datei spam.py möchte den Inhalt der Datei somedata.dat lesen. Verwenden Sie dazu den folgenden Code:

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

Die resultierenden Variablendaten sind eine Bytezeichenfolge, die den Rohinhalt der Datei enthält.

Das erste Argument für get_data () ist eine Zeichenfolge, die den Paketnamen enthält. Sie können es entweder direkt angeben oder eine spezielle Variable verwenden, z __package__. Das zweite Argument ist der relative Name der Datei im Paket. Bei Bedarf können Sie mithilfe der Standardkonventionen für Unix-Dateinamen in verschiedene Verzeichnisse navigieren, solange sich das endgültige Verzeichnis noch im Paket befindet.

Auf diese Weise kann das Paket als Verzeichnis, .zip oder .egg installiert werden.

Chaokunyang
quelle
0

Jedes Python-Modul in Ihrem Paket hat ein __file__Attribut

Sie können es verwenden als:

import os 
from mypackage

templates_dir = os.path.join(os.path.dirname(mypackage.__file__), 'templates')
template_file = os.path.join(templates_dir, 'template.txt')

Informationen zu Eiressourcen finden Sie unter: http://peak.telecommunity.com/DevCenter/PythonEggs#accessing-package-resources

Zaur Nasibov
quelle
-2

Angenommen, Sie verwenden eine Eidatei. nicht extrahiert:

Ich habe dies in einem kürzlich durchgeführten Projekt mithilfe eines Postinstall-Skripts "gelöst", das meine Vorlagen aus dem Ei (Zip-Datei) in das richtige Verzeichnis im Dateisystem extrahiert. Es war die schnellste und zuverlässigste Lösung, die ich gefunden habe, da die Arbeit mit __path__[0]manchmal schief gehen kann (ich erinnere mich nicht an den Namen, aber ich bin auf mindestens eine Bibliothek gestoßen, die etwas vor dieser Liste hinzugefügt hat!).

Auch Eidateien werden normalerweise im laufenden Betrieb an einen temporären Speicherort extrahiert, der als "Eiercache" bezeichnet wird. Sie können diesen Speicherort mithilfe einer Umgebungsvariablen ändern, entweder bevor Sie Ihr Skript starten oder sogar später, z.

os.environ['PYTHON_EGG_CACHE'] = path

Es gibt jedoch pkg_resources , die den Job möglicherweise ordnungsgemäß ausführen .

Florian
quelle