よんちゅBlog

― このブログは自分用のメモや日々の問題などを共有するためのものです ―

20121005185841 お知らせ:  2013/07/17 ブログデザインをリニューアルしました。

JavaでZipファイルを解凍(コピペ用)

Zip解凍処理などは一度作ると次に作ることはなかなかないもの。
初めて使う人や久しぶりにコードを書く人などはググってコピペすることが多いだろう。

というわけで、コピペ用のコードを残しておこうと思う。

Apache Antを使ってZipを解凍

Zipを扱う方法としては、Java標準にZipを扱うクラス群(java.util.zip)が存在するが、解凍・圧縮時共に文字コードUTF-8として扱ってしまうため、環境によっては文字化けが起きてしまう。

例えば、Windows上で圧縮したファイルまたはディレクトリの名前に日本語が含まれていた場合には正常に解凍できなかったりします。
この当たりの話は昔から有名ですね。

対策としては、Apache Ant(ant.jar) を使用する方法が一般的でしょう。
今回は2010/07/27時点で最新の1.8.1(apache-ant-1.8.1-bin.zip)を使用して確認しています。

というわけで、以下コピペ用サンプル

/**
 * zipファイルを解凍します。<br>
 * Ant1.8.1にて確認
 * 
 * @param zipFile
 *            解凍するZIPファイル
 * @param outputDir
 *            解凍先ディレクトリ
 * @param charset
 *            文字コード(ファイル名またはディレクトリ名に使用される文字コード)
 * 
 * @return 出力ディレクトリ直下に解凍されたファイルまたディレクトリのリスト
 * 
 * @throws FileNotFoundException
 * @throws ZipException
 * @throws IOException
 */
public static List<File> unZip(final File zipFile, final File outputDir,
	final String charset) throws FileNotFoundException, ZipException,
	IOException {
	if (zipFile == null) {
		throw new IllegalArgumentException("引数(zipFile)がnullです。");
	}
	if (outputDir == null) {
		throw new IllegalArgumentException("引数(outputDir)がnullです。");
	}
	if (charset == null || charset.isEmpty()) {
		throw new IllegalArgumentException("引数(charset)がnullまたは空文字です。");
	}
	if (outputDir.exists() && !outputDir.isDirectory()) {
		throw new IllegalArgumentException(
			"引数(outputDir)はディレクトリではありません。outputDir=" + outputDir);
	}

	// 出力ディレクトリ直下に解凍されたファイルまたディレクトリのセット
	final Set<File> fileSet = new HashSet<File>();

	// 解答したファイルの親ディレクトリのセット
	final Set<File> parentDirSet = new HashSet<File>();

	ZipFile zip = null;
	try {
		try {
			// 文字コードを指定することで文字化けを回避
			zip = new ZipFile(zipFile, charset);
		} catch (IOException e) {
			throw e;
		}

		final Enumeration<?> zipEnum = zip.getEntries();
		while (zipEnum.hasMoreElements()) {
			// 解凍するアイテムを取得
			final ZipEntry entry = (ZipEntry) zipEnum.nextElement();

			if (entry.isDirectory()) {
				// 解凍対象がディレクトリの場合
				final File dir = new File(outputDir, entry.getName());
				if (dir.getParentFile()
					.equals(outputDir)) {
					// 親ディレクトリが出力ディレクトリなのでfileSetに格納
					fileSet.add(dir);
				}
				// ディレクトリは自分で生成
				if (!dir.exists() && !dir.mkdirs()) {
					logger.error("ディレクトリの生成に失敗しました。dir=" + dir);
				}
			} else {
				// 解凍対象がファイルの場合
				final File file = new File(outputDir, entry.getName());
				final File parent = file.getParentFile();
				assert parent != null;

				if (parent.equals(outputDir)) {
					// 解凍ファイルの親ディレクトリが出力ディレクトリの場合
					fileSet.add(file);
				}

				if (!parentDirSet.contains(parent)) {
					// 親ディレクトリが初見の場合
					parentDirSet.add(parent);

					// 解凍ファイルの上位にある出力ディレクトリ直下のディレクトリを取得
					final File rootDir = getRootDir(outputDir, file);
					assert rootDir != null;
					fileSet.add(rootDir);

					// 親ディレクトリを生成
					if (!parent.exists() && !parent.mkdirs()) {
						logger.error("親ディレクトリの生成に失敗しました。parent=" + parent);
					}
				}

				// 解凍対象のファイルを書き出し
				FileOutputStream fos = null;
				InputStream is = null;
				try {
					fos = new FileOutputStream(file);
					is = zip.getInputStream(entry);

					byte[] buf = new byte[1024];
					int size = 0;
					while ((size = is.read(buf)) != -1) {
						fos.write(buf, 0, size);
					}
					fos.flush();
				} catch (FileNotFoundException e) {
					throw e;
				} catch (ZipException e) {
					throw e;
				} catch (IOException e) {
					throw e;
				} finally {
					if (fos != null) {
						try {
							fos.close();
						} catch (IOException e1) {
							logger.error("IOリソース開放失敗(FileOutputStream)", e1);
						}
					}
					if (is != null) {
						try {
							is.close();
						} catch (IOException e1) {
							logger.error("IOリソース開放失敗(InputStream)", e1);
						}
					}
				}
			}
		}
	} catch (FileNotFoundException e) {
		throw e;
	} catch (ZipException e) {
		throw e;
	} catch (IOException e) {
		throw e;
	} finally {
		if (zip != null) {
			try {
				zip.close();
			} catch (IOException e) {
				logger.error("IOリソース開放失敗(ZipFile)", e);
			}
		}
	}

	// Setだと何かと不便なのでListに変換
	List<File> retList = new ArrayList<File>(fileSet);
	// ソート:特に意味はなし
	Collections.sort(retList);

	return retList;
}

