Java 递归解压.zip/.rar文件工具类

依赖

rar解压依赖

        <dependency>
            <groupId>net.sf.sevenzipjbinding</groupId>
            <artifactId>sevenzipjbinding</artifactId>
            <version>16.02-2.01</version>
        </dependency>
        <dependency>
            <groupId>net.sf.sevenzipjbinding</groupId>
            <artifactId>sevenzipjbinding-all-platforms</artifactId>
            <version>16.02-2.01</version>
        </dependency>

工具类代码

import net.sf.sevenzipjbinding.ExtractOperationResult;
import net.sf.sevenzipjbinding.IInArchive;
import net.sf.sevenzipjbinding.SevenZip;
import net.sf.sevenzipjbinding.simple.ISimpleInArchive;
import net.sf.sevenzipjbinding.simple.ISimpleInArchiveItem;
import net.sf.sevenzipjbinding.util.ByteArrayStream;

import java.io.*;
import java.net.HttpURLConnection;
import java.net.URL;
import java.net.UnknownHostException;
import java.nio.charset.Charset;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;

/**
 * @author: CC
 * @Desc:
 * @create: 2025-06-16 10:18
 **/
public class FileUtils {

    public enum FileType {
        ZIP, RAR
    }

    /**
     * 解压
     *
     * @param url 文件下载链接
     * @return 文件名到文件输入流的映射 eg:{/test1/小红花.sql=略, /test1/test1-1/小红花.txt=略}
     * @throws Exception
     */
    public static Map<String, InputStream> extractFile(String url) throws Exception {
        Objects.requireNonNull(url);

        InputStream inputStream = extractUrlToStream(url);

        Map<String, InputStream> fileContents = new HashMap<>();
        if (url.toUpperCase().endsWith("." + FileType.ZIP.name())) {
            extractZipFile(inputStream, fileContents, "");
        } else if (url.toUpperCase().endsWith("." + FileType.RAR.name())) {
            extractRarFile(inputStream, fileContents, "");
        } else {
            throw new RuntimeException("extractFile error, url type " + url + " is not supported");

        }

        return fileContents;
    }

    /**
     * 解压
     *
     * @param type 压缩文件类型
     * @param in   压缩文件file流
     * @return 文件名到文件输入流的映射 eg:{/test1/小红花.sql=略, /test1/test1-1/小红花.txt=略}
     * @throws Exception
     */
    public static Map<String, InputStream> extractFile(FileType type, InputStream in) throws Exception {
        Objects.requireNonNull(type);
        Objects.requireNonNull(in);

        Map<String, InputStream> fileContents = new HashMap<>();
        switch (type) {
            case ZIP:
                extractZipFile(in, fileContents, "");
                break;
            case RAR:
                extractRarFile(in, fileContents, "");
                break;
            default:
                throw new RuntimeException("extractFile error, Type " + type.name() + " is not supported");
        }

        return fileContents;
    }

