ZIP-File packen

Wenn du dir nicht sicher bist, in welchem der anderen Foren du die Frage stellen sollst, dann bist du hier im Forum für allgemeine Fragen sicher richtig.
Antworten
DB-Hopf
User
Beiträge: 7
Registriert: Dienstag 17. Januar 2023, 09:41

Liebes Python-Forum,

ich habe folgendes Problem, bei dem ich die Lösung noch nicht gefunden habe. Der Inhalt eines "Source-Ordner" soll in eine oder mehrere voneinander unabhängige ZIP-Dateien gepackt und im "Target-Ordner" gespeichert werden. Im Netz hatte ich ein Python-Skript gefunden, dass ich mit meinen begrenzten Fähigkeiten bezüglich "zipfilesname" angepasst. Das Problem ist, dass die Struktur in der erstellten ZIP-Datei so nicht gewünscht ist. Im Beispiel ist die Ordnerebene "Source" in der per Python-Skript erstellten ZIP-Datei nicht erwünscht.

Fallbeispiel:
  • Ordnerstruktur des Source-Ordners "D:\Test\Source":
    • D:\Test\Source\Ordner A\Test 1.txt
    • D:\Test\Source\Ordner A\Test 2.txt
    • D:\Test\Source\Ordner A\Test 3.txt
    • D:\Test\Source\Ordner B\Test 1.txt
    • D:\Test\Source\Ordner B\Test 2.txt
    • D:\Test\Source\Ordner B\Test 3.txt
  • Ordnerstruktur des Target-Ordners "D:\Test\Target":
    • D:\Test\Target\ZipFileName Teil 1.zip
  • Struktur in der ZIP-Datei "D:\Test\Target\ZipFileName Teil 1.zip":
    • Source\Ordner A\Test 1.txt
    • Source\Ordner A\Test 2.txt
    • Source\Ordner A\Test 3.txt
    • Source\Ordner B\Test 1.txt
    • Source\Ordner B\Test 2.txt
    • Source\Ordner B\Test 3.txt
  • gewünschte Struktur in der ZIP-Datei "D:\Test\Target\Wunsch ZipFileName Teil 1.zip":
    • Ordner A\Test 1.txt
    • Ordner A\Test 2.txt
    • Ordner A\Test 3.txt
    • Ordner B\Test 1.txt
    • Ordner B\Test 2.txt
    • Ordner B\Test 3.txt

Code: Alles auswählen

import os
import zipfile

path = "D:\Test\Source"
storage_folder = 'D:\Test\Target'
filesize = 10
zipfilesname = "ZipFileName"

def zip_folder(storage_folder,path, max_filesize=100):
    files_written = 1 # file counter
    zip_name = f"{storage_folder}\{zipfilesname}{files_written}.zip"
    zipWriter = zipfile.ZipFile(zip_name, 'w', zipfile.ZIP_DEFLATED)
    zip_names = [f"{files_written}.zip"]
    for root, dirs, files in os.walk(path):
        for file in files:
            zipWriter.write(os.path.join(root, file), 
                       os.path.relpath(os.path.join(root, file), 
                                       os.path.join(path, '..')))
            
            # current zip size
            current_size = os.path.getsize(zip_name) >> 20
            if current_size > max_filesize:
                zipWriter.close()
                files_written += 1
                zip_name = f"{storage_folder}\{zipfilesname}{files_written}.zip"
                zip_names.append(zip_name)
                
                zipWriter = zipfile.ZipFile(zip_name, 'w', zipfile.ZIP_DEFLATED)
    zipWriter.close()
    return zip_names

zip_folder(storage_folder,path,filesize)
  • Kann jemand den Grund für das beschriebene Problem im Python-Skript sehen und beheben?
  • Wie könnte ich mir die gefundenen Dateien und deren Pfade in der Schleife ausgeben lassen? Um das Problem zu identifizieren.
Ich danke schon einmal für die Mithilfe.
Sirius3
User
Beiträge: 18225
Registriert: Sonntag 21. Oktober 2012, 17:20

\ ist ein Escape-Zeichen im Pythoncode, das darf also so nicht in der Datei verwendet werden. Am einfachsten ist es immer / als Pfadtrennung zu benutzen.
os.walk und os.path sind veraltet, heutzutage benutzt man pathlib.
Pfade setzt man nicht per Stringformatierung zusammen, sondern benutzt pathlib (oder os.path.join, das Du aber scheinbar nur sehr selektiv nutzt).
zip_name erzeugst Du an verschiedenen Stellen im Code, das sollte nur einmal im Code stehen.
So lange die Zip-Datei offen ist, ist die Dateigröße nicht korrekt.
Variablennamen schreibt man generell komplett_klein, Konstanten dagegen KOMPLETT_GROSS.

