首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> Java 程式設計 >> Java 程式設計入門

Java 解壓 ZIP 檔案完美解決中文亂碼:自定義 ZipInputStream 支援 GBK 編碼完整實戰

     編輯:Java 程式設計入門

問題背景

JDK 內建 java.util.zip.ZipInputStream 僅支援 UTF-8 檔名編碼。Windows 系統建立的 ZIP 檔案檔名通常採用 GBK(CP936)編碼,直接解壓會拋出 IllegalArgumentException: MALFORMED 或產生亂碼目錄。根因在於 ZipEntry 解析檔名時硬編碼使用 UTF-8。

解決方案:複製並修改 JDK 內部類

由於 JDK 封裝了 java.util.zip 內部類(ZipConstants、ZipEntry、ZipInputStream、InflaterInputStream、DeflaterOutputStream、ZipOutputStream),且 ZipInputStream 為 final 無法繼承,唯一可行方案是完整複製原始碼到自定義包(如 cn.edu.xidian.crytoll),修改檔名解碼邏輯支援 GBK。

核心修改點

1. ZipEntry 檔名解碼

原始碼中 ZipEntry 建構子解析檔名:

// 原始 JDK (簡化)
this.name = new String(buf, 0, len, StandardCharsets.UTF_8);

修改為:

// 自定義 ZipEntry.java
// 嘗試 UTF-8,失敗則回退 GBK
String name;
try {
    name = new String(buf, 0, len, "UTF-8");
    // 簡易驗證:含不可見控制字元視為解碼錯誤
    if (name.matches(".*[\\p{Cntrl}&&[^\\t\\n\\r]].*")) throw new Exception();
} catch (Exception e) {
    name = new String(buf, 0, len, "GBK"); // Windows 中文預設
}
this.name = name;

2. ZipInputStream 讀取 Entry 時使用修改後的 ZipEntry

確保 getNextEntry() 返回的 ZipEntry 已正確解碼檔名。

3. 相關依賴類同步複製

  • ZipConstants:常數定義(LOCSIG, CENSIG 等)。
  • InflaterInputStream:解壓輸入流基類。
  • DeflaterOutputStream:壓縮輸出流基類(附件包含完整原始碼)。
  • ZipOutputStream:壓縮寫入(同理需修改寫入編碼)。

附件:DeflaterOutputStream.java 完整原始碼(修改版)

package cn.edu.xidian.crytoll;

import java.io.FilterOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.util.zip.Deflater;

public class DeflaterOutputStream extends FilterOutputStream {
    protected Deflater def;
    protected byte[] buf;
    private boolean closed = false;
    boolean usesDefaultDeflater = false;

    public DeflaterOutputStream(OutputStream out, Deflater def, int size) {
        super(out);
        if (out == null || def == null) throw new NullPointerException();
        if (size <= 0) throw new IllegalArgumentException("buffer size <= 0");
        this.def = def;
        buf = new byte[size];
    }

    public DeflaterOutputStream(OutputStream out, Deflater def) {
        this(out, def, 512);
    }

    public DeflaterOutputStream(OutputStream out) {
        this(out, new Deflater());
        usesDefaultDeflater = true;
    }

    public void write(int b) throws IOException {
        byte[] buf = new byte[1];
        buf[0] = (byte)(b & 0xff);
        write(buf, 0, 1);
    }

    public void write(byte[] b, int off, int len) throws IOException {
        if (def.finished()) throw new IOException("write beyond end of stream");
        if ((off | len | (off + len) | (b.length - (off + len))) < 0)
            throw new IndexOutOfBoundsException();
        if (len == 0) return;
        if (!def.finished()) {
            int stride = buf.length;
            for (int i = 0; i < len; i += stride) {
                def.setInput(b, off + i, Math.min(stride, len - i));
                while (!def.needsInput()) deflate();
            }
        }
    }

    public void finish() throws IOException {
        if (!def.finished()) {
            def.finish();
            while (!def.finished()) deflate();
        }
    }

    public void close() throws IOException {
        if (!closed) {
            finish();
            if (usesDefaultDeflater) def.end();
            out.close();
            closed = true;
        }
    }

    protected void deflate() throws IOException {
        int len = def.deflate(buf, 0, buf.length);
        if (len > 0) out.write(buf, 0, len);
    }
}

使用範例

import cn.edu.xidian.crytoll.ZipInputStream; // 自定義版
import java.io.*;

public class UnzipGBK {
    public static void main(String[] args) throws IOException {
        File zipFile = new File("中文檔名.zip");
        File destDir = new File("output");
        destDir.mkdirs();

        try (FileInputStream fis = new FileInputStream(zipFile);
             ZipInputStream zis = new ZipInputStream(fis, Charset.forName("GBK"))) { // 假設構造子支援 Charset

            ZipEntry entry;
            while ((entry = zis.getNextEntry()) != null) {
                File outFile = new File(destDir, entry.getName());
                if (entry.isDirectory()) {
                    outFile.mkdirs();
                } else {
                    outFile.getParentFile().mkdirs();
                    try (FileOutputStream fos = new FileOutputStream(outFile)) {
                        byte[] buf = new byte[8192];
                        int len;
                        while ((len = zis.read(buf)) > 0) fos.write(buf, 0, len);
                    }
                }
                zis.closeEntry();
            }
        }
    }
}

現代化替代方案(推薦)

方案優勢適用場景
Apache Commons Compress成熟、支援 ZIP/7Z/TAR/GZIP/BZIP2、自動檢測編碼、支援 Zip64、加密、分卷企業級專案、複雜壓縮格式
Apache Ant Zip (org.apache.tools.zip)輕量、ZipFile.setEncoding("GBK") 一行解決中文、API 近似 JDK僅需 ZIP、依賴極小
JDK 7+ NIO.2 FileSystemFileSystems.newFileSystem(zipPath, env),支援 zipfs-encoding 環境變數指定編碼純 JDK、現代化 Path API
zeroturnaround/zt-zipFluent API、ZipUtil.unpack(zip, dest, "GBK") 一行碼快速整合、Spring Boot 專案

Ant Zip 一行碼範例

import org.apache.tools.zip.ZipFile;
import java.io.File;

ZipFile zipFile = new ZipFile("中文.zip");
zipFile.setEncoding("GBK"); // 關鍵!
Enumeration<ZipEntry> entries = zipFile.getEntries();
while (entries.hasMoreElements()) {
    ZipEntry entry = entries.nextElement();
    // entry.getName() 已正確解碼為中文
    // zipFile.getInputStream(entry) 讀取內容...
}

總結

  • JDK 內建 ZIP 工具不支援 GBK 檔名,是長期已知限制(JDK 9+ 仍未修復)。
  • 「複製修改 JDK 內部類」屬教學/特殊環境(不可引入第三方 JAR)方案,維護成本極高。
  • 強烈建議:引入 Apache Commons Compress 或 Ant Zip,一行程式碼解決中文亂碼,並獲得豐富進階功能。
  • 若必須純 JDK,JDK 12+ 可嘗試 FileSystemProvider 設置 zipfs-encoding=GBK 環境變數(非標準 API,慎用)。
  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved