
那些年,被“大文件”支配的恐惧
在日常开发和运维中,你是否遇到过以下场景?
- 邮件附件限制:公司邮件系统限制附件最大 50MB,但你的日志包有 2GB。
- 微信/钉钉传输壁垒:想给客户发一个安装包,结果卡在“文件过大无法发送”。
- 弱网环境下上传:好不容易上传了 90% 的云盘,断网了,只能从头再来。
- U盘格式限制:老旧的 FAT32 格式 U盘不支持超过 4GB 的单文件。
痛点: 传统的 Linux split 命令拆出的 xaa, xab 碎片,发给 Windows 用户时,对方往往一脸茫然,不知道怎么合并。
目标: 我们需要一种方案,在 Linux/Java 后端生成,在 Windows 上让对方双击就能解压。
实现方案:标准的“分卷压缩”逻辑
关键点在于使用 Zip Spanning (分卷标准)。它会生成 .z01, .z02 ... 以及一个引导文件 .zip。
方案 A:Shell 脚本工具(运维利器)
我们利用 Linux 原生的 zip 工具。
- 拆分原理:
zip -s [大小] -r [目标名] [源] - Windows 兼容性:100%。Windows 用户只需右键点击
.zip文件,使用 7-Zip 或 WinRAR 即可自动识别全部分卷。
Shell 脚本:ez-split.sh
你可以将以下代码保存为 ez-split.sh,并赋予执行权限 chmod +x ez-split.sh。
#!/bin/bash
# 显示用法
usage() {
echo "用法:"
echo " 拆分: $0 split [文件或文件夹] [每个分卷大小(如 100M, 1G)]"
echo " 恢复: $0 merge [主zip文件(.zip)]"
echo ""
echo "示例:"
echo " $0 split my_data/ 500M -> 将生成 my_data.zip, my_data.z01, ..."
echo " $0 merge my_data.zip -> 将合并并解压到当前目录"
exit 1
}
# 检查是否安装了 zip 和 unzip
check_tools() {
if ! command -v zip &> /dev/null || ! command -v unzip &> /dev/null; then
echo "错误: 未找到 zip 或 unzip 工具。请先安装: sudo apt install zip unzip"
exit 1
fi
}
# 拆分压缩逻辑
split_zip() {
local TARGET=$1
local SIZE=$2
local OUTPUT="${TARGET%/}.zip"
echo "正在压缩并拆分 [$TARGET] ..."
# -r: 递归压缩 (针对文件夹)
# -s: 设置分卷大小
# --out: 指定输出主文件名
zip -r -s "$SIZE" "$OUTPUT" "$TARGET"
if [ $? -eq 0 ]; then
echo "------------------------------------------------"
echo "成功!已生成分卷文件。"
echo "Windows 用户说明: 请确保所有 .z01, .z02... 和 .zip 文件在同一目录下,"
echo "右键点击 [.zip] 后缀的文件,使用 7-Zip 或 WinRAR 选择 '提取到当前文件夹' 即可。"
echo "------------------------------------------------"
else
echo "拆分过程中出现错误。"
fi
}
# 恢复逻辑 (修正了 unzip 不支持分卷的问题)
merge_zip() {
local MAIN_ZIP=$1
local TEMP_SINGLE="temp_merged_archive.zip"
if [[ ! "$MAIN_ZIP" == *.zip ]]; then
echo "错误: 请选择以 .zip 结尾的主文件。"
exit 1
fi
# 优先检查是否安装了 7z (7-zip 在 Linux 上对分卷支持最好)
if command -v 7z &> /dev/null; then
echo "检测到 7z,正在使用 7z 直接解压..."
7z x "$MAIN_ZIP"
return
fi
# 如果没有 7z,使用 zip 自身的合并功能
echo "正在将分卷合并为单文件 [$TEMP_SINGLE] ..."
# zip -s 0 是合并分卷的核心命令
zip -s 0 "$MAIN_ZIP" --out "$TEMP_SINGLE"
if [ $? -eq 0 ]; then
echo "合并成功,正在解压..."
unzip "$TEMP_SINGLE"
rm "$TEMP_SINGLE" # 解压后删除临时单文件
echo "解压完成!"
else
echo "合并失败。请确保 .z01, .z02... 等分卷文件在当前目录。"
[ -f "$TEMP_SINGLE" ] && rm "$TEMP_SINGLE"
fi
}
# 脚本逻辑入口
check_tools
case "$1" in
split)
if [ -z "$2" ] || [ -z "$3" ]; then usage; fi
split_zip "$2" "$3"
;;
merge)
if [ -z "$2" ]; then usage; fi
merge_zip "$2"
;;
*)
usage
;;
esac
执行示例:
# 拆包
./ez.split.sh split utools_5.1.1_amd64.deb 20M
# 合并压缩包
./ez.split.sh merge utools_5.1.1_amd64.deb.zip