Und zu Deinem Problem:
Die Pfade sind relativ zum Test-Ordner und nicht zum Source-Ordner, weil Du das so wünschst, wenn das nicht der Fall sein soll, dann mach das einfach nicht.
Die gefundenen Dateien und deren Pfade kann man sich mit print ausgeben lassen.

Das selbe mit pathlib (ungetestet):

Code: Alles auswählen

from pathlib import Path
import zipfile

SOURCE_PATH = Path("D:/Test/Source")
STORAGE_PATH = Path('D:/Test/Target')
FILE_SIZE_IN_MB = 10
ZIPFILENAME = "ZipFileName{}.zip"

def zip_folder(storage_folder, source_path, max_filesize_in_mb=100):
    zip_names = []
    zip_writer = None
    for path in source_path.rglob('*'):
        if path.is_file():
            if zip_writer is None:
                filename = storage_folder / ZIPFILENAME.format(len(zip_names) + 1)
                zip_names.append(filename)
                zip_writer = zipfile.ZipFile(filename, "w", zipfile.ZIP_DEFLATED)
            zip_writer.write(path, path.relative_to(source_path.parent))
            if filename.stat().st_size >> 20 > max_filesize_in_mb:
                zip_writer.close()
                zip_writer = None
    if zip_writer is not None:
        zip_writer.close()
    return zip_names

def main():
    zip_folder(STORAGE_PATH, SOURCE_PATH, FILE_SIZE_IN_MB)

if __name__ == "__main__":
    main()
DB-Hopf
User
Beiträge: 7
Registriert: Dienstag 17. Januar 2023, 09:41

Hallo Sirius3,

schon einmal ganz herzlichen Dank für die Verbesserungen sowie die kurzen Erläuterungen. Ich versuche mich daran zu halten, bis es wieder falsch mache. :-)

Ich habe es mal am Fallbeispiel getestet und kann die grundsätzliche Funktionalität (Erstelle ZIP-Datei im Target und trenne nach Überschreitung der Dateigröße auf) bestätigen.

Mit dem CODE

Code: Alles auswählen

print("path=",path, "path.relative_to=",path.relative_to(source_path.parent))
habe ich mit den "Dateipfad" sowie "relativen" Pfad mit folgenden Ergebnis ausgegeben:

"path= D:\Test\Source\Ordner A\Test 1.txt path.relative_to= Source\Ordner A\Test 1.txt"

Struktur in der ZIP-Datei "D:\Test\Target\ZipFileName_forum1.zip":
Source\Ordner A\Test 1.txt

"Die Pfade sind relativ zum Test-Ordner und nicht zum Source-Ordner, weil Du das so wünschst, wenn das nicht der Fall sein soll, dann mach das einfach nicht."

1. Wie mache ich den die Pfade relativ zum Source-Ordner?
2. Zum Verständnis:
  • Der ZIP-Filename "ZipFileName_forum{}.zip" wird an der Stelle "()" die laufende Nummer des ZIP-Paketes geschrieben? Können wir statt "()" den String "<ZIPNR>" ändern?
  • Das Python-Teilmodul "Path" aus "pathlib" konvertiert den Pfadstring "D:/Test/Source" in eine OS-konforme Pfadstring?
3. Der Vollständigkeit halber: Das beschriebene Problem besteht weiterhin. Die Struktur in der erstellten ZIP-Datei aktuelle unverändert.

aktuelle Version des Codes:

Code: Alles auswählen

from pathlib import Path
import zipfile

SOURCE_PATH = Path("D:/Test/Source")
STORAGE_PATH = Path('D:/Test/Target')
FILE_SIZE_IN_MB = 10
ZIPFILENAME = "ZipFileName_forum{}.zip"

def zip_folder(storage_folder, source_path, max_filesize_in_mb=100):
    zip_names = []
    zip_writer = None
    for path in source_path.rglob('*'):
        if path.is_file():
            if zip_writer is None:
                filename = storage_folder / ZIPFILENAME.format(len(zip_names) + 1)
                zip_names.append(filename)
                zip_writer = zipfile.ZipFile(filename, "w", zipfile.ZIP_DEFLATED)
            zip_writer.write(path, path.relative_to(source_path.parent))
            print("path=",path, "path.relative_to=",path.relative_to(source_path.parent))
            if filename.stat().st_size >> 20 > max_filesize_in_mb:
                zip_writer.close()
                zip_writer = None
    if zip_writer is not None:
        zip_writer.close()
    return zip_names

