Java實現解壓縮檔案和資料夾

Mr_ηobody發表於2021-03-10

一 前言

專案開發中,總會遇到解壓縮檔案的時候。比如,使用者下載多個檔案時,服務端可以將多個檔案壓縮成一個檔案(例如xx.zip或xx.rar)。使用者上傳資料時,允許上傳壓縮檔案,服務端進行解壓讀取每一個檔案。

基於通用性,以下介紹幾種解壓縮檔案的方式,包裝成工具類,供平時開發使用。

二 壓縮檔案

壓縮檔案,顧名思義,即把一個或多個檔案壓縮成一個檔案。壓縮也有2種形式,一種是將所有檔案壓縮到同一目錄下,此種方式要注意檔案重名覆蓋的問題。另一種是按原有檔案樹結構進行壓縮,即壓縮後的檔案樹結構保持不變。

壓縮檔案操作,會使用到一個類,即ZipOutputStream。

2.1 壓縮多個檔案

此方法將所有檔案壓縮到同一個目錄下。方法傳入多個檔案列表,和一個最終壓縮到的檔案路徑名。

    /**
     * 壓縮多個檔案,壓縮後的所有檔案在同一目錄下
     * 
     * @param zipFileName 壓縮後的檔名
     * @param files 需要壓縮的檔案列表
     * @throws IOException IO異常
     */
    public static void zipMultipleFiles(String zipFileName, File... files) throws IOException {
        ZipOutputStream zipOutputStream = null;
        try {
            // 輸出流
            zipOutputStream = new ZipOutputStream(new FileOutputStream(zipFileName));
            // 遍歷每一個檔案,進行輸出
            for (File file : files) {
                zipOutputStream.putNextEntry(new ZipEntry(file.getName()));
                FileInputStream fileInputStream = new FileInputStream(file);
                int readLen;
                byte[] buffer = new byte[1024];
                while ((readLen = fileInputStream.read(buffer)) != -1) {
                    zipOutputStream.write(buffer, 0, readLen);
                }
                // 關閉流
                fileInputStream.close();
                zipOutputStream.closeEntry();
            }
        } finally {
            if (null != zipOutputStream) {
                try {
                    zipOutputStream.close();
                } catch (IOException ex) {
                    ex.printStackTrace();
                }
            }
        }
    }

測試,將D盤下的infp.txt和infp1.txt檔案壓縮到D盤下,壓縮檔名為my.zip。

    public static void main(String[] args) throws Exception {
        zipMultipleFiles("D:/my.zip", new File("D:/infp.txt"), new File("D:/infp1.txt"));
    }

2.2 壓縮檔案或檔案樹

此方法將資料夾下的所有檔案按原有的樹形結構壓縮到檔案中,也支援壓縮單個檔案。原理也簡單,無非就是遞迴遍歷檔案樹中的每一個檔案,進行壓縮。有個注意的點每一個檔案的寫入路徑是基於壓縮檔案位置的相對路徑。

package com.nobody.zip;

import java.io.*;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;
import java.util.zip.ZipOutputStream;

public class ZipUtils {

    /**
     * 壓縮檔案或資料夾(包括所有子目錄檔案)
     *
     * @param sourceFile 原始檔
     * @param format 格式(zip或rar)
     * @throws IOException 異常資訊
     */
    public static void zipFileTree(File sourceFile, String format) throws IOException {
        ZipOutputStream zipOutputStream = null;
        try {
            String zipFileName;
            if (sourceFile.isDirectory()) { // 目錄
                zipFileName = sourceFile.getParent() + File.separator + sourceFile.getName() + "."
                        + format;
            } else { // 單個檔案
                zipFileName = sourceFile.getParent()
                        + sourceFile.getName().substring(0, sourceFile.getName().lastIndexOf("."))
                        + "." + format;
            }
            // 壓縮輸出流
            zipOutputStream = new ZipOutputStream(new FileOutputStream(zipFileName));
            zip(sourceFile, zipOutputStream, "");
        } finally {
            if (null != zipOutputStream) {
                // 關閉流
                try {
                    zipOutputStream.close();
                } catch (IOException ex) {
                    ex.printStackTrace();
                }
            }
        }
    }

