Categories: Java

【Java入門】zipファイルを生成・出力する方法

この記事では、zipファイルを作成する方法を解説しています。

  • 1ファイルをzip化する方法
  • 複数ファイルをzip化する方法
  • フォルダ内のファイルをzip化する方法


本記事ではFiles.readAllBytes(filePath);によりinputファイルをバイトで読み込んでいます。
そのため、圧縮したいファイルがUTF-8のテキストやSJISのテキスト、jpgやExcelファイルであっても文字化けすることなく圧縮することができます。

ですが、Files.readAllBytes(filePath);を使うと万が一巨大なファイルを読み込んだ際にOutOfMemoryが発生することがあるため、これ回避するためにバイト単位に読み込む方法についても記事の後半で解説しています。

ファイルをzip化する方法

以下のように、inputフォルダ直下のテキストファイルをzipファイルに圧縮してみます。

user01@DESKTOP-AQBVPOG MINGW64 /c/workspace/input
$ ls -l
total 1
-rw-r--r-- 1 user01 197609 9 11月 19 15:13 apple.txt

Javaコードは以下のとおりです。

File2Zip.java

public class Files2Zip {

    public static void main(String[] args) throws IOException {

        Path filePath = Paths.get("C:\\workspace\\input\\apple.txt");

        Path result = Paths.get("C:\\workspace\\output\\apple.zip");

        //zipファイルを作成
        try(FileOutputStream fos = new FileOutputStream(result.toString());
            BufferedOutputStream bos = new BufferedOutputStream(fos);
            ZipOutputStream zos = new ZipOutputStream(bos)){

            ZipEntry entry = new ZipEntry(filePath.getFileName().toString());
            zos.putNextEntry(entry);

            //ファイルの中身をbyte配列で取得し、書き込み。
            byte[] data = Files.readAllBytes(filePath);
            zos.write(data);
        };
    }
}

実行後、outputフォルダ配下にapple.zipが作成されていることが確認できます。

user01@DESKTOP-AQBVPOG MINGW64 /c/workspace/output
$ ls -l
total 2
-rw-r--r-- 1 user01 197609 143 11月 19 15:35 apple.zip

解説

Zipファイルの出力先パスについては、outputフォルダまでは存在している必要があります。フォルダが存在していない場合java.io.FileNotFoundExceptionがスローされてしまいます。
apple.zipについては、zipファイル出力時に生成されるので、プログラム実行時点では存在していなくてOKです。

今回は、try-with-resources文を採用しています。そのため、クローズ処理zos.closeEntry();を記述していませんが、処理終了時に自動的にクローズされます。

複数ファイルをzip化する方法

以下のように、inputフォルダ直下に存在する3つのテキストファイルを1つのzipファイルに圧縮してみます。

user01@DESKTOP-AQBVPOG MINGW64 /c/workspace/input
$ ls -l
total 3
-rw-r--r-- 1 user01 197609 9 11月 19 15:13 apple.txt
-rw-r--r-- 1 user01 197609 9 11月 19 15:13 banana.txt
-rw-r--r-- 1 user01 197609 9 11月 19 15:13 lemon.txt

Javaコードは以下のとおりです。

Files2Zip.java

public class Files2Zip {

    public static void main(String[] args) throws IOException  {

        //Zip化したい元ネタとなるテキストファイルが格納されているパス
        Path inputPath = Paths.get("C:\\workspace\\input");

        // 「inputPath」直下のファイルのパスの一覧を取得。
        List<Path> pathList = Files.walk(inputPath)
                                .filter(filePath -> filePath != inputPath)
                                .collect(Collectors.toList());

        //Zipファイルの出力先パス
        Path result = Paths.get("C:\\workspace\\output\\result.zip");

        //zipファイルを作成
        try(FileOutputStream fos = new FileOutputStream(result.toString());
            BufferedOutputStream bos = new BufferedOutputStream(fos);
            ZipOutputStream zos = new ZipOutputStream(bos)){
        
            //ファイルの数分、ループする。
            for (Path filePath : pathList){

                //テキストファイルの名前を取得。
                ZipEntry entry = new ZipEntry(filePath.getFileName().toString());
                zos.putNextEntry(entry);

                //ファイルの中身をbyte配列で取得し、書き込み。
                byte[] data = Files.readAllBytes(filePath);
                zos.write(data);
            }
        }
    }
}

実行後、outputフォルダ配下にresult.zipが作成されていることが確認できます。

user01@DESKTOP-AQBVPOG MINGW64 /c/workspace/output
$ ls -l
total 1
-rw-r--r-- 1 user01 197609 386 11月 19 15:55 result.zip

解説

まず、「inputPath」直下のファイルのパスの一覧を取得している以下の処理についてです。

List<Path> pathList = Files.walk(inputPath).filter(filePath -> filePath!= inputPath).collect(Collectors.toList());

Files.walkは通常、取得できるパス一覧に自分自身("C:\\workspace\\input")も含まれてしまいます。
ですが、今回取得したいのはzip化したいテキストファイルのパスのみなので、上記のようにフィルタリングすることで自分自身のパスを除外しています。

Zipファイルの出力先パスについては、outputフォルダまでは存在している必要があります。フォルダが存在していない場合java.io.FileNotFoundExceptionがスローされてしまいます。
result.zipについては、zipファイル出力時に生成されるので、プログラム実行時点では存在していなくてOKです。

今回も、try-with-resources文を採用しています。そのため、クローズ処理zos.closeEntry();を記述していませんが、処理終了時に自動的にクローズされます。

複数のフォルダ内に存在するファイルを1つのzipファイルに圧縮する方法

以下のように、inputフォルダ直下に複数のフォルダが存在していてその中にあるファイルを、フォルダ階層を保ったまま1つのzipファイルに圧縮する方法です。

