Java Tips | I/O・ネットワーク:zip圧縮

Java Java
スポンサーリンク

なぜ「zip圧縮ユーティリティ」が業務で強い武器になるのか

gzip が「1ファイル専用の圧縮袋」だとしたら、ZIP は「複数ファイルをまとめる箱」です。 業務システムでは、レポート一式、エクスポート結果、バックアップ、ユーザーへの一括ダウンロードなど、「複数ファイルをひとまとめにしたい」場面がとても多いです。

毎回その場で ZipOutputStream を直接書いていると、 エントリ名のミス、ディレクトリ構造の扱い、ストリームの閉じ忘れ、文字コードの混乱などが起きやすくなります。 だからこそ、「zip圧縮」をユーティリティとしてまとめておくと、業務コードがすっきりし、再利用性と安全性が一気に上がります。

基本形:複数ファイルを ZIP にまとめるユーティリティ

ZipOutputStream と Files を組み合わせる

まずは、「指定した複数ファイルを ZIP にまとめる」一番ベーシックなユーティリティから押さえましょう。

import java.io.IOException;
import java.io.OutputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;

public class ZipUtils {

    public static void zipFiles(List<Path> inputFiles, Path zipFile) throws IOException {
        try (OutputStream out = Files.newOutputStream(zipFile);
             ZipOutputStream zipOut = new ZipOutputStream(out)) {

            for (Path file : inputFiles) {
                String entryName = file.getFileName().toString();
                ZipEntry entry = new ZipEntry(entryName);
                zipOut.putNextEntry(entry);

                byte[] bytes = Files.readAllBytes(file);
                zipOut.write(bytes);

                zipOut.closeEntry();
            }
        }
    }
}
Java

使い方の例です。

import java.nio.file.Path;
import java.util.List;

public class ZipExample {

    public static void main(String[] args) throws Exception {
        List<Path> files = List.of(
                Path.of("data/report1.csv"),
                Path.of("data/report2.csv"),
                Path.of("data/summary.txt")
        );

        Path zip = Path.of("out/reports.zip");
        ZipUtils.zipFiles(files, zip);
    }
}
Java

ここで重要なのは、「ZipEntry ごとに putNextEntry → 書き込み → closeEntry の流れを守る」ことです。 ZIP は「エントリ(ファイル)単位」で構造を持っているので、この流れを崩すと壊れた ZIP になります。

もう一つのポイントは、「エントリ名に file.getFileName() を使っている」ことです。 ここではシンプルにファイル名だけを ZIP に入れていますが、 ディレクトリ構造を持たせたい場合は、後述のようにパスを工夫します。

大きなファイルをストリームで ZIP に入れる

readAllBytes を避けて、少しずつ読みながら書く

先ほどの例は分かりやすい反面、巨大ファイルでは readAllBytes() がメモリを圧迫します。 そこで、「ストリームコピー」で ZIP に書き込む形にします。

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;

public class ZipStreamUtils {

    public static void zipFilesStreaming(List<Path> inputFiles, Path zipFile) throws IOException {
        try (OutputStream out = Files.newOutputStream(zipFile);
             ZipOutputStream zipOut = new ZipOutputStream(out)) {

            byte[] buffer = new byte[8192];

            for (Path file : inputFiles) {
                ZipEntry entry = new ZipEntry(file.getFileName().toString());
                zipOut.putNextEntry(entry);

                try (InputStream in = Files.newInputStream(file)) {
                    int len;
                    while ((len = in.read(buffer)) != -1) {
                        zipOut.write(buffer, 0, len);
                    }
                }

                zipOut.closeEntry();
            }
        }
    }
}
Java

ここで深掘りしたいのは、「部分読み込みと部分書き込み」の扱いです。 in.read(buffer) は「バッファサイズぴったり読める」とは限らず、 実際に読めたバイト数(len)を返します。 必ず zipOut.write(buffer, 0, len) として、「有効な範囲だけ」書き出す必要があります。

もう一つの重要ポイントは、「内側の InputStream も try-with-resources で閉じている」ことです。 ZIP 全体のストリームとは別に、各ファイルのストリームも確実に閉じることで、 ファイルハンドルのリークを防ぎます。

ディレクトリ構造を ZIP に含めるユーティリティ

ベースディレクトリからの相対パスをエントリ名にする

業務では、「フォルダ構造ごと ZIP にしたい」場面も多いです。 その場合は、エントリ名を「ベースディレクトリからの相対パス」にします。

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;

public class ZipDirUtils {

