Python: Wie kopiert man eine Datei?
shutil bietet dafür vier Funktionen, und sie unterscheiden sich darin, was
sie mitnehmen. Seit Python 3.14 gibt es zusätzlich eine Methode in pathlib —
die ist neuer als jede Antwort, die man dazu findet.
Zuerst die neue
from pathlib import Path
Path("quelle.txt").copy("ziel.txt")
Path.copy() kam mit Python
3.14, zusammen mit Path.copy_into() und Path.move(). Davor konnte
pathlib mit Pfaden fast alles außer kopieren — deshalb greift jede ältere
Antwort zu shutil.
Beachte die Voreinstellung:
src.copy(target) : -rw-r--r-- mtime=<now>
copy(preserve_metadata=True) : -rwxr-x--- mtime=<original>
Path.copy() kopiert nur den Inhalt, solange du nicht mehr verlangst. Es
verhält sich also wie shutil.copyfile und nicht wie copy2.
Und um in ein Verzeichnis statt auf einen Namen zu kopieren:
src.copy_into(a_directory) : created True
Wenn du auf 3.13 oder älter bist — was realistischerweise die meisten Projekte sind — lies weiter.
Die vier shutil-Funktionen
Der Unterschied liegt darin, was sie erhalten. Die Quelle ist hier
-rwxr-x--- mit einer mtime von gestern:
source : -rwxr-x--- mtime=1788078255
copyfile -> -rw-r--r-- mtime=1788164655
copy -> -rwxr-x--- mtime=1788164655
copy2 -> -rwxr-x--- mtime=1788078255
copyfile— der Inhalt. Das Ziel bekommt frische Rechte aus deiner umask.copy— Inhalt und Zugriffsrechte.copy2— Inhalt, Rechte und Zeitstempel.
copy2 kommt cp -p am nächsten und ist die Wahl, wenn die Kopie das Original
vertreten soll — eine Sicherung, ein vorbereitetes Deployment, alles wo später
ein Werkzeug auf die mtime schaut.
Der Unterschied, bei dem es nicht um Metadaten geht
Dieser erwischt einen häufiger:
shutil.copy(src, a_directory) : created True
shutil.copyfile(src, a_directory) -> IsADirectoryError: [Errno 21] Is a directory
copy und copy2 nehmen ein Verzeichnis als Ziel entgegen und behalten
den ursprünglichen Dateinamen. copyfile verlangt einen vollständigen
Zielpfad.
shutil.copy(f, "/backup/") funktioniert also und
shutil.copyfile(f, "/backup/") nicht — unabhängig davon, was du erhalten
wolltest.
Eine Datei auf sich selbst kopieren
copyfile(src, src) -> SameFileError: ... are the same file
SameFileError
ist eine Unterklasse von OSError, du kannst also gezielt diesen Fall
abfangen. Das ist wissenswert, wenn du Quelle und Ziel getrennt zusammenbaust
und sie zusammenfallen könnten — ohne die Prüfung würdest du die Datei
abschneiden, bevor du sie liest.
Was copy2 trotzdem nicht kopiert
copy2 verwendet copystat, das Modus, Zeitstempel und, wo unterstützt, Flags
und erweiterte Attribute abdeckt. Den Eigentümer deckt es nicht ab.
Kopierst du eine Datei, die jemand anderem gehört, gehört dir die Kopie. Das zu
ändern braucht os.chown und in der Praxis root. Wenn du als Dienstkonto
Dateien umherschiebst und der Eigentümer eine Rolle spielt, ist das ein
Schritt, den du selbst schreiben musst.
Ganze Verzeichnisse
copytree : True
again -> FileExistsError
with dirs_exist_ok=True : fine (added in 3.8)
copytree
verweigert ein bereits vorhandenes Ziel. dirs_exist_ok=True fügt
hinein. Es nimmt außerdem ein ignore= entgegen, wofür
shutil.ignore_patterns den üblichen Fall abdeckt:
shutil.copytree(quelle, ziel, ignore=shutil.ignore_patterns("*.pyc", "__pycache__"))
Wenn du Dateiobjekte statt Pfade hast
with open(quelle, "rb") as fsrc, open(ziel, "wb") as fdst:
shutil.copyfileobj(fsrc, fdst)
copyfileobj : 'hello\n'
copyfileobj
arbeitet mit allem Dateiähnlichen und ist damit die Wahl für den Body einer
HTTP-Antwort, einen Upload-Strom oder ein Element aus einem tar- oder
zip-Archiv. Es kopiert in Blöcken, die Dateigröße wird also nicht zu deinem
Speicherverbrauch.
Was man nicht tun sollte
Der Einzeiler, der in älterem Code auftaucht:
open(ziel, "w").write(open(quelle).read())
Drei Probleme. Er liest die ganze Datei in den Speicher. Er lässt beide Dateihandles offen. Und im Textmodus dekodiert und kodiert er neu, was Zeilenenden umschreibt und alles zerlegt, was kein Text ist:
reading a binary file as text -> UnicodeDecodeError: 'utf-8' codec can't decode byte 0xc3 ...
Hier hat es geworfen, und das ist der gute Ausgang. Bei einer Datei, die sich zufällig sauber dekodieren lässt, wäre es durchgelaufen und hätte dir stillschweigend eine andere Datei hinterlassen.
Ein Grund, shutil dem Selbstbau vorzuziehen
8 MiB, shutil.copyfile : 1.8 ms
8 MiB, manual 64K chunks : 2.1 ms
Seit Python 3.8 verwendet shutil die Zero-Copy-Aufrufe der jeweiligen
Plattform, wo es sie gibt — os.sendfile und copy_file_range unter Linux,
fcopyfile unter macOS. Die Daten müssen dabei gar nicht durch deinen Prozess.
Der Abstand oben ist klein, weil die Datei im Page Cache lag und das
Container-Dateisystem ein Overlay ist. Der Punkt ist nicht, dass shutil
dramatisch schneller wäre — sondern dass eine selbst geschriebene Blockschleife
nicht schneller sein kann und mehr Möglichkeiten hat, falsch zu sein.
Hinweis zu Netcup (Werbung)
Der deutsche Hoster Netcup bietet unter anderem günstige und zugleich leistungsstarke Webhosting Pakete, KVM-basierte Root Server und dezidierte Server an. Mit unseren Gutscheincodes kannst du noch mehr Geld sparen (6€ bei deiner ersten Bestellung, 30% Rabatt auf alle KVM-basierten Root Server, ...).