この記事では、zipファイルを作成する方法を解説しています。
本記事ではFiles.readAllBytes(filePath);
によりinputファイルをバイトで読み込んでいます。
そのため、圧縮したいファイルがUTF-8のテキストやSJISのテキスト、jpgやExcelファイルであっても文字化けすることなく圧縮することができます。
ですが、Files.readAllBytes(filePath);
を使うと万が一巨大なファイルを読み込んだ際にOutOfMemory
が発生することがあるため、これ回避するためにバイト単位に読み込む方法についても記事の後半で解説しています。
以下のように、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();
を記述していませんが、処理終了時に自動的にクローズされます。
以下のように、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();
を記述していませんが、処理終了時に自動的にクローズされます。
以下のように、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
subpath
とgetNameCount
を使用することで、この黄色部分を取得できるようになります。
これらのメソッドの使い方については以下の記事で解説しています。
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がオススメ!同僚に差をつけよう!