方案 B:Java 通用方法(后端集成)
在企业级应用(如自动备份系统)中,我们需要用代码实现。推荐使用 Zip4j 库,因为它完美支持了 PKWare 的分卷协议。
在 Java 生态中,处理分卷压缩(Split/Spanned Zip)最成熟、最简单的库是 Zip4j。原生的 java.util.zip 不支持分卷,手动写流处理逻辑非常复杂且容易出错(涉及到 Zip64 头的处理)。
下面使用 Zip4j 编写一个通用的工具类,能够实现:
- 拆分压缩:将文件/文件夹压缩并拆分为 .z01, .z02, ... .zip。
- 合并解压:识别主 .zip 文件并自动关联分卷进行解压。
1. 添加依赖 (Maven)
如果你使用 Maven,请在 pom.xml 中添加:
<dependency>
<groupId>net.lingala.zip4j</groupId>
<artifactId>zip4j</artifactId>
<version>2.11.5</version>
</dependency>
2. Java 工具类实现
import net.lingala.zip4j.ZipFile;
import net.lingala.zip4j.model.ZipParameters;
import net.lingala.zip4j.model.enums.CompressionLevel;
import net.lingala.zip4j.model.enums.CompressionMethod;
import java.io.File;
import java.io.IOException;
public class ZipSplitUtil {
/**
* 拆分压缩方法
* @param sourcePath 要压缩的文件或文件夹路径
* @param destinationZipPath 输出的主 zip 文件路径 (例如: D:/test/myfiles.zip)
* @param splitSize 分卷大小(单位:字节),例如 10 * 1024 * 1024 (10MB)
*/
public static void splitZip(String sourcePath, String destinationZipPath, long splitSize) throws IOException {
File sourceFile = new File(sourcePath);
ZipFile zipFile = new ZipFile(destinationZipPath);
ZipParameters parameters = new ZipParameters();
parameters.setCompressionMethod(CompressionMethod.DEFLATE);
parameters.setCompressionLevel(CompressionLevel.NORMAL);
System.out.println("开始分卷压缩...");
// 如果是文件夹
if (sourceFile.isDirectory()) {
zipFile.createSplitZipFileFromFolder(sourceFile, parameters, true, splitSize);
} else {
// 如果是单文件
zipFile.createSplitZipFile(java.util.Collections.singletonList(sourceFile), parameters, true, splitSize);
}
System.out.println("压缩完成!生成文件: " + destinationZipPath + " 等");
}
/**
* 合并并解压方法
* @param mainZipPath 分卷压缩包的主 zip 文件路径 (必须是 .zip 后缀那个)
* @param extractPath 解压到的目标目录
*/
public static void mergeAndExtract(String mainZipPath, String extractPath) throws IOException {
ZipFile zipFile = new ZipFile(mainZipPath);
// 检查是否是分卷压缩包
if (zipFile.isSplitArchive()) {
System.out.println("检测到分卷压缩包,正在合并解压...");
} else {
System.out.println("检测到普通压缩包,正在解压...");
}
// 自动识别同目录下的 .z01, .z02...
zipFile.extractAll(extractPath);
System.out.println("解压完成!目标路径: " + extractPath);
}
public static void main(String[] args) {
try {
// 示例 1: 拆分压缩一个文件夹,每个包 5MB
String src = "C:/Temp/BigData";
String dest = "C:/Temp/Output/Archive.zip";
long size = 5 * 1024 * 1024; // 5MB
splitZip(src, dest, size);
// 示例 2: 解压分卷包
// mergeAndExtract("C:/Temp/Output/Archive.zip", "C:/Temp/Restored");
} catch (IOException e) {
e.printStackTrace();
}
}
}
关键点详解
- Windows 兼容性:
Zip4j 生成的格式是标准的 PKWare 分卷格式。它会生成 Archive.z01, Archive.z02 ... Archive.zip。这种格式与 WinRAR 和 7-Zip 完全兼容。用户在 Windows 上只需右键点击 .zip 文件即可解压。 - createSplitZipFile 逻辑:
- 第一个参数是文件列表。
- 第二个参数是压缩配置。
- 第三个参数 true 表示是否在完成后生成单体文件的逻辑(通常保持为 true)。
- 第四个参数是分卷大小(Byte)。注意:ZIP 标准规定分卷大小最小不能低于 65536 字节 (64KB)。
- 自动识别分卷:
在解压时,你只需要指定 .zip 文件。ZipFile 对象会自动在同一目录下寻找同名的 .z01, .z02 等文件。如果缺少任何一个分卷,它会抛出异常。 - 不需要手动合并:
有些做法是先用 Java 的 FileInputStream 把所有 .z 文件拼成一个大文件再解压,那种做法非常低效且容易导致内存溢出。Zip4j 采用流式读取,直接从各个分卷中跳转读取数据,性能最优。
注意事项
- 文件名一致性:确保分卷文件(.z01, .z02)与主文件(.zip)的前缀完全一致且在同一文件夹下。
- 权限:确保 Java 进程对目标文件夹有写权限。
- Zip64:如果单个文件超过 4GB,Zip4j 会自动启用 Zip64 扩展,这在现代解压软件中都是支持的。
进阶:如何优雅地通过邮件发送这些拆分包?
当你拆分了 10 个 20MB 的包后,手动发 10 封邮件会让人崩溃。我们需要一个自动化的“邮件拆包发送”最佳实践:
- 主题命名规范:必须包含
(Part 1/5)标识。 - 间隔发送策略:防止触发反垃圾邮件系统。
- MD5 校验:随邮件发送正文,告知每个分卷的校验码。
邮件发送分卷文件的最佳实践代码 (Java)
这里使用 Jakarta Mail (原 JavaMail) 结合前面的拆分逻辑。
import jakarta.mail.*;
import jakarta.mail.internet.*;
import java.util.Properties;
import java.io.File;
public class SplitFileEmailSender {
public static void sendSplitFiles(String recipient, String folderPath, String zipName) throws Exception {
// 1. 配置邮件服务器
Properties prop = new Properties();
prop.put("mail.smtp.host", "smtp.example.com");
prop.put("mail.smtp.port", "587");
prop.put("mail.smtp.auth", "true");
prop.put("mail.smtp.starttls.enable", "true");
Session session = Session.getInstance(prop, new Authenticator() {
protected PasswordAuthentication getPasswordAuthentication() {
return new PasswordAuthentication("your-email@example.com", "password");
}
});
// 2. 获取目录下所有的分卷文件 (.z01, .z02, ..., .zip)
File dir = new File(folderPath);
File[] files = dir.listFiles((d, name) -> name.startsWith(zipName));
if (files == null) return;
int totalParts = files.length;
System.out.println("准备发送 " + totalParts + " 个分卷文件...");
// 3. 循环发送每一部分
for (int i = 0; i < files.length; i++) {
File currentFile = files[i];
Message message = new MimeMessage(session);
message.setFrom(new InternetAddress("sender@example.com"));
message.setRecipients(Message.RecipientType.TO, InternetAddress.parse(recipient));
// 规范的主题命名
message.setSubject(String.format("大文件传输: %s (第 %d 部分, 共 %d 部分)",
zipName, (i + 1), totalParts));
// 邮件正文
MimeBodyPart textPart = new MimeBodyPart();
textPart.setText("这是分卷压缩包的一部分。请下载所有附件到同一文件夹后,解压后缀为 .zip 的文件。");
// 附件部分
MimeBodyPart attachmentPart = new MimeBodyPart();
attachmentPart.attachFile(currentFile);
Multipart multipart = new MimeMultipart();
multipart.addBodyPart(textPart);
multipart.addBodyPart(attachmentPart);
message.setContent(multipart);
System.out.println("正在发送: " + currentFile.getName());
Transport.send(message);
// 频率限制:每发一封邮件休息 2 秒,防止被封
Thread.sleep(2000);
}
System.out.println("全部邮件已安全发出。");
}
}
Q.E.D.


