S-JIS[2007-12-09/2015-12-12] 変更履歴
Javaでzipファイルを扱う方法。
|
|
zipはjavaでよく使う(jarファイルがzip形式だから)のでjava.util.zipというパッケージが用意されており、標準で扱える。
しかしJDK1.6以前では日本語ファイル名が扱えない(※)ので日本人としては非常に不便。[/2014-04-16]
※
正確には、圧縮時にUTF8で保存されてしまい、解凍時もUTF8として扱ってしまう。Java以外のツールで日本語ファイル名をUTF8で扱ってくれない場合、文字化けしてしまう(大半はそうだと思う)。
一方、Ant(ant.jar)でもzipを扱うクラスが(半分)独自実装されており、こちらはエンコードを指定することができるので日本語ファイル名を簡単に扱うことができる。(クラス名や使い方はJava標準クラスと
だいたい同じ)
Eclipseを使っていればant.jarが入っているし、Antのものを使うのもいいだろう。
JDK1.7ではZipOutputStreamやZipFileのコンストラクターでファイル名のエンコードを指定できるようになったので、無理してAnt版を使う必要はなくなった。[2014-04-16]
zipファイルを作成するには、ZipOutputStreamというクラスを使う。
(標準ライブラリならjava.util.zip.ZipOutputStream
、Antのならorg.apache.tools.zip.ZipOutputStream
)
ZipOutputStreamは素直にnewでインスタンスを作成するのだが、Ant版ではコンストラクターがちょっと違う。
パッケージ | 生成方法 | 概要 | zipファイルサイズ |
---|---|---|---|
java.util.zip |
ZipOutputStream(OutputStream) |
標準版。出力ストリームに対して作成する。 | 普通 |
ZipOutputStream(OutputStream,
Charset) |
ファイル名およびコメントのエンコーディングを指定できる。 JDK1.7以降。[2014-04-16] |
||
org.apache.tools.zip |
ZipOutputStream(OutputStream) |
Ant版。標準版と同様、ストリームに対して出力する。 | ちょっと大 |
org.apache.tools.zip |
ZipOutputStream(File) |
Ant版。ファイル名を指定し、内部ではランダムアクセスファイルを使って出力する。 | ちょっと小 |
java.nio.file |
FileSystems.newFileSystem(String, ClassLoader) |
PathやFilesを使って書き込む。JDK1.7以降。[2015-12-12] →zipのFileSystemで書き込む例 |
生成されたzipファイルのサイズは、ランダムアクセスで作ったファイルの方がちょっと小さくなる。(データ自体の圧縮サイズは変わらない)
どうもzipでは、圧縮する個々のファイル毎に、最初にヘッダー(ファイル名やサイズやCRC等の情報)を出力し、その後に圧縮したデータ本体、最後にフッター的な情報を出力するようだ。
で、ストリームの場合は前に戻れないのでヘッダーのサイズ項目にはダミーの値を出力し、フッターにサイズやCRC等のチェック情報を入れるようだ。
ランダムアクセスファイルを使うとファイル内を自由に移動できるので、フッターは使わず、ヘッダーに戻って書き込むのではないかと思う。したがってフッターの分だけストリームで出力したものよりサイズが減る、と。
//標準Javaのzipクラスを使う場合 import java.util.zip.ZipEntry; import java.util.zip.ZipOutputStream;
//Antのzipクラスを使う場合 import org.apache.tools.zip.ZipEntry; import org.apache.tools.zip.ZipOutputStream;
File file = new File("〜\sample.zip"); //作成するzipファイルの名前 File[] files = { new File("directory") }; //圧縮対象を相対パスで指定 ZipOutputStream zos = new ZipOutputStream(new FileOutputStream(file)); zos.setEncoding("MS932"); //Ant版のみ try { encode(zos, files); } finally { zos.close(); }
static byte[] buf = new byte[1024]; static void encode(ZipOutputStream zos, File[] files) throws Exception { for (File f : files) { if (f.isDirectory()) { encode(zos, f.listFiles()); } else { ZipEntry entry = new ZipEntry(f.getPath().replace('\\', '/')); zos.putNextEntry(entry); try (InputStream is = new BufferedInputStream(new FileInputStream(f))) { for (;;) { int len = is.read(buf); if (len < 0) break; zos.write(buf, 0, len); } } } } }
1つの圧縮対象ファイルにつき、1つのZipEntryを生成する。(パスは「/」スラッシュ区切りにしておく必要がある)
それをZipOutputStreamに登録してwrite()すると、データが圧縮される。
コンストラクターを変えてやると圧縮にかかる実行時間に影響するので、ちょっと時間を計ってみた。
(JDK1.6、Ant1.6.5(Eclipse3.2に付いてるant.jar))
パッケージ | コンストラクター | 実行時間 | zipファイルサイズ |
---|---|---|---|
java.util.zip |
zos = new ZipOutputStream(new FileOutputStream(zipf)); | 110ms | 41513バイト |
java.util.zip |
zos = new ZipOutputStream(new BufferedOutputStream(new FileOutputStream(zipf))); | 30ms | 41513バイト |
org.apache.tools.zip |
zos = new ZipOutputStream(new FileOutputStream(zipf)); | 60ms | 41571バイト |
org.apache.tools.zip |
zos = new ZipOutputStream(new BufferedOutputStream(new FileOutputStream(zipf))); | 30ms | 41571バイト |
org.apache.tools.zip |
zos = new ZipOutputStream(zipf); | 47ms | 40419バイト |
バッファリングをしたストリーム方式が、標準JavaでもAnt版でもほぼ同等の速度(若干Ant版の方が速そう)で一番速い。
次いで、ランダムアクセス方式。
バッファリングをしない標準Java版が一番遅い。
ただし、生成されたzipファイルのサイズはランダムアクセス方式が一番小さく、ストリーム方式のAnt版が一番大きい。(と言っても大差ないけど)
java: 860 ant_os: 844 javaB: 203 ant_osB: 140 ant_raf: 110 java: 594 ant_os: 62 javaB: 31 ant_osB: 47 ant_raf: 47 java: 109 ant_os: 62 javaB: 32 ant_osB: 485 ant_raf: 46 java: 110 ant_os: 63 javaB: 31 ant_osB: 31 ant_raf: 47 java: 109 ant_os: 94 javaB: 31 ant_osB: 344 ant_raf: 47 java: 93 ant_os: 62 javaB: 32 ant_osB: 31 ant_raf: 32 java: 125 ant_os: 63 javaB: 15 ant_osB: 31 ant_raf: 47 java: 344 ant_os: 46 javaB: 47 ant_osB: 16 ant_raf: 47 java: 109 ant_os: 47 javaB: 16 ant_osB: 31 ant_raf: 31 java: 125 ant_os: 62 javaB: 16 ant_osB: 16 ant_raf: 31 java: 109 ant_os: 62 javaB: 110 ant_osB: 16 ant_raf: 31 java: 93 ant_os: 63 javaB: 16 ant_osB: 15 ant_raf: 47 java: 94 ant_os: 63 javaB: 15 ant_osB: 16 ant_raf: 46 java: 94 ant_os: 62 javaB: 16 ant_osB: 16 ant_raf: 47 java: 156 ant_os: 47 javaB: 31 ant_osB: 16 ant_raf: 47 java: 93 ant_os: 47 javaB: 32 ant_osB: 15 ant_raf: 47 java: 94 ant_os: 47 javaB: 31 ant_osB: 16 ant_raf: 46 java: 94 ant_os: 47 javaB: 31 ant_osB: 32 ant_raf: 250 java: 94 ant_os: 62 javaB: 16 ant_osB: 16 ant_raf: 31 java: 93 ant_os: 47 javaB: 16 ant_osB: 31 ant_raf: 31
最初の一行は、初期処理が入るためか、実行時間がけっこう長め。
それ以外でときどき大きくなるのは、GCが働いているのではないかと思う。
Javaのzipファイルの作成では、通常の圧縮(Deflate)と無圧縮で格納するだけ(Store)の二種類の方法(Method)がある。[2007-12-19]
あんまりStoreは使わないと思う(Storeの例もほとんど見たことない)ので、ここにメモしておく。
ZipOutputStream zos = new ZipOutputStream(new FileOutputStream(f)); zos.setMethod(ZipOutputStream.STORED); //デフォルトはDEFLATED
//1つめのファイルを格納 ZipEntry entry = new ZipEntry("test/a.txt"); //格納ファイル名 byte[] data = "aaaaaaaaaa".getBytes(); //格納データ entry.setSize(data.length); //データサイズをセット CRC32 crc = new CRC32(); crc.update(data); entry.setCrc(crc.getValue()); //CRCをセット zos.putNextEntry(entry); zos.write(data);
//2つめのファイルを格納 entry = new ZipEntry("test/b.txt"); data = "bbbbbbbbbb".getBytes(); entry.setSize(data.length * 10); crc.reset(); for(int i=0; i<10; i++) crc.update(data); entry.setCrc(crc.getValue(); zos.putNextEntry(entry); for(int i=0; i<10; i++) zos.write(data);
//終了 zos.close();
STOREDの場合、putNextEntry()を呼び出す前にデータサイズとCRCをセットしておく必要がある。(CRCはデータのチェックを行う為の値。いわゆるチェックサム)
これらの値は、データ本体の前のヘッダー部に出力される。
DEFLATEDの場合はCRCやデータサイズは自動的に計算されてデータ本体の直後(フッター部)に付加される。
データサイズもCRCも出力データを元に算出するのでwrite()が終わってからでないと値が分からない。しかしストリーム(OutputStream)は前に戻れないから、ヘッダー部には書かずにフッター部に書く。
STOREDの場合はそういったフッター部は使われないので、事前にセットしておく必要があるわけ。
ちなみにCRC32#update()は、一度にデータ全部を渡すのではなく、1バイトずつ渡したりバイト配列を分割して渡したりしてもよい。
CRC32のインスタンスは reset()を呼び出すとコンストラクターで生成した直後の状態に戻るので、使い回すことが出来る。
zipファイルを読み込むクラスには、ZipInputStreamとZipFileといった複数の種類がある。
Ant版ではZipFileだけある。ただしメソッド名はちょっと違う(Antの方が整合性がとれているかな)。
スピード的にはAnt版がちょっと遅いようだけど、どれも大差ない。(上で作ったファイルだと、どの方法でも16ms程度で読み込める)
パッケージ | 生成方法 | 概要 | 備考 |
---|---|---|---|
java.util.zip |
ZipInputStream(InputStream) |
標準版。入力ストリームから読み込む。 | ファイル名のエンコードはUTF-8。 |
ZipInputStream(InputStream,
Charset) |
ファイル名のエンコードを指定する。JDK1.7以降。[2014-04-16] | ||
java.util.zip |
ZipFile(File) |
標準版。ファイルから読み込む。 | ファイル名のエンコードはUTF-8。 |
ZipFile(File, Charset) |
ファイル名のエンコードを指定する。JDK1.7以降。[2014-04-16] | ||
org.apache.tools.zip |
ZipFile(File) |
Ant版。ファイルから読み込む。 | |
ZipFile(File, String) |
ファイル名のエンコードを指定する。 | ||
java.nio.file |
FileSystems.newFileSystem(String, ClassLoader) |
PathやFilesを使って読み込む。 | JDK1.7以降。[2015-12-12] →zipのFileSystemで読み込む例 |
Java入力ストリーム版の場合:
import java.util.zip.ZipEntry; import java.util.zip.ZipInputStream;
public static void decode(File file) throws Exception { byte[] buf = new byte[1024]; ZipInputStream zis = new ZipInputStream(new FileInputStream(file)); for (ZipEntry entry = zis.getNextEntry(); entry != null; entry = zis.getNextEntry()) { // System.out.println(entry.getName()); if (entry.isDirectory()) continue; for (;;) { int len = zis.read(buf); if (len < 0) break; //bufを使って処理 } } zis.close(); }
Javaファイル版の場合:
import java.util.zip.ZipEntry; import java.util.zip.ZipFile;
public static void decode(File file) throws Exception { byte[] buf = new byte[1024]; ZipFile zf = new ZipFile(file); for (Enumeration<? extends ZipEntry> e = zf.entries(); e.hasMoreElements();) { ZipEntry entry = e.nextElement(); // System.out.println(entry.getName()); if (entry.isDirectory()) continue; InputStream is = zf.getInputStream(entry); for (;;) { int len = is.read(buf); if (len < 0) break; //bufを使って処理 } is.close(); } zf.close(); }
↓JDK1.8では、Stream<ZipEntry>を返すstream()が使える。[2014-04-16]
public static void decode(File file) throws IOException { try (ZipFile zipFile = new ZipFile(file, Charset.forName("MS932"))) { zipFile.stream().filter(entry -> !entry.isDirectory()).forEach(entry -> { try (InputStream is = zipFile.getInputStream(entry)) { byte[] buf = new byte[1024]; for (;;) { int len = is.read(buf); if (len < 0) { break; } // bufを使って処理 } } catch (IOException e) { throw new UncheckedIOException(e); } }); } }
Antファイル版の場合:
import org.apache.tools.zip.ZipEntry; import org.apache.tools.zip.ZipFile;
public static void decode(File file) throws Exception { byte[] buf = new byte[1024]; ZipFile zf = new ZipFile(file, "MS932"); for (Enumeration e = zf.getEntries(); e.hasMoreElements();) { ZipEntry entry = (ZipEntry) e.nextElement(); // System.out.println(entry.getName()); if (entry.isDirectory()) continue; InputStream is = zf.getInputStream(entry); for (;;) { int len = is.read(buf); if (len < 0) break; //bufを使って処理 } is.close(); } zf.close(); }
JDK1.5以降のJava版では、ジェネリクスを使うように拡張されている。
Ant版は(ant用なので)そういった拡張はされないだろう。
読み込んだデータが正しいか(破損していないか)どうか、CRCを使ってチェックすることが出来る。[2008-12-21/2008-12-22]
/** * CRCチェック. * * @param file zipファイル * @param name zipファイル内のファイル名 * @return CRCが一致したとき、true */ public static boolean testCRC(File file, String name) throws Exception { ZipFile zf = new ZipFile(file); try { ZipEntry entry = zf.getEntry(name); InputStream is = zf.getInputStream(entry); byte[] buf = new byte[256]; CRC32 crc = new CRC32(); for (;;) { int len = is.read(buf); if (len < 0) break; crc.update(buf, 0, len); // int c = is.read(); // if (c < 0) break; // crc.update(c); } is.close(); long expected = entry.getCrc(); if (expected == -1) { //ZipEntryのCRCが不明の場合 return true; //判定できないけど、とりあえずtrueを返しておこうか } return crc.getValue() == expected; } finally { zf.close(); } }
参考: TNKソフトウェアさんの私的ZIPファイル研究所[暗号zipを復元する]
CheckedInputStreamを使ってCRCを計算させる方がちょっと楽?[2008-12-22]
/** * CRCチェック. * * @param file zipファイル * @param name zipファイル内のファイル名 * @return CRCが一致したとき、true */ public static boolean testCRC(File file, String name) throws Exception { ZipFile zf = new ZipFile(file); try { ZipEntry entry = zf.getEntry(name); CheckedInputStream is = new CheckedInputStream(zf.getInputStream(entry), new CRC32()); byte[] buf = new byte[256]; for (;;) { int len = is.read(buf); if (len < 0) break; // int c = is.read(); // if (c < 0) break; } is.close(); long expected = entry.getCrc(); if (expected == -1) { //ZipEntryのCRCが不明の場合 return true; //判定できないけど、とりあえずtrueを返しておこうか } return is.getChecksum().getValue() == expected; } finally { zf.close(); } }
CheckedInputStream#getChecksum()の戻り型はCheckSumインターフェースだが、CRC32クラスはそのインターフェースを実装している。
JDK1.7で、zipファイルをFileSystemクラスで読み書き出来るようになった。[2015-12-12]
(参考:『Java SE 7/8 速攻入門』)
import java.io.IOException; import java.io.InputStream; import java.nio.file.FileSystem; import java.nio.file.FileSystems; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths;
Path zipPath = Paths.get("〜/hoge.zip"); try (FileSystem fs = FileSystems.newFileSystem(zipPath, ClassLoader.getSystemClassLoader())) { Path path = fs.getPath("hoge.txt"); try (InputStream is = Files.newInputStream(path)) { 〜 } }
FileSystemsのnewFileSystemメソッドでFileSystemの具象クラス(zipファイルの場合、ZipFileSystem)が返る。
zipファイルの場合、第2引数のクラスローダーはnullでも大丈夫のようだ。
zipファイルでなかった場合(というか、該当するFileSystem具象クラスが見つからなかった場合)は「java.nio.file.ProviderNotFoundException: Provider not found
」という例外が発生する。
zipファイルかどうかの判断はファイルの拡張子でしているわけではなく、実際に中身がzipかどうかを見ているようだ。
zipファイル内のファイル(エントリー)は、ファイル名を指定してFileSystem#getPath()でPathとして取得する。
あとは、普通のファイルと同様にFilesを使って読み込むことが出来る。
zipファイル内の各ファイル(エントリー)のファイル名のエンコーディングはMap<String, Object> envによって指定する。[2015-12-12]
(デフォルトはUTF-8)
envを渡したい場合、zipファイルをPathのままで渡すメソッドは無い。URIに変換する必要がある。
ZipFileSystemの場合、URIのスキーマは「jar」。URIの本体が「file://〜」というファイルパスになる。
import java.io.IOException; import java.net.URI; import java.net.URISyntaxException; import java.nio.file.FileSystem; import java.nio.file.FileSystems; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.util.HashMap; import java.util.Map;
Path zipPath = Paths.get("D:/tmp/zip-encode-test.zip"); URI zipUri = new URI("jar", zipPath.toUri().toString(), null); // System.out.println(zipUri); // jar:file:///D:/tmp/zip-encode-test.zip // System.out.println(zipUri.getSchemeSpecificPart()); // file:///D:/tmp/zip-encode-test.zip Map<String, Object> env = new HashMap<>(); env.put("encoding", "MS932"); try (FileSystem fs = FileSystems.newFileSystem(zipUri, env, ClassLoader.getSystemClassLoader())) { Path path = fs.getPath("テスト1.txt"); Files.lines(path).forEach(System.out::println); }
zipファイル内のエントリー一覧を取得したい場合は、FileSystem#getRootDirectories()を使ってルートパスを取得し(メソッド名は複数形だが、実際には「/」ひとつだけが返ってくる)、Files.walk()等で一覧を取得する。[2015-12-12]
public static List<Path> zipEntries(Path zipFilePath) throws IOException { final List<Path> result = new ArrayList<>(); try (FileSystem fs = FileSystems.newFileSystem(zipFilePath, ClassLoader.getSystemClassLoader())) { for (Path rootPath : fs.getRootDirectories()) { Files.walkFileTree(rootPath, new SimpleFileVisitor<Path>() { @Override public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException { result.add(file); return FileVisitResult.CONTINUE; } }); } } return result; }
Path zipPath = Paths.get("〜/hoge.zip"); try (Stream<String> s = zipLines(zipPath, path -> path.toString().endsWith(".txt"))) { s.forEach(System.out::println); }
/** * zipファイル内の全ファイルの全テキストを返す * @param zipFilePath zipファイルのパス * @param pathPredicate 使用するzipエントリー(ファイル名)の条件 * @return テキスト行一覧 */ public static Stream<String> zipLines(Path zipFilePath, Predicate<Path> pathPredicate) throws IOException { final FileSystem fs = FileSystems.newFileSystem(zipFilePath, ClassLoader.getSystemClassLoader()); return StreamSupport.stream(fs.getRootDirectories().spliterator(), false) .onClose(() -> { // Stream終了時にFileSystemをクローズする try { fs.close(); } catch (IOException e) { throw new UncheckedIOException(e); } }).flatMap(rootPath -> { // zipファイル内のエントリー一覧を取得する try { return Files.walk(rootPath); } catch (IOException e) { throw new UncheckedIOException(e); } }).filter(path -> !Files.isDirectory(path)) .filter(pathPredicate) .flatMap(path -> { // zipエントリーをUTF-8のテキストファイルとしてオープンし、Stream<String>を返す try { return Files.lines(path); } catch (IOException e) { throw new UncheckedIOException(e); } }); }
ZipFileSystemを使ってzipファイルを作成および更新することが出来る。[2015-12-12]
import java.io.BufferedWriter; import java.io.IOException; import java.net.URI; import java.net.URISyntaxException; import java.nio.file.FileSystem; import java.nio.file.FileSystems; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.util.HashMap; import java.util.Map;
Path zipPath = Paths.get("D:/tmp/zip-write-test.zip");
URI zipUri = new URI("jar", zipPath.toUri().toString(), null);
Map<String, Object> env = new HashMap<>();
env.put("create", "true"); // zipファイルが存在しない場合、作成する
try (FileSystem fs = FileSystems.newFileSystem(zipUri, env, ClassLoader.getSystemClassLoader())) {
// ファイルを書き込む例
Path path1 = fs.getPath("test1.txt");
try (BufferedWriter br = Files.newBufferedWriter(path1)) {
br.write("test1");
}
// ディレクトリー付きファイルを書き込む例
Path path2 = fs.getPath("dir2", "test2.txt");
Files.createDirectories(path2.getParent());
try (BufferedWriter br = Files.newBufferedWriter(path2)) {
br.write("test2");
}
}
envを指定しない場合、zipファイルが存在していないとエラーになる。
envでcreateにtrueを指定すると、zipファイルが存在していない場合は新規に作成される。
zipファイルが存在していて、書き込もうとしたエントリー(ファイル)が既に存在している場合は、書き込もうとするとFileAlreadyExistsExceptionが発生する。
Files.exists()でファイル(エントリー)が存在するかどうか判定できるし、
Files.deleteIfExists()を使って存在する場合だけ削除することも出来る。
// ファイル(エントリー)が存在しない場合だけ書き込む Path path1 = fs.getPath("test1.txt"); if (!Files.exists(path1)) { try (BufferedWriter br = Files.newBufferedWriter(path1)) { br.write("test1"); } }
// ファイル(エントリー)が存在する場合は削除してから書き込む Path path1 = fs.getPath("test1.txt"); Files.deleteIfExists(path1); try (BufferedWriter br = Files.newBufferedWriter(path1)) { br.write("test1"); }
Files.copy()を使って、普通のディレクトリー内のファイルをzipファイル内にコピーすることも出来る。[2015-12-12]
import java.io.IOException; import java.net.URI; import java.net.URISyntaxException; import java.nio.file.FileSystem; import java.nio.file.FileSystems; import java.nio.file.FileVisitResult; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.SimpleFileVisitor; import java.nio.file.StandardCopyOption; import java.nio.file.attribute.BasicFileAttributes; import java.util.HashMap; import java.util.Map;
Path fromDir = Paths.get("D:/tmp/text"); Path zipPath = Paths.get("D:/tmp/zip-copy-test.zip"); copyToZip(fromDir, zipPath);
public static void copyToZip(Path fromDir, Path zipFilePath) throws URISyntaxException, IOException { Files.deleteIfExists(zipFilePath); final String zipRootDirName = fromDir.getFileName().toString(); URI zipUri = new URI("jar", zipFilePath.toUri().toString(), null); Map<String, Object> env = new HashMap<>(); env.put("create", "true"); try (FileSystem fs = FileSystems.newFileSystem(zipUri, env, ClassLoader.getSystemClassLoader())) { Files.walkFileTree(fromDir, new SimpleFileVisitor<Path>() { @Override public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException { Path relative = fromDir.relativize(file); // 相対パス取得 Path zipFile = fs.getPath(zipRootDirName, relative.toString()); Path parent = zipFile.getParent(); if (parent != null) { Files.createDirectories(parent); } Files.copy(file, zipFile, StandardCopyOption.COPY_ATTRIBUTES); return FileVisitResult.CONTINUE; } }); } }
zipファイルにはパスワードを付けることが出来るが、上記のクラスでは対応していない。[2007-12-14]
(javaではjarファイルとかを作るのが主目的だったので必要なかったのかな?)
そこで、Info-ZIPのzipcloak(zipファイルの暗号化・復号化を行うコマンドらしい)をJavaに移植してみました。