    /**
     * 遞迴壓縮檔案
     * 
     * @param file 當前檔案
     * @param zipOutputStream 壓縮輸出流
     * @param relativePath 相對路徑
     * @throws IOException IO異常
     */
    private static void zip(File file, ZipOutputStream zipOutputStream, String relativePath)
            throws IOException {

        FileInputStream fileInputStream = null;
        try {
            if (file.isDirectory()) { // 當前為資料夾
                // 當前資料夾下的所有檔案
                File[] list = file.listFiles();
                if (null != list) {
                    // 計算當前的相對路徑
                    relativePath += (relativePath.length() == 0 ? "" : "/") + file.getName();
                    // 遞迴壓縮每個檔案
                    for (File f : list) {
                        zip(f, zipOutputStream, relativePath);
                    }
                }
            } else { // 壓縮檔案
                // 計算檔案的相對路徑
                relativePath += (relativePath.length() == 0 ? "" : "/") + file.getName();
                // 寫入單個檔案
                zipOutputStream.putNextEntry(new ZipEntry(relativePath));
                fileInputStream = new FileInputStream(file);
                int readLen;
                byte[] buffer = new byte[1024];
                while ((readLen = fileInputStream.read(buffer)) != -1) {
                    zipOutputStream.write(buffer, 0, readLen);
                }
                zipOutputStream.closeEntry();
            }
        } finally {
            // 關閉流
            if (fileInputStream != null) {
                try {
                    fileInputStream.close();
                } catch (IOException ex) {
                    ex.printStackTrace();
                }
            }
        }
    }

    public static void main(String[] args) throws Exception {
        String path = "D:/test";
        String format = "zip";
        zipFileTree(new File(path), format);
    }
}

上例將test目錄下的所有檔案壓縮到同一目錄下的test.zip檔案中。

2.3 藉助檔案訪問器壓縮

還有一種更簡單的方式,我們不自己寫遞迴遍歷。藉助Java原生類,SimpleFileVisitor,它提供了幾個訪問檔案的方法,其中有個方法visitFile,對於檔案樹中的每一個檔案(資料夾除外),都會呼叫這個方法。我們只要寫一個類繼承SimpleFileVisitor,然後重寫visitFile方法,實現將每一個檔案寫入到壓縮檔案中即可。

當然,除了visitFile方法,它裡面還有preVisitDirectory,postVisitDirectory,visitFileFailed等方法,通過方法名大家也猜出什麼意思了。

package com.nobody.zip;

import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.file.*;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;

/**
 * @Description
 * @Author Mr.nobody
 * @Date 2021/3/8
 * @Version 1.0.0
 */
public class ZipFileTree extends SimpleFileVisitor<Path> {

    // zip輸出流
    private ZipOutputStream zipOutputStream;
    // 源目錄
    private Path sourcePath;

    public ZipFileTree() {}

    /**
     * 壓縮目錄以及所有子目錄檔案
     *
     * @param sourceDir 源目錄
     */
    public void zipFile(String sourceDir) throws IOException {
        try {
            // 壓縮後的檔案和源目錄在同一目錄下
            String zipFileName = sourceDir + ".zip";
            this.zipOutputStream = new ZipOutputStream(new FileOutputStream(zipFileName));
            this.sourcePath = Paths.get(sourceDir);

            // 開始遍歷檔案樹
            Files.walkFileTree(sourcePath, this);
        } finally {
            // 關閉流
            if (null != zipOutputStream) {
                zipOutputStream.close();
            }
        }
    }

    // 遍歷到的每一個檔案都會執行此方法
    @Override
    public FileVisitResult visitFile(Path file, BasicFileAttributes attributes) throws IOException {
        // 取相對路徑
        Path targetFile = sourcePath.relativize(file);
        // 寫入單個檔案
        zipOutputStream.putNextEntry(new ZipEntry(targetFile.toString()));
        byte[] bytes = Files.readAllBytes(file);
        zipOutputStream.write(bytes, 0, bytes.length);
        zipOutputStream.closeEntry();
        // 繼續遍歷
        return FileVisitResult.CONTINUE;
    }

