Java7 から、標準ライブラリで SJIS ファイル名の ZIP圧縮/解答 ができるようになった → JavaSE ZIP圧縮/解凍 Java7


概要

  1. 圧縮
    • ZipOutputStream?に対してファイルを登録します
    • ファイルを1つ登録するときには以下の手順で登録します
      1. ZipOutputStream?#putNextEntry?() で、ファイルエントリーを登録します。
      2. ZipOutputStream?#write() で、データを書き込みます。
      3. データの CRC および SIZE を Entry に書き込みます。
      4. ZipOutputStream?#closeEntry() で、ファイルエントリーを閉じます。
    • 中身が空のディレクトリを登録するには
      1. ファイルエントリーに登録するファイル名の最後を "/" にします。
      2. データを書き込まずに ZipOutputStream?#closeEntry()で、ファイルエントリーを閉じます。
        deflate.png
  2. 展開
    • ZipInputStream?からファイルを読み取ります。
    • ZipInputStream?#getNextEntry?() で、ファイルエントリーが得られます。
    • ZipInputStream?#read() で、このファイルエントリーに対するデータが得られます。
    • ZipInputStream? が EOF に達したら、このファイルエントリーのデータが終わり。
    • 次のエントリーを読み込む。
      inflate.png

ソースコード

package com.snail.zip;

import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.zip.CRC32;
import java.util.zip.CheckedInputStream;
import java.util.zip.CheckedOutputStream;
import java.util.zip.Deflater;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;
import java.util.zip.ZipOutputStream;
// Zipファイルに日本語のファイル名を格納したいときには、ant.jar にある
// 以下のクラスに差し替える
// import org.apache.tools.zip.ZipEntry;
// import org.apache.tools.zip.ZipOutputStream;

public class ZipUtil {
  /**
   * 登録できるディレクトリの最大深さ(シンボリックリンクによる循環参照を防ぐため)
   */
  private static final int MAX_DEPTH = 255;

  /**
   * ファイルを読み込むときのバッファイサイズ
   */
  private static final int BUF_SIZE = 1024;

  /**
   * 詳細ログ出力モード
   */
  private static boolean verboseMode = false;

  /**
   * @param args
   */
  public static void main(String[] args) {

    verboseMode = true;

    try {
      if ("-c".equals(args[0])) {
        // 圧縮モード
        String[] targetFiles = new String[args.length - 2];
        System.arraycopy(args, 2, targetFiles, 0, targetFiles.length);
        deflate(new FileOutputStream(args[1]), targetFiles);
        return;
      } else if ("-x".equals(args[0])) {
        // 展開モード
        inflate(new FileInputStream(args[1]));
        return;
      }
    } catch (Exception e) {
      e.printStackTrace();
    }
    System.out
        .println("[Usage] java com.snail.zip.ZipUtil -[cx] [zip file] [source files]");

  }

  /**
   * ファイルをZIP形式で圧縮します.
   * 
   * <pre>
   *           deflate(os, targetFileNames, Deflater.DEFAULT_COMPRESSION)
   *          と同じです
   * </pre>
   * 
   * @param os
   *            圧縮結果を出力する OutputStream
   * @param targetFileNames
   *            圧縮するファイル名群
   * @throws IOException
   *             圧縮に失敗
   */
  public static void deflate(OutputStream os, String[] targetFileNames)
      throws IOException {
    deflate(os, targetFileNames, Deflater.DEFAULT_COMPRESSION);
  }

  /**
   * ファイルをZIP形式で圧縮します.
   * 
   * @param os
   *            圧縮結果を出力する OutputStream
   * @param targetFileNames
   *            圧縮するファイル名群
   * @param leve
   *            圧縮レベル(Deflater.BEST_SPEED(=0)〜Deflater.BEST_COMPRESSION(9))
   * @throws IOException
   *             圧縮に失敗
   */
  public static void deflate(OutputStream os, String[] targetFileNames,
      int level) throws IOException {

    ZipOutputStream zipOut = new ZipOutputStream(new BufferedOutputStream(
        os));

    zipOut.setLevel(level);

    // java.util.zip ではファイル名はUTF-8決めうち
    // org.apache.tools.zip ではファイル名の文字コードを指定できる
    // zipOut.setEncoding("Windows-31J"); 

    for (int i = 0; i < targetFileNames.length; i++) {
      File file = new File(targetFileNames[i]);
      entryFile(zipOut, file, 0);
    }
    zipOut.close();
  }