/**
 * zipファイルを解凍します。
 * 
 * @param zipFilePath
 *            解凍するZIPファイルのフルパス
 * @param outputDirPath
 *            解凍先ディレクトリのフルパス
 * @param charset
 *            文字コード(ファイル名またはディレクトリ名に使用される文字コード)
 * @return 出力ディレクトリ直下に解凍されたファイルまたディレクトリのリスト
 * 
 * @throws IOException
 */
public static List<File> unZip(final String zipFilePath,
	final String outputDirPath, final String charset) throws IOException {
	if (zipFilePath == null || zipFilePath.isEmpty()) {
		throw new IllegalArgumentException("引数(zipFilePath)がnullまたは空文字です。");
	}
	if (outputDirPath == null || outputDirPath.isEmpty()) {
		throw new IllegalArgumentException(
			"引数(outputDirPath)がnullまたは空文字です。");
	}
	return unZip(new File(zipFilePath), new File(outputDirPath), charset);
}


/**
 * 指定ディレクトリ直下にある、指定ファイルの親ディレクトリを再帰的に検索する。
 * 
 * 
 * @param dir
 * @param file
 * @return
 */
private static File getRootDir(final File dir, final File file) {
	assert dir != null;
	assert !dir.exists() || dir.exists() && dir.isDirectory();
	assert file != null;

	final File parent = file.getParentFile();
	if (parent == null) {
		return null;
	}
	if (parent.equals(dir)) {
		return file;
	}
	return getRootDir(dir, parent);
}
/** インポート文
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Enumeration;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.zip.ZipException;

import org.apache.commons.logging.Log;
import org.apache.tools.zip.ZipEntry;
import org.apache.tools.zip.ZipFile;
*/

補足

少し変わった点として、戻り値として「出力ディレクトリに指定したディレクトリ"直下"に解凍されたファイルまたはディレクトリのFileオブジェクトリスト」を返すようになっています。

コード上で、何もしないにも関わらず例外を補足(catch)して再スローしている箇所がありますが、これはどこで何の例外が発生するのかソースを見て分かるようにするために、わざと書いています。

実際に使用する場合は、「FileNotFoundException」や「ZipException」、「IOException」は補足(catch)せずに、throws宣言するだけという方法でも良いでしょう。

また、throws宣言する場合も今回のケースだと別々にthrowsしてもあまり意味がないので「throws IOException」だけでも良いと思います。

今回のサンプルでは、引数のチェックやclose()時の例外情報の記録などを行っていますが、プログラミングの書籍その他Web上のサンプルでは、紙面上の都合(スペースなど)や、それが本質ではないからなどの理由により省略されていることがほんとんどです。

経験者などから見ればそれで良いのですが、初学者にとってはあまり良いことではないでしょう。

ということで、今回はその辺を考慮してサンプルとしてはかなり長めのコードとなっていますが、ご了承下さい。