    // 遍歷每一個目錄時都會呼叫的方法
    @Override
    public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs)
            throws IOException {
        return super.preVisitDirectory(dir, attrs);
    }

    // 遍歷完一個目錄下的所有檔案後,再呼叫這個目錄的方法
    @Override
    public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException {
        return super.postVisitDirectory(dir, exc);
    }

    // 遍歷檔案失敗後呼叫的方法
    @Override
    public FileVisitResult visitFileFailed(Path file, IOException exc) throws IOException {
        return super.visitFileFailed(file, exc);
    }

    public static void main(String[] args) throws IOException {
        // 需要壓縮源目錄
        String sourceDir = "D:/test";
        // 壓縮
        new ZipFileTree().zipFile(sourceDir);
    }
}

三 解壓檔案

解壓壓縮包,藉助ZipInputStream類,可以讀取到壓縮包中的每一個檔案,然後根據讀取到的檔案屬性,寫入到相應路徑下即可。對於解壓壓縮包中是檔案樹的結構,每讀取到一個檔案後,如果是多層路徑下的檔案,需要先建立父目錄,再寫入檔案流。

package com.nobody.zip;

import java.io.*;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;
import java.util.zip.ZipOutputStream;

/**
 * @Description 解壓縮檔案工具類
 * @Author Mr.nobody
 * @Date 2021/3/8
 * @Version 1.0.0
 */
public class ZipUtils {

    /**
     * 解壓
     * 
     * @param zipFilePath 帶解壓檔案
     * @param desDirectory 解壓到的目錄
     * @throws Exception
     */
    public static void unzip(String zipFilePath, String desDirectory) throws Exception {

        File desDir = new File(desDirectory);
        if (!desDir.exists()) {
            boolean mkdirSuccess = desDir.mkdir();
            if (!mkdirSuccess) {
                throw new Exception("建立解壓目標資料夾失敗");
            }
        }
        // 讀入流
        ZipInputStream zipInputStream = new ZipInputStream(new FileInputStream(zipFilePath));
        // 遍歷每一個檔案
        ZipEntry zipEntry = zipInputStream.getNextEntry();
        while (zipEntry != null) {
            if (zipEntry.isDirectory()) { // 資料夾
                String unzipFilePath = desDirectory + File.separator + zipEntry.getName();
                // 直接建立
                mkdir(new File(unzipFilePath));
            } else { // 檔案
                String unzipFilePath = desDirectory + File.separator + zipEntry.getName();
                File file = new File(unzipFilePath);
                // 建立父目錄
                mkdir(file.getParentFile());
                // 寫出檔案流
                BufferedOutputStream bufferedOutputStream =
                        new BufferedOutputStream(new FileOutputStream(unzipFilePath));
                byte[] bytes = new byte[1024];
                int readLen;
                while ((readLen = zipInputStream.read(bytes)) != -1) {
                    bufferedOutputStream.write(bytes, 0, readLen);
                }
                bufferedOutputStream.close();
            }
            zipInputStream.closeEntry();
            zipEntry = zipInputStream.getNextEntry();
        }
        zipInputStream.close();
    }

    // 如果父目錄不存在則建立
    private static void mkdir(File file) {
        if (null == file || file.exists()) {
            return;
        }
        mkdir(file.getParentFile());
        file.mkdir();
    }

    public static void main(String[] args) throws Exception {
        String zipFilePath = "D:/test.zip";
        String desDirectory = "D:/a";
        unzip(zipFilePath, desDirectory);
    }
}

四 總結

  • 在解壓縮檔案過程中,主要是對流的讀取操作,注意進行異常處理,以及關閉流。
  • web應用中,通過介面可以實現檔案上傳下載,對應的我們只要把壓縮後的檔案,寫入到response.getOutputStream()輸出流即可。
  • 解壓縮檔案時,注意空資料夾的處理。

此演示專案已上傳到Github,如有需要可自行下載,歡迎 Star 。 https://github.com/LucioChn/common-utils

相關文章