    /**
     * 递归解压Zip文件方法
     * <p>
     * 可能存在问题:仅限windows压缩,第三方压缩(如:7-zip)存在解压 bug:java.lang.IllegalArgumentException: MALFORMED
     * 因为java.util.zip下的格式转换有问题 ,jdk中的zip存在字符编码的问题,windows下压缩的默认编码是GBK。
     * 参考 https://www.cnblogs.com/NickyYe/p/4450839.html
     *
     * @param in           压缩文件输入流
     * @param fileContents 解压结果映射 eg:{/test1/小红花.sql=略, /test1/test1-1/小红花.txt=略}
     * @param basePath     基础路径,用于处理嵌套压缩文件
     */
    public static void extractZipFile(InputStream in, Map<String, InputStream> fileContents, String basePath) throws Exception {
        Objects.requireNonNull(fileContents);
        if (!basePath.isEmpty() && !basePath.startsWith("/")) {
            basePath = "/" + basePath;
        }

        // 读入流:zip文件里含有中文名称的文件,windows 环境下,默认字符集为GBK,ZipFile 默认使用 UTF-8 字符集,当文件名存在中文时,处理时就会报错。创建 ZipFile 时,设置字符集为GBK(JDK 1.7以上)
        ZipInputStream zipInputStream = new ZipInputStream(in, Charset.forName("GBK"));

        // 遍历每一个文件
        ZipEntry zipEntry = zipInputStream.getNextEntry();
        while (zipEntry != null) {
            String entryPath = zipEntry.getName().replaceAll("\\\\", "/");
            String fullPath = basePath.isEmpty() ? "/" + entryPath : basePath + "/" + entryPath;

            if (!zipEntry.isDirectory()) { // 只处理文件,不处理目录
                System.out.println("extractZipFile ==> " + fullPath);

                if (entryPath.toUpperCase().endsWith("." + FileType.ZIP.name())) {
                    extractZipFile(zipInputStream, fileContents, fullPath);
                } else if (entryPath.toUpperCase().endsWith("." + FileType.RAR.name())) {
                    extractRarFile(zipInputStream, fileContents, fullPath);
                } else {
                    // 普通文件,直接解压到Map
                    ByteArrayOutputStream outputStream = extractEntryToStream(zipInputStream);
                    fileContents.put(fullPath, new ByteArrayInputStream(outputStream.toByteArray()));
                }
            }

            zipInputStream.closeEntry();
            zipEntry = zipInputStream.getNextEntry();
        }
    }

    /**
     * 递归解压Rar文件方法
     * <p>
     * 参考:https://blog.youkuaiyun.com/weixin_42477023/article/details/129619848
     *
     * @param rarInputStream 压缩文件输入流
     * @param fileContents   解压结果映射 eg:{/test1/小红花.sql=略, /test1/test1-1/小红花.txt=略}
     * @param basePath       基础路径,用于处理嵌套压缩文件
     */
    public static void extractRarFile(InputStream rarInputStream, Map<String, InputStream> fileContents, String basePath) throws Exception {
        Objects.requireNonNull(fileContents);
        if (!basePath.isEmpty() && !basePath.startsWith("/")) {
            basePath = "/" + basePath;
        }

        ByteArrayOutputStream baos = new ByteArrayOutputStream();

        // 将输入流复制到临时文件,因为 sevenzipjbinding 需要随机访问
        copyStream(rarInputStream, baos);
        byte[] rarBytes = baos.toByteArray();

        ByteArrayStream inStream = new ByteArrayStream(rarBytes, true, Math.max(1024, rarBytes.length));

        try (IInArchive inArchive = SevenZip.openInArchive(null, inStream)) {
            // 获取简单接口
            ISimpleInArchive simpleInArchive = inArchive.getSimpleInterface();

            // 遍历所有文件
            for (ISimpleInArchiveItem item : simpleInArchive.getArchiveItems()) {
                // 忽略目录
                if (!item.isFolder()) {
                    // 获取文件路径
                    String entryPath = item.getPath().replaceAll("\\\\", "/");
                    String fullPath = basePath.isEmpty() ? "/" + entryPath : basePath + "/" + entryPath;

                    System.out.println("extractRarFile ==> " + fullPath);

                    InputStream subInputStream = extractISimpleInArchiveItemToStream(fullPath, item);

                    if (entryPath.toUpperCase().endsWith("." + FileType.RAR.name())) {
                        extractRarFile(subInputStream, fileContents, fullPath);
                    } else if (entryPath.toUpperCase().endsWith("." + FileType.ZIP.name())) {
                        extractZipFile(subInputStream, fileContents, fullPath.replace(".zip", ""));
                    } else {
                        fileContents.put(fullPath, subInputStream);
                    }
                }
            }
        }
    }