  /**
   * ファイルをZIP形式で展開します.
   * 
   * @param is
   *            展開内容を入力する InputStream
   * @throws IOException
   *             展開に失敗
   */
  public static void inflate(InputStream is) throws IOException {
    ZipInputStream zipInp = new ZipInputStream(new BufferedInputStream(is));
    ZipEntry entry = null;
    while ((entry = zipInp.getNextEntry()) != null) {
      if (entry.isDirectory()) {
        new File(entry.getName()).mkdirs();
      } else {
        new File(new File(entry.getName()).getParent()).mkdirs();
        CheckedOutputStream out = new CheckedOutputStream(
            new BufferedOutputStream(new FileOutputStream(entry
                .getName())), new CRC32());
        byte[] buf = new byte[BUF_SIZE];
        int writeSize = 0;
        int totalSize = 0;
        while ((writeSize = zipInp.read(buf)) != -1) {
          totalSize += writeSize;
          out.write(buf, 0, writeSize);
        }
        out.close();

        verbose(entry);
        if (entry.getSize() != totalSize) {
          verbose("格納サイズが違います");
        }
        if (entry.getCrc() != out.getChecksum().getValue()) {
          verbose("チェックサムが違います");
        }
      }
      zipInp.closeEntry();
    }
    zipInp.close();
  }

  /**
   * ZIPにファイルを登録します.
   * 
   * <pre>
   *           ZIPファイルにエントリを登録します。
   *           登録しようとしているファイルがディレクトリの場合には、
   *           再帰的に中身をエントリします。
   * </pre>
   * 
   * @param zip
   *            ZipOutputStream
   * @param targetFile
   *            エントリするファイル
   * @param depth
   *            現在登録しようとしているディレクトリの深さ
   * @throws IOException
   *             エントリの登録に失敗
   */
  private static void entryFile(final ZipOutputStream zipOut,
      final File targetFile, final int depth) throws IOException {

    if (depth > MAX_DEPTH) {
      throw new IOException("格納しようとしているディレクトリの構造が深すぎます。"
          + "シンボリックリンクによる循環参照が起きているのかもしれません。");
    }
    if (targetFile.isDirectory()) {
      // エントリ内容はディレクトリ
      File[] targetFiles = targetFile.listFiles();

      if (targetFiles.length == 0) {
        // エントリ内容は空のディレクトリ(ファイルパスの末尾を"/"にしてエントリ)
        entry(zipOut, getFilePath(targetFile) + "/", null);
      } else {
        // エントリ内容が中身のあるディレクトリの場合には
        // このディレクトリ自体はエントリせず再帰的に中身をエントリする
        for (int i = 0; i < targetFiles.length; i++) {
          entryFile(zipOut, targetFiles[i], depth + 1);
        }
      }
    } else {
      // エントリ内容はファイル
      entry(zipOut, getFilePath(targetFile), new FileInputStream(
          targetFile));
    }
  }

  /**
   * ZIPにデータを登録します.
   * 
   * <pre>
   *      InputStreamから出力されるデータをZIPに登録します。
   *      このとき、CRC、SIZEも計算して登録します。
   *      
   *      InputStreamに、nullを設定するとディレクトリエントリと見なして
   *      データは登録しません。
   * </pre>
   * 
   * @param zipOut
   *            ZipOutputStream
   * @param name
   *            エントリするファイル名
   * @param entryIs
   *            エントリするデータを供給するInputStream
   * @throws IOException
   *             エントリの登録に失敗
   */
  private static void entry(final ZipOutputStream zipOut, final String name,
      final InputStream entryIs) throws IOException {
    ZipEntry entry = new ZipEntry(name);

    zipOut.putNextEntry(entry);

    int totalSize = 0;

    if (entryIs != null) {

      byte buf[] = new byte[BUF_SIZE];
      int readSize;
      CheckedInputStream in = new CheckedInputStream(
          new BufferedInputStream(entryIs), new CRC32());
      while ((readSize = in.read(buf, 0, BUF_SIZE)) != -1) {
        totalSize += readSize;
        zipOut.write(buf, 0, readSize);
      }
      in.close();
      entry.setCrc(in.getChecksum().getValue());
    }

    entry.setSize(totalSize);
    entry.setCompressedSize(totalSize); // Javadocによると此処にも圧縮解除時の容量を入れる

    verbose(entry);
    zipOut.closeEntry();

  }