def main():
    zip_folder(STORAGE_PATH, SOURCE_PATH, FILE_SIZE_IN_MB)

if __name__ == "__main__":
    main()
Sirius3
User
Beiträge: 18225
Registriert: Sonntag 21. Oktober 2012, 17:20

Natürlich besteht das Problem weiterhin, weil ich Deinen Code, mit Deinem aktuellen Vorgehen nicht verändert habe. Der Punkt ist, wie Du relative_to, bzw. os.path.relpath verwendest, entspricht nicht dem, was Du möchtest. Und das zu ändern ist trivial, wenn Du verstanden hast, was diese Funktion macht.
DB-Hopf
User
Beiträge: 7
Registriert: Dienstag 17. Januar 2023, 09:41

Hallo Sirius3,

ich wollte mich noch für das "Schubsen" in die richtige Richtung bedanken.

alt

Code: Alles auswählen

zip_writer.write(path, path.relative_to(source_path.parent))
neu

Code: Alles auswählen

zip_writer.write(path, path.relative_to(source_path))
aktueller Code:

Code: Alles auswählen

from pathlib import Path
import zipfile

SOURCE_PATH = Path("D:/Test/Source")
STORAGE_PATH = Path('D:/Test/Target')
FILE_SIZE_IN_MB = 10
ZIPFILENAME = "ZipFileName_forum {}.zip"

def zip_folder(storage_folder, source_path, max_filesize_in_mb=100):
    zip_names = []
    zip_writer = None
    for path in source_path.rglob('*'):
        if path.is_file():
            if zip_writer is None:
                filename = storage_folder / ZIPFILENAME.format(len(zip_names)+1)
                zip_names.append(filename)
                zip_writer = zipfile.ZipFile(filename, "w", zipfile.ZIP_DEFLATED)
            zip_writer.write(path, path.relative_to(source_path))
            if filename.stat().st_size >> 20 > max_filesize_in_mb:
                zip_writer.close()
                zip_writer = None
    if zip_writer is not None:
        zip_writer.close()
    return zip_names

def main():
    zip_folder(STORAGE_PATH, SOURCE_PATH, FILE_SIZE_IN_MB)

if __name__ == "__main__":
    main()
Benutzeravatar
__blackjack__
User
Beiträge: 13931
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

@DB-Hopf: Anmerkungen: Die Zahl im Dateinamen würde ich mit 0en auffüllen, damit eine lexikographische Sortierung der Namen auch der entsprechenden Reihenfolge entspricht und nicht 10 vor 2 kommt.

Die Beobachtung von Sirius3 bezüglich der Dateigrösse würde ich als Kommentar dokumentieren. Nicht das sich am Ende jemand über Archivdateien wundert die grösser als `max_filesize_in_mb` sind. Wobei dieser Name sowieso irreführend ist, denn der Test greift ja erst wenn diese Grösse durch eine hinzugefügte Datei überschritten wurde.

Das schliessen am Ende sollte mit einem ``try``/``finally`` abgesichert werden.

Zwischenstand (ungetestet):

Code: Alles auswählen

#!/usr/bin/env python3
from pathlib import Path
from zipfile import ZIP_DEFLATED, ZipFile

SOURCE_PATH = Path("D:/Test/Source")
STORAGE_PATH = Path("D:/Test/Target")
FILE_SIZE_IN_MB = 10
ZIP_FILENAME_TEMPLATE = "ZipFileName_forum {:04d}.zip"


def zip_folder(storage_folder, source_path, max_filesize_in_mb=100):
    zip_names = []
    zip_file = None
    try:
        for path in source_path.rglob("*"):
            if path.is_file():
                if zip_file is None:
                    filename = storage_folder / ZIP_FILENAME_TEMPLATE.format(
                        len(zip_names) + 1
                    )
                    zip_names.append(filename)
                    zip_file = ZipFile(filename, "w", ZIP_DEFLATED)

                zip_file.write(path, path.relative_to(source_path))
                #
                # ATTENTION The current file size doesn't include the size of
                #   the ZIP directory structure which is only written when
                #   closing the archive!
                #
                if filename.stat().st_size >> 20 > max_filesize_in_mb:
                    zip_file.close()
                    zip_file = None
    
    finally:
        if zip_file is not None:
            zip_file.close()

    return zip_names


def main():
    zip_folder(STORAGE_PATH, SOURCE_PATH, FILE_SIZE_IN_MB)


if __name__ == "__main__":
    main()
“Java is a DSL to transform big Xml documents into long exception stack traces.”
— Scott Bellware
DB-Hopf
User
Beiträge: 7
Registriert: Dienstag 17. Januar 2023, 09:41