    /**
     * 将当前URL转为输出流
     *
     * @param theUrl URL
     * @return InputStream
     * @throws IOException
     */
    public static InputStream extractUrlToStream(String theUrl) throws IOException {
        int retryCount = 0;
        long backoffMillis = 100; // 初始退避时间,单位毫秒

        while (true) {
            try {
                URL url = new URL(theUrl);
                HttpURLConnection urlConn = (HttpURLConnection) url.openConnection();

                // 设置请求方法和超时时间(可选)
                urlConn.setRequestMethod("GET");
                urlConn.setConnectTimeout(5000); // 连接超时时间,单位毫秒
                urlConn.setReadTimeout(5000); // 读取超时时间,单位毫秒

                // 尝试获取输入流
                return urlConn.getInputStream(); // 成功获取输入流,返回
//                urlConn.disconnect(); // 关闭连接(可选,但推荐在不再需要连接时关闭)

            } catch (IOException e) {
                if (retryCount >= 3 || e instanceof UnknownHostException) {
                    // 如果是未知主机异常或已达到最大重试次数,则不再重试,直接抛出异常
                    throw e;
                }

                // 等待一段时间后重试
                try {
                    System.err.println("extractUrlToStream Operation failed, retrying... (" + e.getMessage() + ")");
                    Thread.sleep(backoffMillis);
                    backoffMillis *= 2; // 应用指数退避策略
                    retryCount++;
                } catch (InterruptedException ie) {
                    Thread.currentThread().interrupt(); // 保持中断状态
                    throw new RuntimeException("extractUrlToStream Sleep interrupted: " + ie.getMessage());
                }
            }
        }
    }

    /**
     * 将当前ZipEntry解压为输出流
     */
    private static ByteArrayOutputStream extractEntryToStream(ZipInputStream zipInputStream) throws IOException {
        // 使用ByteArrayOutputStream保存解压后的数据
        ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
        byte[] bytes = new byte[1024];
        int readLen;
        while ((readLen = zipInputStream.read(bytes)) != -1) {
            outputStream.write(bytes, 0, readLen);
        }
        return outputStream;
    }

    /**
     * 将当前ISimpleInArchiveItem转为输出流
     */
    private static InputStream extractISimpleInArchiveItemToStream(String path, ISimpleInArchiveItem item) throws IOException {
        // 提取文件内容
        ByteArrayOutputStream fileBaos = new ByteArrayOutputStream();
        // 解压文件
        ExtractOperationResult result = item.extractSlow(data -> {
            try {
                fileBaos.write(data);
            } catch (IOException e) {
                return 0; // 出错时返回 0 表示中断
            }
            return data.length; // 返回处理的数据长度
        });

        if (result != ExtractOperationResult.OK) {
            throw new IOException("提取文件失败: " + path);
        }
        return new ByteArrayInputStream(fileBaos.toByteArray());
    }

    /**
     * 辅助方法:将输入流复制到输出流
     */
    private static void copyStream(InputStream input, OutputStream output) throws IOException {
        byte[] buffer = new byte[8192];
        int bytesRead;
        while ((bytesRead = input.read(buffer)) != -1) {
            output.write(buffer, 0, bytesRead);
        }
    }


}

测试方法

import java.io.*;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.Map;

/**
 * @author: CC
 * @Desc:
 * @create: 2025-06-13 16:39
 **/
public class Test {

    /**
     * 将输入流保存到指定文件
     */
    private static void saveStreamToFile(InputStream in, String filePath) throws IOException {
        File file = new File(filePath);
        mkdir(file.getParentFile());

        try (BufferedOutputStream out = new BufferedOutputStream(Files.newOutputStream(Paths.get(filePath)))) {
            byte[] buffer = new byte[1024];
            int length;
            while ((length = in.read(buffer)) > 0) {
                out.write(buffer, 0, length);
            }
        }
    }

    /**
     * 创建目录
     */
    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:/temp/ziptest/zip/test1.rar";
        String desDirectory = "D:/temp/ziptest/unzip/test6rar";

        Map<String, InputStream> unzip = FileUtils.extractFile(FileUtils.FileType.RAR, Files.newInputStream(Paths.get(zipFilePath)));
        for (String filePath : unzip.keySet()) {
            saveStreamToFile(unzip.get(filePath), desDirectory + filePath);
        }
    }

}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值