ZIP-Dateien in Java lesen und schreiben

Mit Hilfe der Java-Klassen ZipInputStream und ZipOutputStream kannst du ZIP-Dateien in Java direkt lesen und schreiben. Beide verwenden die Klasse ZipEntry zum Zugriff auf die einzelnen Dateien, die sich im Archiv befinden. All diese Klassen gehören zum Paket java.util.zip und sind Teil der Standardbibliothek von Java. Hier zeige ich dir, wie du diese einsetzt.

In diesem Beispiel entwickeln wir Schritt für Schritt die Methoden für das direkte Lesen und Schreiben eines ZIP-Archivs in Java. Anschließend gibt es dann den kompletten Quellcode.

Vorbereitung

Zuerst benötigen wir eine ZIP-Datei, die du lesen willst. Verwende dazu ein ZIP-Tool deiner Wahl Du kannst z.B. 7-zip verwenden(Download unter https://www.7-zip.org/). Oft geht es auch direkt im Windows-Explorer. (Kontextmenü „Senden an ZIP“)

Das ZIP-Archiv könnte z.B. so aussehen:

  • somefile.txt
  • dir/someotherfile.txt
  • dir/emptysubdir

So liest du das Inhaltsverzeichnis einer ZIP-Datei in Java

Wir fangen einfach an: als ersten Schritt erzeugst du eine neue Java-Klasse: ZipFileExample. Als nächstes lernen wir, das Inhaltsverzeichnis der ZIP-Datei aufzulisten. Als weiteren Schritt lesen wir dann den Inhalt der Dateien aus.

Du erzeugst eine Methode

public static void readZipDirectory(String filename) throws IOException {
}

filename ist der Name (inkl. komplettem Pfad) der ZIP-Datei, die du lesen willst.

Im Prinzip können an dieser Stelle allerlei Fehler passieren. Deshalb wirft die Methode eine Ausnahme, wenn die Datei nicht gelesen werden kann.

Vielleicht gibt es die Datei gar nicht. Oder sie existiert, aber du hast keine Berechtigung, sie zu öffnen. Vielleicht fängst du an, die Datei zu lesen, aber die Netzwerkverbindung fällt aus, weil es beim Internetprovider einen Stromausfall gibt. Vielleicht ist die Datei korrupt. Wie auch immer. Wir merken uns: immer dann, wenn du in Java auf Dateien zugreifst, können die Methode eine Ausnahme werfen.

Desalb musst du dich immer fragen, wie du damit umgehst. Du kannst sie mit try-catch einfangen, oder du reichst sie an den Aufrufer weiter. In irgend einer Form musst du sie jedoch behandeln. Einen Fehler zu ignorieren, ist selten eine gute Wahl. Hier reichen wir die Ausnahme an den Aufrufer weiter.

Nun öffnen wir die ZIP-Datei als Java-Stream. Streams sind in Java immer das Mittel der Wahl, um auf Dateien zuzugreifen.

        // the zip file
        File f = new File(filename);
        FileInputStream fis = new FileInputStream(f);
        BufferedInputStream bfis = new BufferedInputStream(fis);
        ZipInputStream zipis = new ZipInputStream(bfis);

Im Prinzip geht es auch ohne den BufferedInputStream. Es ist aber üblich, einen zu benutzen. Wichtig ist das, wenn die Dateien etwas größer sind. Irgendwann fragt man dann doch nach der Performance. Wenn du von Anfang an daran denkst, brauchst du dich nicht erneut darum zu kümmern.

Der Zugriff auf die einzelnen Dateien innerhalb der ZIP-Datei erfolgt über die Klasse ZipEntry. („entry“=Eintrag) Hier gibt es keinen Iterator. Du brauchst daher die alte Vorgehensweise einer while-Schleife. Hole dir einen ZipEntry nach dem anderen, bis du fertig bist.

        ZipEntry zipentry;
        int numEntry = 0;
        while ((zipentry = zipis.getNextEntry()) != null) {
            numEntry++;
            if ( zipentry.isDirectory()) {
                System.out.print("Directory ");
            }
            else {
                System.out.print("File ");
            }
            System.out.format("Entry #%d: path=%s, size=%d, compressed size=%d \n", numEntry, zipentry.getName(), zipentry.getSize(), zipentry.getCompressedSize(), );
        }
        // remember to close the zip input stream!
        zipis.close();

Jeder Eintrag kann dir erzählen, ob er eine Datei (file) oder ein Verzeichnis (directory) ist. Jeder Eintrag kennt seine ursprüngliche Größe und die gepackte Größe. Darüber hinaus gibt es noch weitere Methoden, z.B. zum Auslesen des Zeitstempels. Deine IDE kann dir die möglichen Methoden in der Autovervollständigung vorschlagen.

Wenn du mit allem fertig bist, dann denke daran, den ZIP-Stream zu schließen.

Nun kannst du ein Hauptprogramm schreiben und die Methode aufrufen. Du übergibst nur den Pfad zur ZIP-Datei an readZipDirectory.

Meine Ausgabe sieht so aus:

Directory Entry #1: path=dir/, size=0, compressed size=0
Directory Entry #2: path=dir/emptysubdir/, size=0, compressed size=0
File Entry #3: path=dir/someotherfile.txt, size=37, compressed size=35
File Entry #4: path=somefile.txt, size=25, compressed size=25

Und hier ist der komplette Quelltext:

// preconditions: generate a zip file with some content like
// somefile.txt
// dir/someotherfile.txt
// dir/emptysubdir
    public static void readZipDirectory(String filename) throws IOException {
        // the zip file
        File f = new File(filename);
        FileInputStream fis = new FileInputStream(f);
        BufferedInputStream bfis = new BufferedInputStream(fis);
        ZipInputStream zipis = new ZipInputStream(bfis);

        ZipEntry zipentry;
        int numEntry = 0;
        while ((zipentry = zipis.getNextEntry()) != null) {
            numEntry++;
            if ( zipentry.isDirectory()) {
                System.out.print("Directory ");
            }
            else {
                System.out.print("File ");
            }
            System.out.format("Entry #%d: path=%s, size=%d, compressed size=%d\n", numEntry, zipentry.getName(), zipentry.getSize(), zipentry.getCompressedSize());
        }
        // remember to close the zip input stream!
        zipis.close();
    }

So liest du den Inhalt einer ZIP-Datei in Java aus

Von hier ist es nicht weit bis zum Zugriff auf die Dateiinhalte. Kopiere die Methode readZipContents.

Der Code zum Zugriff auf den Dateiinhalt sieht so aus:

            byte[] buffer = new byte[2048];
            ByteArrayOutputStream bos = new ByteArrayOutputStream();
            int len;
            // read bytes of file
            while ( (len = zis.read(buffer)) > 0 ) {
                System.out.println( "file: " + zipEntry.getName() + " : " + len + "  bytes read");
                bos.write(buffer,0,len );
            }
            // convert bytes to string
            byte[] zipFileBytes = bos.toByteArray();
            String fileContent = new String ( zipFileBytes );
            System.out.println("Content of file: " + zipEntry.getName() + " : " + fileContent );

Beim Auslesen des Streams erhältst du unmittelbar die unkomprimierten Daten. Das Auspacken übernimmt der ZipOuputStream für dich.

Für einen effizienten Zugriff brauchen wir einen Puffer. Blockweise lässt sich der Datenstrom besser verarbeiten.

Wir arbeiten mit Bytes. Der Zugriff auf die Rohdaten von Dateien erfolgt in Java typischerweise auf Ebene der Bytes. Mit einem Puffer-Array von 2048 Bytes kannst du also immer bis zu 2048 Bytes pro Zugriff aus dem Stream lesen und anschließend verarbeiten.

Wir lesen so lange aus dem Stream, wie der Stream Daten für uns hat. Sobald 0 Bytes gelesen werden, sind wir fertig. Java nennt uns bei jedem Zugriff die Anzahl der gelesenen Bytes. Das müssen nicht immer die 2048 Bytes aus dem Puffer sein, es können auch weniger sein. Alle Bytes sammeln wir in einem ByteArrayOutputStream.

Sobald die Datei komplett gelesen wurde, fragen wir den Ausgabepuffer nach dem kompletten Dateiinhalt. Wieder erhalten wir das Ergebnis als Bytes. Diese wandeln wir in einen String, denn dies ist bei Textdateien das Format, in dem du diese wahrscheinlich weiterverarbeiten wirst.

Der Einfachheit halber schreiben wir den Inhalt der Dateien direkt nach stdout. Du kannst diese Inhalte aber auch direkt weiterverarbeiten. Vielleicht handelt es sich um eine XML-Datei, die du parsen willst.

Wenn es sich hingegen um eine Bilddatei handelt, dann macht die Umwandlung der Bytes in einen String nicht so viel Sinn. In diesem Fall ist wichtig, welches Format die weitere Verarbeitung erfordert.

Wie versprochen ist hier nun der komplette Code dieses Beispiels:

    public static void readZipContents(String filename) throws IOException {
        // the zip file
        File f = new File(filename);
        FileInputStream fis = new FileInputStream(f);
        BufferedInputStream bfis = new BufferedInputStream(fis);
        ZipInputStream zis = new ZipInputStream(bfis);

        ZipEntry zipEntry;
        int numEntry = 0;
        while ((zipEntry = zis.getNextEntry()) != null) {
            numEntry++;
            System.out.format("Entry #%d: path=%s, size=%d, compressed size=%d \n", numEntry, zipEntry.getName(), zipEntry.getSize(), zipEntry.getCompressedSize());
            byte[] buffer = new byte[2048];
            ByteArrayOutputStream bos = new ByteArrayOutputStream();
            int len;
            // read bytes of file
            while ( (len = zis.read(buffer)) > 0 ) {
                System.out.println( "file: " + zipEntry.getName() + " : " + len + "  bytes read");
                bos.write(buffer,0,len );
            }
            // convert bytes to string
            byte[] zipFileBytes = bos.toByteArray();
            String fileContent = new String ( zipFileBytes );
            System.out.println("Content of file: " + zipEntry.getName() + " : " + fileContent );
            
        }
        // remember to close the zip input stream!
        zis.close();
    }

So schreibst du eine ZIP-Datei direkt aus Java

Java kann ZIP-Dateien auch schreiben. Aber woher kommen die Daten? Du kannst z.B. Dateien des Dateisystems verwenden. Du kannst ein Programm schreiben, das ein Verzeichnis nimmt, über alle Dateien des Verzeichnisse iteriert, die Dateien liest und in einem ZIP-Archiv verpackt.

Was ich erreichen will, ist etwas ganz anderes. Ich will ein ZIP-Package generieren, um einen Integration Flow in ein SAP CPI zu laden. Mein Java-Programm wird alle Dateinamen und die Dateiinhalte direkt in Java generieren. Ich werde die Dateien nicht erste auf die Festplatte schreiben, um sie dann zu packen. Ich will sie direkt ins Archiv schreiben. Wie ich die Dateiinhalte generiere, kann ich an dieser Stelle nicht zeigen. Nehmen wir einfach an, wir hätten jeweils den Dateinamen und den Dateiinhalt als String.

Die zentrale Klasse ist ZipOutputStream. Wir öffnen also diesen Stream und schreiben ihn direkt auf die Platte. Dafür benötigen wir ein File (Datei), einen FileOutputStream, einen BufferedOutputStream für die Performance und den Zip-OutputStream.

Nehmen wir also an, wir hätten ein paar Dateinamen und Inhalte dazu. Für dieses kleine Beispiel nehmen wir Strings. In einem echten Programm würde eine andere Klasse oder Methode diese Inhalte bereitstellen und vielleicht sogar die Dateinamen.

Wir wollen mehrere Dateien schreiben. Also delegieren wir das eigentliche Schreiben an eine Methode writeSingleFileToZip und diese verwenden wir mehrmals. Damit sieht dieser Abschnitt so aus:

    public static void writeZipFile(String zipFilename)
            throws IOException {

        File f = new File(zipFilename);
        FileOutputStream fos = new FileOutputStream(f);
        BufferedOutputStream bos = new BufferedOutputStream(fos);
        ZipOutputStream zos = new ZipOutputStream(bos);
        // write these files:

        String filename = "sometextfile.txt";
        String content = "Here is some text";
        writeSingleFileToZip(zos, filename, content);

        filename = "subdir/somemoretext.txt";
        content = "Here is some more text";
        writeSingleFileToZip(zos, filename, content);

        zos.close();
        
        System.out.println("zip file " + zipFilename + " written");
    }

Im Vergleich zum Lesen einer ZIP-Datei stecken hier keine Überraschungen drin.

Übrigens: du brauchst das File nicht selbst zu schließen. Wenn du den ZipOutputStream schließt, macht dieser für dich die Datei schon zu.

Und so verpackst du den Dateiinhalt im ZIP-Archiv:

Bitte beachte: wir müssen wieder byteweise arbeiten. Daher konvertierst du den String in ein Byte Array.

Für jede Datei erzeugst du einen neuen ZipEntry und meldest dem Zip Stream, dass du eine neue Datei hast. Dann schreibst du den Dateiinhalt in den Stream. Die Signatur erwartet, dass du mitgibst, wie viele Bytes du schreiben willst. Das Komprimieren der Inhalte übernimmt der ZipOutputStream für dich.

Wenn du fertig bist, schließt du den ZipEntry (nicht den Stream!). Der ZipOutputStream bleibt für die nächste Datei offen. Erst ganz am Ende wird er geschlossen.

    private static void writeSingleFileToZip(ZipOutputStream zos, String filename, String content) throws IOException {
        System.out.println("Writing file " + filename + " to ZIP archive ");
        // we need the content as bytes, not as a String
        byte[] bytes = content.getBytes();

        // start new entry
        ZipEntry zipEntry = new ZipEntry(filename);
        zos.putNextEntry(zipEntry);

        // write content to ZIP output stream, awkward method signature.
        zos.write(bytes, 0, bytes.length);
        // don't forget to close the entry!
        zos.closeEntry();
    }

Viel Spaß mit ZIP-Dateien in Java

Jetzt weißt du, wie du ZIP-Dateien in Java direkt aus- und einpacken kannst. Wie du siehst, ist das ganz einfach. Es geht direkt im Speicher. Du brauchst nicht über das Dateisystem zu arbeiten.

Und jetzt viel Spaß mit ZIP-Dateien. Schreib ein großartiges Programm damit, so wie z.B. das Erzeugen von Migrationsdateien für SAP CPI.

Mehr Java-Tipps findest du hier.

heiko

Dipl.-Ing. Heiko Evermann

Vorheriger Artikel