Hallo __blackjack__,
hallo Sirius3,

ich möchte mich für eure Mithilfe bedanken. Das hätte ich auf keinen Fall in der Zeit so hinbekommen.

Eine "Eigenschaft" wird noch gewünscht:

Die nächste ZIP-FILE-Nummer des Archives soll erst nach der Verarbeitung des gesamten Unterordner erfolgen. Ordner der aktuellen Datei "Source\Ordner A\" ungleich Ordner der nächsten Datei "Source\Ordner B\". Ich bekomme aus

Code: Alles auswählen

path.relative_to(source_path)
nicht "Ordner A"

Der Versuch:

Code: Alles auswählen

(path.relative_to(source_path)).rstrip("\\")
war nicht erfolgreich.

Wie kann ich den Ordner der aktuellen Datei und den Ordner der nächsten Datei ausgeben?

Code: Alles auswählen

            if filename.stat().st_size >> 20 > max_filesize_in_mb and "Ordner der aktuellen Datei" != "Ordner der nächsten Datei":
                zip_writer.close()
                zip_writer = None
Sachverhalt:
aktuelle Struktur in der ZIP-Datei
  • "D:\Test\Target\ZipFileName Teil 1.zip" (10 MB):
    • Source\Ordner A\Test 1.txt
    • Source\Ordner A\Test 2.txt
    • Source\Ordner A\Test 3.txt
    • Source\Ordner B\Test 1.txt
    • Source\Ordner B\Test 2.txt
  • "D:\Test\Target\ZipFileName Teil 2.zip" (3 MB):
    • Source\Ordner B\Test 3.txt
    • Source\Ordner C\Test 1.txt
    • Source\Ordner C\Test 2.txt
    • Source\Ordner C\Test 3.txt
gewünschte Struktur in der ZIP-Datei
  • "D:\Test\Target\ZipFileName Teil 1.zip" (11 MB):
    • Source\Ordner A\Test 1.txt
    • Source\Ordner A\Test 2.txt
    • Source\Ordner A\Test 3.txt
    • Source\Ordner B\Test 1.txt
    • Source\Ordner B\Test 2.txt
    • Source\Ordner B\Test 3.txt
  • "D:\Test\Target\ZipFileName Teil 2.zip" (2 MB):
    • Source\Ordner C\Test 1.txt
    • Source\Ordner C\Test 2.txt
    • Source\Ordner C\Test 3.txt
Sirius3
User
Beiträge: 18225
Registriert: Sonntag 21. Oktober 2012, 17:20

Jetzt ist wieder Source\ Teil der Zip-Namen???
Und welche Unterordnerstruktur ist gemeint? Bis zu welcher Ebene?
Raten ist selten in sinnvolles Vorgehen beim Programmieren. Dokumentation-Lesen hilft dagegen.
DB-Hopf
User
Beiträge: 7
Registriert: Dienstag 17. Januar 2023, 09:41

aktuell verwendeter und getesteter Code:

Code: Alles auswählen

#!/usr/bin/env python3
from pathlib import Path
from zipfile import ZIP_DEFLATED, ZipFile

SOURCE_PATH = Path("D:/Test/Source")
STORAGE_PATH = Path("D:/Test/Target")
FILE_SIZE_IN_MB = 10
ZIP_FILENAME_TEMPLATE = "ZipFileName_forum {:04d}.zip"


def zip_folder(storage_folder, source_path, max_filesize_in_mb=100):
    zip_names = []
    zip_file = None
    try:
        for path in source_path.rglob("*"):
            if path.is_file():
                if zip_file is None:
                    filename = storage_folder / ZIP_FILENAME_TEMPLATE.format(
                        len(zip_names) + 1
                    )
                    zip_names.append(filename)
                    zip_file = ZipFile(filename, "w", ZIP_DEFLATED)

                zip_file.write(path, path.relative_to(source_path))
                print(path, path.relative_to(source_path))
                #
                # ATTENTION The current file size doesn't include the size of
                #   the ZIP directory structure which is only written when
                #   closing the archive!
                #
                if filename.stat().st_size >> 20 > max_filesize_in_mb:
                    zip_file.close()
                    zip_file = None
    
    finally:
        if zip_file is not None:
            zip_file.close()

    return zip_names


def main():
    zip_folder(STORAGE_PATH, SOURCE_PATH, FILE_SIZE_IN_MB)


if __name__ == "__main__":
    main()
Jetzt ist wieder Source\ Teil der Zip-Namen???"
Wenn ich mir mit dem Code

Code: Alles auswählen

