緣由: java對於文字的編碼是以unicode為基礎,因此,若是以ZipInputStream及ZipOutputStream來處理壓縮及解壓縮的工作,碰到中文檔名或路徑,那當然是以unicode來處理羅。 但是,現在市面上的壓縮及解壓縮軟體,例如winzip,卻是不支援unicode的,一碰到檔名以unicode編碼的檔案,它就不處理。
那要如何才能做出讓winzip能夠處理的壓縮檔呢。 有兩種方式:
一種是使用apache的ant實現zip解壓縮,另一種是修改jdk內建zip工具類的源碼
因為ant內部是多線程讀取檔案,解壓的檔案雖然是亂序的,但是效率明顯比jdk的zip方式高很多。推薦使用ant的zip實現。
第一種使用ant實現的zip解壓縮,其中解壓的亂碼注意使用 public void unZip(String unZipFileName,String outputPath) 其中
this.zipFile = new ZipFile(unZipFileName, "GB18030");是解決中文名亂碼的關鍵。
import java.io.*;import org.apache.tools.zip.*;import java.util.Enumeration;/** *<p> * <b>功能:zip壓縮、解壓(支援中文檔案名稱)</b> *<p> * 說明:使用Apache Ant提供的zip工具org.apache.tools.zip實現zip壓縮和解壓功能. * 解決了由於java.util.zip包不支援漢字的問題。 * * @author Winty * @modifier vernon.zheng */public class AntZip {private ZipFile zipFile;private ZipOutputStream zipOut; // 壓縮Zipprivate ZipEntry zipEntry;private static int bufSize; // size of bytesprivate byte[] buf;private int readedBytes;// 用於壓縮中。要去除的絕對父路路徑,目的是將絕對路徑變成相對路徑。private String deleteAbsoluteParent;/** *構造方法。預設緩衝區大小為512位元組。 */public AntZip() {this(512);}/** *構造方法。 * * @param bufSize * 指定壓縮或解壓時的緩衝區大小 */public AntZip(int bufSize) {this.bufSize = bufSize;this.buf = new byte[this.bufSize];deleteAbsoluteParent = null;}/** *壓縮檔夾內的所有檔案和目錄。 * * @param zipDirectory * 需要壓縮的檔案夾名 */public void doZip(String zipDirectory) {File zipDir = new File(zipDirectory);doZip(new File[] { zipDir }, zipDir.getName());}/** *壓縮多個檔案或目錄。可以指定多個單獨的檔案或目錄。而 <code>doZip(String zipDirectory)</code> * 則直接壓縮整個檔案夾。 * * @param files * 要壓縮的檔案或目錄組成的<code>File</code>數組。 *@param zipFileName * 壓縮後的zip檔案名稱,如果尾碼不是".zip", 自動添加尾碼".zip"。 */public void doZip(File[] files, String zipFileName) {// 未指定壓縮檔名,預設為"ZipFile"if (zipFileName == null || zipFileName.equals(""))zipFileName = "ZipFile";// 添加".zip"尾碼if (!zipFileName.endsWith(".zip"))zipFileName += ".zip";try {this.zipOut = new ZipOutputStream(new BufferedOutputStream(new FileOutputStream(zipFileName)));compressFiles(files, this.zipOut, true);this.zipOut.close();} catch (IOException ioe) {ioe.printStackTrace();}}/** *壓縮檔和目錄。由doZip()調用 * * @param files * 要壓縮的檔案 *@param zipOut * zip輸出資料流 *@param isAbsolute * 是否是要去除的絕對路徑的根路徑。因為compressFiles() * 會遞迴地被調用,所以只用deleteAbsoluteParent不行。必須用isAbsolute來指明 * compressFiles()是第一次調用,而不是後續的遞迴調用。即如果要壓縮的路徑是 * E:\temp,那麼第一次調用時,isAbsolute=true,則deleteAbsoluteParent會記錄 * 要刪除的路徑就是E:\ ,當壓縮子目錄E:\temp\folder時,isAbsolute=false, * 再遞迴調用compressFiles()時,deleteAbsoluteParent仍然是E:\ 。從而保證了 * 將E:\temp及其子目錄均正確地轉化為相對目錄。這樣壓縮才不會出錯。不然絕對 路徑E:\也會被寫入到壓縮檔中去。 */private void compressFiles(File[] files, ZipOutputStream zipOut,boolean isAbsolute) throws IOException {for (File file : files) {if (file == null)continue; // 空的檔案對象// 刪除絕對父路徑if (file.isAbsolute()) {if (isAbsolute) {deleteAbsoluteParent = file.getParentFile().getAbsolutePath();deleteAbsoluteParent = appendSeparator(deleteAbsoluteParent);}} elsedeleteAbsoluteParent = "";if (file.isDirectory()) {// 是目錄compressFolder(file, zipOut);} else {// 是檔案compressFile(file, zipOut);}}}/** *壓縮檔或空目錄。由compressFiles()調用。 * * @param file * 需要壓縮的檔案 *@param zipOut * zip輸出資料流 */public void compressFile(File file, ZipOutputStream zipOut)throws IOException {String fileName = file.toString();/* 去除絕對父路徑。 */if (file.isAbsolute())fileName = fileName.substring(deleteAbsoluteParent.length());if (fileName == null || fileName == "")return;/* * 因為是空目錄,所以要在結尾加一個"/"。 不然就會被當作是空檔案。 ZipEntry的isDirectory()方法中,目錄以"/"結尾. * org.apache.tools.zip.ZipEntry : public boolean isDirectory() { return * getName().endsWith("/"); } */if (file.isDirectory())fileName = fileName + "/";// 此處不能用"\\"zipOut.putNextEntry(new ZipEntry(fileName));// 如果是檔案則需讀;如果是空目錄則無需讀,直接轉到zipOut.closeEntry()。if (file.isFile()) {FileInputStream fileIn = new FileInputStream(file);while ((this.readedBytes = fileIn.read(this.buf)) > 0) {zipOut.write(this.buf, 0, this.readedBytes);}fileIn.close();}zipOut.closeEntry();}/** *遞迴完成目錄檔案讀取。由compressFiles()調用。 * * @param dir * 需要處理的檔案對象 *@param zipOut * zip輸出資料流 */private void compressFolder(File dir, ZipOutputStream zipOut)throws IOException {File[] files = dir.listFiles();if (files.length == 0)// 如果目錄為空白,則單獨壓縮空目錄。compressFile(dir, zipOut);else// 如果目錄不為空白,則分別處理目錄和檔案.compressFiles(files, zipOut, false);}/** *解壓指定zip檔案。 * * @param unZipFileName * 需要解壓的zip檔案名稱 */public void unZip(String unZipFileName) {FileOutputStream fileOut;File file;InputStream inputStream;try {this.zipFile = new ZipFile(unZipFileName);for (Enumeration entries = this.zipFile.getEntries(); entries.hasMoreElements();) {ZipEntry entry = (ZipEntry) entries.nextElement();file = new File(entry.getName());if (entry.isDirectory()) {// 是目錄,則建立之file.mkdirs();} else {// 是檔案// 如果指定檔案的父目錄不存在,則建立之.File parent = file.getParentFile();if (parent != null && !parent.exists()) {parent.mkdirs();}inputStream = zipFile.getInputStream(entry);fileOut = new FileOutputStream(file);while ((this.readedBytes = inputStream.read(this.buf)) > 0) {fileOut.write(this.buf, 0, this.readedBytes);}fileOut.close();inputStream.close();}}this.zipFile.close();} catch (IOException ioe) {ioe.printStackTrace();}}/** *解壓指定zip檔案。其中"GB18030"解決中文亂碼 * * @param unZipFileName * 需要解壓的zip檔案名稱 * @param outputPath * 輸出路徑 */public void unZip(String unZipFileName,String outputPath) {FileOutputStream fileOut;File file;InputStream inputStream;try {this.zipFile = new ZipFile(unZipFileName, "GB18030");for (Enumeration entries = this.zipFile.getEntries(); entries.hasMoreElements();) {ZipEntry entry = (ZipEntry) entries.nextElement();file = new File(outputPath+entry.getName());if (entry.isDirectory()) {// 是目錄,則建立之file.mkdirs();} else {// 是檔案// 如果指定檔案的父目錄不存在,則建立之.File parent = file.getParentFile();if (parent != null && !parent.exists()) {parent.mkdirs();}inputStream = zipFile.getInputStream(entry);fileOut = new FileOutputStream(file);while ((this.readedBytes = inputStream.read(this.buf)) > 0) {fileOut.write(this.buf, 0, this.readedBytes);}fileOut.close();inputStream.close();}}this.zipFile.close();} catch (IOException ioe) {ioe.printStackTrace();}}/** *給檔案路徑或目錄結尾添加File.separator * * @param fileName * 需要添加路徑分割符的路徑 *@return 如果路徑已經有分割符,則原樣返回,否則添加分割符後返回。 */private String appendSeparator(String path) {if (!path.endsWith(File.separator))path += File.separator;return path;}/** *解壓指定zip檔案。 * * @param unZipFile * 需要解壓的zip檔案對象 */public void unZip(File unZipFile) {unZip(unZipFile.toString());}/** *設定壓縮或解壓時緩衝區大小。 * * @param bufSize * 緩衝區大小 */public void setBufSize(int bufSize) {this.bufSize = bufSize;}// 主函數,用於測試AntZip類/* * public static void main(String[] args)throws Exception{ * if(args.length>=2){ AntZip zip = new AntZip(); * * if(args[0].equals("-zip")){ //將後續參數全部轉化為File對象 File[] files = new File[ * args.length - 1]; for(int i = 0;i < args.length - 1; i++){ files = new * File(args[i + 1]); } * * //將第一個檔案名稱作為zip檔案名稱 zip.doZip(files , files[0].getName()); * * return ; } else if(args[0].equals("-unzip")){ zip.unZip(args[1]); return * ; } } * * System.out.println("Usage:"); * System.out.println("壓縮:java AntZip -zip [directoryName | fileName]... "); * System.out.println("解壓:java AntZip -unzip fileName.zip"); } */}
第二種 從修改ZipInputStream及ZipOutputStream對於檔名的編碼方式來著手了。
我們可以從jdk的src.zip取得ZipInputStream及ZipOutputStream的原始碼來加以修改:
一、ZipOutputStream.java
1.從jdk的src.zip取得ZipOutputStream.java原始碼,另存新檔存到c:/java/util/zip這個資料夾裡,檔名改為CZipOutputStream.java。
2.開始修改原始碼,將class名稱改為CZipOutputStream
3.建構式也必須更改為CZipOutputStream
4.新增member,這個member記錄編碼方式
private String encoding="UTF-8";
5.再新增一個建構式(這個建構式可以讓這個class在new的時候,設定檔名的編碼)
public CZipOutputStream(OutputStream out,String encoding) { super(out, new Deflater(Deflater.DEFAULT_COMPRESSION, true)); usesDefaultDeflater = true; this.encoding=encoding; }
6.找到byte[] nameBytes = getUTF8Bytes(e.name);(有二個地方),將它修改如下:
byte[] nameBytes = null; try { if (this.encoding.toUpperCase().equals("UTF-8")) nameBytes =getUTF8Bytes(e.name); else nameBytes= e.name.getBytes(this.encoding); } catch(Exception byteE) { nameBytes=getUTF8Bytes(e.name); }
7.將檔案儲存在c:/java/util/zip這個資料夾內,請記得一定要有這個路徑結構,
才能把CZipOutputStream.class放在正確的package結構裡
二、ZipInputStream.java
1.從jdk的src.zip取得ZipInputStream.java原始碼,另存新檔存到c:/java/util/zip這個資料夾裡,檔名改為CZipInputStream.java。
2.開始修改原始碼,將class名稱改為CZipInputStream
3.建構式也必須更改為CZipInputStream
4.新增member,這個member記錄編碼方式
private String encoding="UTF-8";
5.再新增一個建構式如下(這個建構式可以讓這個class在new的時候,設定檔名的編碼)
public CZipInputStream(InputStream in,String encoding) { super(new PushbackInputStream(in,512),new Inflater(true),512); usesDefaultInflater = true; if(in == null) { throw new NullPointerException("in is null"); } this.encoding=encoding; }
6.找到ZipEntry e = createZipEntry(getUTF8String(b, 0, len));這一行,將它改成如下:
ZipEntry e=null; try { if (this.encoding.toUpperCase().equals("UTF-8")) e=createZipEntry(getUTF8String(b, 0, len)); else e=createZipEntry(new String(b,0,len,this.encoding)); } catch(Exception byteE) { e=createZipEntry(getUTF8String(b, 0, len)); }
7.將檔案儲存在c:/java/util/zip這個資料夾內,請記得一定要有這個路徑結構,才能把CZipInputStream.class放在正確的package結構裡
以上兩個檔案儲存後compile產生CZipOutputStream.class及CZipInputStream.class,使用winzip開啟[java_home]/jre/lib/rt.jar這個檔案,將CZipOutputStream.class及CZipInputStream.class加進去,記得「Save full path info」一定要打勾。
以後當壓縮及解壓縮時有中文檔名及路徑的問題時,就可以指定編碼方式來處理了。
CZipOutputStream zos=new CZipOutputStream(OutputStream os,String encoding);CZipInputStream zins=new CZipInputStream(InputStream ins,String encoding);
以「壓縮與解壓縮(1)」為例:
FileOutputStream fos =new FileOutputStream(request.getRealPath("/")+"myzip.zip");CZipOutputStream zos=new CZipOutputStream(fos,"GBK");
其他地方都不用改,便可以處理中文檔名的壓縮。