user01@DESKTOP-AQBVPOG MINGW64 /c/workspace/input
$ ls -l -d $(find `pwd`)
drwxr-xr-x 1 user01 197609  0 11月 19 16:44 /c/workspace/input/
-rw-r--r-- 1 user01 197609 12 11月 19 16:44 /c/workspace/input/chocolate.txt
drwxr-xr-x 1 user01 197609  0 11月 19 16:43 /c/workspace/input/fruits/
-rw-r--r-- 1 user01 197609  9 11月 19 15:13 /c/workspace/input/fruits/apple.txt
-rw-r--r-- 1 user01 197609  9 11月 19 15:13 /c/workspace/input/fruits/banana.txt
-rw-r--r-- 1 user01 197609  9 11月 19 15:13 /c/workspace/input/fruits/lemon.txt
drwxr-xr-x 1 user01 197609  0 11月 19 16:43 /c/workspace/input/vegetable/
-rw-r--r-- 1 user01 197609  9 11月 19 16:43 /c/workspace/input/vegetable/tomato.txt

Javaコードは以下のとおりです。

Files2ZipMultiFolder.java

public class Files2ZipMultiFolder {

    public static void main(String[] args) throws IOException  {

        //Zip化したい元ネタとなるテキストファイルが格納されているパス
        Path inputPath = Paths.get("C:\\workspace\\input");

        // 「inputPath」直下のファイルのパスの一覧を取得。
        List<Path> pathList = Files.walk(inputPath)
                                    .filter(filePath -> !filePath.toFile().isDirectory())
                                    .collect(Collectors.toList());

        //Zipファイルの出力先パス
        Path result = Paths.get("C:\\workspace\\output\\result.zip");

        //zipファイルを作成
        try(FileOutputStream fos = new FileOutputStream(result.toString());
            BufferedOutputStream bos = new BufferedOutputStream(fos);
            ZipOutputStream zos = new ZipOutputStream(bos)){

            //ファイルの数分、ループする。
            for (Path filePath : pathList){

                //サブパスを取得
                String subPath = filePath.subpath(inputPath.getNameCount(), filePath.getNameCount()).toString();
                ZipEntry entry = new ZipEntry(subPath);
                zos.putNextEntry(entry);

                //ファイルの中身をbyte配列で取得し、書き込み。
                byte[] data = Files.readAllBytes(filePath);
                zos.write(data);
            }
        }
    }
}

実行後、outputフォルダ配下にresult.zipが作成されていることが確認できます。

user01@DESKTOP-AQBVPOG MINGW64 /c/workspace/output
$ ls -l
total 1
-rw-r--r-- 1 user01 197609 703 11月 19 18:05 result.zip

これを解凍すると以下のようにフォルダ階層を保ったままファイルが格納されていることを確認できます。

PS C:\workspace\output\result> tree /f
フォルダー パスの一覧:  ボリューム OS
ボリューム シリアル番号は A264-A203 です
C:.
│  chocolate.txt
│
├─fruits
│      apple.txt
│      banana.txt
│      lemon.txt
│
└─vegetable
        tomato.txt

解説

まず、Files.walkのフィルタリング処理についてです。今回は以下のようにしています。

.filter(filePath -> !filePath.toFile().isDirectory())

Files.walkの場合、以下のようなディレクトリだけのパスも取得できてしまいますから、isDirectory()でディレクトリ判定を行うことで、これを除外してファイルのパスだけを取得できるようにしています。

/c/workspace/input/
/c/workspace/input/fruits/
/c/workspace/input/vegetable/

次にサブパスの処理です。以下のように記述しています。

String subPath = filePath.subpath(inputPath.getNameCount(), filePath.getNameCount()).toString();

zipファイル作成時、inputフォルダ配下の各ファイルを、元のフォルダ階層を保ったままzip化することを考えると欲しいパスは以下の黄色部分ということになります。

/c/workspace/input/fruits/apple.txt

/c/workspace/input/fruits/banana.txt

/c/workspace/input/fruits/lemon.txt

/c/workspace/input/vegetable/tomato.txt

/c/workspace/input/chocolate.txt

subpathgetNameCountを使用することで、この黄色部分を取得できるようになります。
これらのメソッドの使い方については以下の記事で解説しています。

指定したバイト単位に読み込む

Files.readAllBytes(filePath);の場合、ファイルの中身すべてを一括してbyte配列として取得するので、読み込み対象のファイルサイズが大きい場合、OutOfMemoryが発生する危険性があります。 OutOfMemoryが発生しないようにしたい場合、以下のようにbyte単位に読み込むコードに変更する必要があります。

変更前

//テキストファイルの中身をbyte配列で取得し、書き込み。
byte[] data = Files.readAllBytes(filePath);
zos.write(data);

変更後

//テキストファイルの中身をbyte配列で取得し、書き込み。
try(FileInputStream fis = new FileInputStream(filePath.toString());
    BufferedInputStream bis = new BufferedInputStream(fis);){

    byte[] buf = new byte[1024];
    int len = 0;

    while ((len = bis.read(buf)) > 0) {
        zos.write(buf, 0, len);
    }
}

なお、java8以降の環境では以下のようにFiles.newInputStreamを使用することができます。が、バッファしたい場合は、BufferedInputStreamのラップは別途必要になります。

Java8以降の環境

//テキストファイルの中身をbyte配列で取得し、書き込み。
try(InputStream fis = Files.newInputStream(filePath)){

    byte[] buf = new byte[1024];
    int len = 0;

    while ((len = fis.read(buf)) > 0) {
        zos.write(buf, 0, len);
    }
}

以上で記事の解説はお終い!

もっとJavaやSpringを勉強したい方にはUdemyがオススメ!同僚に差をつけよう!

issiki_wp