print(path, path.relative_to(source_path))
die Angaben zum Ordner ausgeben lasse, erhalte ich für:
  • path : ""D:\Test\Source\Ordner A\Test 1.txt" und
  • path.relative_to(source_path) : "Ordner A\Test 1.txt""
Daher "Source\" wird nicht als Teil der ZIP-Names oder Struktur verwendet.
Und welche Unterordnerstruktur ist gemeint? Bis zu welcher Ebene?
  • "Ebene 1\Ebene 2\File 1.txt""
Beispiel:
  • "Bundesland A\Region A\file 1.txt""
Im entsprechenden Anwendungsbeispiel möchten wir die "Bundesländer" nicht in unterschiedliche ZIP-Pakete aufteilen.
  • "Bundesland A\Region A\file 1.txt", "ZipFileName_forum 0001.zip"
  • "Bundesland A\Region A\file 2.txt", "ZipFileName_forum 0001.zip"
  • "Bundesland A\Region A\file 3.txt", "ZipFileName_forum 0001.zip"
  • "Bundesland A\Region B\file 1.txt", "ZipFileName_forum 0001.zip"
  • "Bundesland A\Region B\file 2.txt", "ZipFileName_forum 0001.zip" "Überschreitung ZIP-FILE-LIMIT"
  • "Bundesland A\Region B\file 3.txt", "ZipFileName_forum 0001.zip"
  • "Bundesland B\Region A\file 1.txt", "ZipFileName_forum 0002.zip"
  • "Bundesland B\Region A\file 2.txt", "ZipFileName_forum 0002.zip" "Überschreitung ZIP-FILE-LIMIT"
  • "Bundesland B\Region A\file 3.txt", "ZipFileName_forum 0002.zip"
  • "Bundesland B\Region B\file 1.txt", "ZipFileName_forum 0002.zip"
  • "Bundesland B\Region B\file 2.txt", "ZipFileName_forum 0002.zip"
  • "Bundesland B\Region B\file 3.txt", "ZipFileName_forum 0002.zip"
In einem anderen Anwendungsbeispiel möchten wir die "Region" nicht in unterschiedliche ZIP-Pakete aufteilen.
  • "Bundesland A\Region A\file 1.txt", "ZipFileName_forum 0001.zip"
  • "Bundesland A\Region A\file 2.txt", "ZipFileName_forum 0001.zip"
  • "Bundesland A\Region A\file 3.txt", "ZipFileName_forum 0001.zip"
  • "Bundesland A\Region B\file 1.txt", "ZipFileName_forum 0001.zip"
  • "Bundesland A\Region B\file 2.txt", "ZipFileName_forum 0001.zip" "Überschreitung ZIP-FILE-LIMIT"
  • "Bundesland A\Region B\file 3.txt", "ZipFileName_forum 0001.zip"
  • "Bundesland B\Region A\file 1.txt", "ZipFileName_forum 0002.zip"
  • "Bundesland B\Region A\file 2.txt", "ZipFileName_forum 0002.zip" "Überschreitung ZIP-FILE-LIMIT"
  • "Bundesland B\Region A\file 3.txt", "ZipFileName_forum 0002.zip"
  • "Bundesland B\Region B\file 1.txt", "ZipFileName_forum 0003.zip"
  • "Bundesland B\Region B\file 2.txt", "ZipFileName_forum 0003.zip" "Überschreitung ZIP-FILE-LIMIT"
  • "Bundesland B\Region B\file 3.txt", "ZipFileName_forum 0003.zip"
Raten ist selten in sinnvolles Vorgehen beim Programmieren. Dokumentation-Lesen hilft dagegen.
Bis jetzt ist mir leider verborgen gebliegen, wie man in einer For-Schleife ein voriges oder das nächste Element ansprechen kann. Auch das entsprechende Schlüsselwort dafür ist mir bis jetzt nicht eingefallen.
Benutzeravatar
__blackjack__
User
Beiträge: 13931
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

@DB-Hopf: Ein vorheriges kann man sich einfach merken. Das nächste wird schwierig ohne weitere Hilfsmittel. Also beispielsweise `peekable()` oder `spy()` aus `more_itertools`. Oder man bastelt sich so etwas selbst.

Letztlich würde ich das wohl vorschalten und die Pfade in Gruppen aufteilen. `itertools.groupby()` wäre dabei sicher hilfreich.

Es wird langsam ein bisschen sehr viel für *eine* Funktion die alles macht. Iterieren über die Pfade, aufteilen in Gruppen je nach Bundesland oder Region, und dann das erstellen von Archivdateien würde ich mindestens mal in einzeln testbare Funktionen aufteilen.
“Java is a DSL to transform big Xml documents into long exception stack traces.”
— Scott Bellware
Antworten