  /**
   * ファイルのパス名を得る.
   * 
   * <pre>
   *          バックスラッシュ(\)をスラッシュ(/)に変更します
   * </pre>
   * 
   * @param file
   *            ファイル
   * @return 引数ファイルのパス名
   */
  private static String getFilePath(File file) {

   String rootPath = root.getAbsolutePath();
   String entryPath = file.getAbsolutePath();
   String retPath;
   
   // 基底ディレクトリがファイルと同じ場合 (ファイル名を指定された場合)
   // にはファイル名を返す。
   if (rootPath.equals(entryPath)) {
     retPath = file.getName();
   } else {
     retPath = entryPath.replace(rootPath, "").replaceAll("\\\\", "/");
   }
   
   // Windows XP/Vista の標準の展開ツールは先頭が"/"だとファイル名として認識しないための対策
   if (retPath.charAt(0) == '/') {
     retPath = retPath.substring(1);
   }
   
   return retPath;
 }

  /**
   * 詳細ログを出力します.
   * 
   * <pre>
   *        static変数の verbose_mode が、true のときに詳細ログを出力します。
   * </pre>
   * 
   * @param obj
   *            出力対象のオブジェクト
   */
  private static void verbose(Object obj) {

    if (verboseMode) {

      if (obj instanceof ZipEntry) {
        ZipEntry zipEntry = (ZipEntry) obj;
        System.out.println(zipEntry.getName() + "\tSize:"
            + zipEntry.getSize() + "\tCRC32:" + zipEntry.getCrc());
      }
      System.out.println(obj);
    }
  } 
}

実行結果

  1. 圧縮
    C:\>java com.snail.zip.ZipUtil -c a.zip Infineon
    Infineon/0x0404.ini     Size:3187       CRC32:3437384706
    Infineon/0x0404.ini
    Infineon/0x0407.ini     Size:5050       CRC32:4276019520
    Infineon/0x0407.ini
    Infineon/0x0409.ini     Size:4346       CRC32:21103272
    Infineon/0x0409.ini
    Infineon/0x040a.ini     Size:5101       CRC32:2468132550
    Infineon/0x040a.ini
    Infineon/0x040c.ini     Size:5262       CRC32:2174859066
    ...
  2. 展開
    C:\temp>java com.snail.zip.ZipUtil -x ..\a.zip
    Infineon/0x0404.ini     Size:3187       CRC32:3437384706
    Infineon/0x0404.ini
    Infineon/0x0407.ini     Size:5050       CRC32:4276019520
    Infineon/0x0407.ini
    Infineon/0x0409.ini     Size:4346       CRC32:21103272
    Infineon/0x0409.ini
    Infineon/0x040a.ini     Size:5101       CRC32:2468132550
    Infineon/0x040a.ini
    Infineon/0x040c.ini     Size:5262       CRC32:2174859066
    ...

日本語対応

  1. 問題
    • JDK付属の java.util.zip では、エントリファイル名を UTF-8 で格納する
      • ZIP(PKZIP)の規格上は、エントリファイル名はバイナリでどの文字コードを使うかは規定されていない
      • 慣用的に、日本ではエントリファイル名は Shift-JIS で格納されている
    • つまり、java.util.zip で作った ZIP ファイルを 一般的な解凍ツールで解凍すると日本語部分のファイル名が文字化けしてしまう
      zip_garbledChar.png
  2. 解決法
    • java.util.zip の代わりに、Apache Antの org.apache.tools.zip を使えばよい。(ant.jar を classpath に登録)
    • 両者には継承関係はないが、同じメソッドをそろえているのでパッケージ宣言のみを換えるやれほぼそのまま動く。
      - import java.util.zip.ZipEntry;
      - import java.util.zip.ZipOutputStream;
      + import org.apache.tools.zip.ZipEntry;
      + import org.apache.tools.zip.ZipOutputStream;
    • org.apache.tools.zip.ZipOutputStream? には、ファイル名の文字コードを指定するためのメソッドが用意されているので、それを使ってファイル名の文字コードを指定してやる。特に指定しなければ(System.getProperty("file.encoding"))が使われるようだ。
      ZipOutputStream#setEncoding("Windows-31J");
    • org.apache.tools.zip を使って圧縮した ZIPファイルを展開すると、以下のようにきちんとしたファイル名で展開される
      zip_correctChar.png

各国ではどのような文字コードが ZIP ファイルの内部ファイル名に使われているのか


Java#JavaSE


添付ファイル: filezip_garbledChar.png 1623件 [詳細] filedeflate.png 1650件 [詳細] filezip_correctChar.png 1617件 [詳細] fileinflate.png 1948件 [詳細]

トップ   編集 凍結 差分 バックアップ 添付 複製 名前変更 リロード   新規 一覧 単語検索 最終更新   ヘルプ   最終更新のRSS   sitemap
Last-modified: 2013-11-24 (日) 15:46:13 (1111d)
ISBN10
ISBN13
9784061426061