    public static void zipDirectory(Path baseDir, Path zipFile) throws IOException {
        try (OutputStream out = Files.newOutputStream(zipFile);
             ZipOutputStream zipOut = new ZipOutputStream(out)) {

            Files.walk(baseDir)
                 .filter(Files::isRegularFile)
                 .forEach(path -> {
                     try {
                         Path relative = baseDir.relativize(path);
                         String entryName = relative.toString().replace("\\", "/");

                         ZipEntry entry = new ZipEntry(entryName);
                         zipOut.putNextEntry(entry);

                         try (InputStream in = Files.newInputStream(path)) {
                             byte[] buffer = new byte[8192];
                             int len;
                             while ((len = in.read(buffer)) != -1) {
                                 zipOut.write(buffer, 0, len);
                             }
                         }

                         zipOut.closeEntry();
                     } catch (IOException e) {
                         throw new RuntimeException(e);
                     }
                 });
        }
    }
}
Java

ここで深掘りしたいポイントは、「ZIP のエントリ名は / 区切りで書く」ということです。 Windows ではパス区切りが \ ですが、そのまま使うと ZIP ビューアによっては正しく解釈されません。 replace("\\", "/") で統一しておくと、どの環境でも安定して扱えます。

もう一つのポイントは、「Files.walk でディレクトリを再帰的にたどっている」ことです。 これにより、サブディレクトリも含めて一括で ZIP にできます。

ZIP圧縮と文字列・文字コードの関係

テキストファイルは「中身の文字コード」と「エントリ名の文字コード」を意識する

ZIP は「バイト列」をそのまま格納するので、 テキストファイルの中身は、UTF-8 でも Shift_JIS でも何でも構いません。

ただし、業務システムとしては次のような方針を決めておくと安全です。

中身のテキストは UTF-8 に統一する。 エントリ名(ファイル名)は、基本的に ASCII か UTF-8 で扱う。

Java の ZipOutputStream は内部的にエントリ名をバイト列に変換しますが、 古い ZIP ツールとの互換性などを考えると、 「極端に複雑な文字(絵文字など)をファイル名にしない」方針も現場ではよく取られます。

セキュリティ・運用の観点から見た ZIP圧縮

「何を ZIP に入れてよいか」を明確にする

ZIP は「複数ファイルをまとめる箱」なので、 機密情報や個人情報を含むファイルをうっかり一緒に入れてしまう危険があります。

業務では、次のような点を意識する必要があります。

ZIP に含めるファイルの種類・場所を事前に決めておく。 ユーザーに渡す ZIP には、機密情報を含めない(もしくは暗号化する)。 ログや監査用に「ZIP に何を入れたか」を記録しておく。

ユーティリティ側で「対象ディレクトリを限定する」「特定拡張子だけを対象にする」といった制御を入れておくと、 業務コード側のセキュリティ負担を減らせます。

ZIP爆弾への意識(圧縮率の高い巨大データ)

圧縮そのものは安全な操作ですが、 「解凍すると異常に大きくなる ZIP(いわゆる ZIP爆弾)」は攻撃手法として存在します。

圧縮側ユーティリティとしては直接の対策ではありませんが、 「どのくらいのサイズのファイルを ZIP にしているか」をログに残しておくと、 運用側で異常なサイズを検知しやすくなります。

まとめ:zip圧縮ユーティリティで身につけてほしい感覚

zip圧縮ユーティリティは、「複数ファイルやディレクトリ構造を、ひとつのアーカイブにまとめる」ための道具です。 その中には、次のような大事な感覚が詰まっています。

ZipOutputStream では、エントリごとに putNextEntry → 書き込み → closeEntry の流れを守る。 小さなファイルは readAllBytes、大きなファイルはストリームコピーで書き込む。 ディレクトリ構造を ZIP に含めるときは、相対パス+/ 区切りでエントリ名を作る。 ストリームは try-with-resources で必ず閉じ、リソースリークを防ぐ。 何を ZIP に入れるか、どこまでユーザーに渡すかをセキュリティ・運用の観点で設計する。

もしあなたのプロジェクトで、 「レポート一式を毎回バラバラにダウンロードさせている」 「バックアップが散らばっていて管理しづらい」 といった状況があるなら、 この zip圧縮ユーティリティを軸に、「まとめて ZIP にする」フローを設計してみてください。

そこから先は、ファイル配布・バックアップ・エクスポートの扱いが、ぐっと業務レベルに洗練されていきます。

タイトルとURLをコピーしました