平时做技术实践时,很多问题不是概念不会,而是细节没串起来。拿“基于C#实现方法的文件分割与合并工具”来说,它看着像小点,放到项目里常会牵出环境、配置、兼容性和维护成本。下面按实际采用顺序,把思路、关键写法和容易踩坑的地方讲清楚,便于大家直接对照操作。
文件分割与合同时是数据管理中的常用操作,广泛应用来以下场景:
落到代码里,本工具基于 C# 语言开发,采用 .NET 6.0 运行时,借助 System.IO 命名空间实现二进制文件流操作。核心特性包括:
chunkSize = 1024 * 1024字节,即 1MB)的子文件,最后一个子文件可能小于指定大小。原文件名.partN(N 为序号,从 1 开始),如 largefile.zip.part1、largefile.zip.part2。原文件名.info文件,记录源文件大小、分割数量、校验和(如 MD5),用来合同时时校验完整性。SourceFile: archive.zip
FileSize: 104857600
PartCount: 10
ChunkSize: 1048576
MD5: a1b2c3d4e5f6...
原文件名.partN子文件,按序号从小到大拼接为完整文件。System.IO(文件流)、System.Security.Cryptography(MD5 校验)using System;
using System.IO;
using System.Security.Cryptography;
using System.Text;
public class FileSplitter
{
/// <summary>
/// 分割文件为多个子文件
/// </summary>
/// <param name="sourcePath">源文件路径</param>
/// <param name="outputDir">输出目录(默认源文件所在目录)</param>
/// <param name="chunkSizeBytes">子文件大小(字节,默认 1MB)</param>
/// <param name="generateInfoFile">是否生成 .info 元数据文件</param>
/// <returns>分割后的子文件数量</returns>
public static int SplitFile(string sourcePath, string outputDir = null,
long chunkSizeBytes = 1024 * 1024, bool generateInfoFile = true)
{
// 参数校验
if (!File.Exists(sourcePath))
throw new FileNotFoundException($"源文件不存在: {sourcePath}");
if (chunkSizeBytes <= 0)
throw new ArgumentException("分块大小必须大于 0", nameof(chunkSizeBytes));
// 确定输出目录
outputDir ??= Path.GetDirectoryName(sourcePath);
Directory.CreateDirectory(outputDir);
string fileName = Path.GetFileName(sourcePath);
string baseName = Path.GetFileNameWithoutExtension(sourcePath);
string extension = Path.GetExtension(sourcePath);
string fullBaseName = baseName + extension;
// 计算 MD5(需要在读取流之前计算,或重新打开流)
string md5Hash = string.Empty;
if (generateInfoFile)
md5Hash = CalculateFileMD5(sourcePath);
using (FileStream sourceStream = new FileStream(sourcePath, FileMode.Open, FileAccess.Read))
{
long fileSize = sourceStream.Length;
int partCount = (int)Math.Ceiling((double)fileSize / chunkSizeBytes);
byte[] buffer = new byte[chunkSizeBytes];
// 生成元数据文件
if (generateInfoFile)
{
string infoPath = Path.Combine(outputDir, $"{fullBaseName}.info");
using (StreamWriter infoWriter = new StreamWriter(infoPath, false, Encoding.UTF8))
{
infoWriter.WriteLine($"SourceFile: {fileName}");
infoWriter.WriteLine($"FileSize: {fileSize}");
infoWriter.WriteLine($"PartCount: {partCount}");
infoWriter.WriteLine($"ChunkSize: {chunkSizeBytes}");
infoWriter.WriteLine($"MD5: {md5Hash}");
infoWriter.WriteLine($"CreatedAt: {DateTime.Now:yyyy-MM-dd HH:mm:ss}");
}
}
// 执行分割
for (int i = 0; i < partCount; i++)
{
long position = i * chunkSizeBytes;
int bytesToRead = (int)Math.Min(chunkSizeBytes, fileSize - position);
sourceStream.Seek(position, SeekOrigin.Begin);
int bytesRead = sourceStream.Read(buffer, 0, bytesToRead);
if (bytesRead != bytesToRead)
Console.WriteLine($"警告: 第 {i + 1} 个分块实际读取 {bytesRead} 字节,预期 {bytesToRead} 字节");
string partPath = Path.Combine(outputDir, $"{fullBaseName}.part{i + 1}");
using (FileStream partStream = new FileStream(partPath, FileMode.Create, FileAccess.Write))
{
partStream.Write(buffer, 0, bytesRead);
}
Console.WriteLine($"[分割] 已创建: {Path.GetFileName(partPath)} ({bytesRead} 字节)");
}
Console.WriteLine($"分割完成: {partCount} 个分块,输出目录: {outputDir}");
return partCount;
}
}
/// <summary>
/// 计算文件的 MD5 校验值
/// </summary>
private static string CalculateFileMD5(string filePath)
{
using (var md5 = MD5.Create())
using (var stream = File.OpenRead(filePath))
{
byte[] hash = md5.ComputeHash(stream);
return BitConverter.ToString(hash).Replace("-", "").ToLowerInvariant();
}
}
}
public class FileMerger
{
/// <summary>
/// 合并子文件为完整文件
/// </summary>
/// <param name="partDirectory">子文件所在目录</param>
/// <param name="originalFileName">原文件名(如 "archive.zip")</param>
/// <param name="outputPath">合并后的输出路径</param>
/// <param name="verifyIntegrity">是否校验 MD5(需要 .info 文件)</param>
/// <returns>合并后的文件大小(字节)</returns>
public static long MergeFiles(string partDirectory, string originalFileName,
string outputPath, bool verifyIntegrity = true)
{
if (!Directory.Exists(partDirectory))
throw new DirectoryNotFoundException($"目录不存在: {partDirectory}");
string extension = Path.GetExtension(originalFileName);
string baseName = Path.GetFileNameWithoutExtension(originalFileName);
string fullBaseName = baseName + extension;
string pattern = $"{fullBaseName}.part*";
// 获取并排序分块文件
string[] partFiles = Directory.GetFiles(partDirectory, pattern, SearchOption.TopDirectoryOnly);
if (partFiles.Length == 0)
throw new FileNotFoundException($"未找到匹配的分块文件: {pattern}");
// 按序号排序(解析 partN 中的 N)
Array.Sort(partFiles, (a, b) =>
{
int indexA = ExtractPartNumber(a, fullBaseName);
int indexB = ExtractPartNumber(b, fullBaseName);
return indexA.CompareTo(indexB);
});
// 验证序号连续性(可选)
for (int i = 0; i < partFiles.Length; i++)
{
int expected = i + 1;
int actual = ExtractPartNumber(partFiles[i], fullBaseName);
if (actual != expected)
throw new InvalidOperationException($"分块序号不连续: 期望 {expected},实际 {actual}");
}
// 合并文件
using (FileStream outputStream = new FileStream(outputPath, FileMode.Create, FileAccess.Write))
{
foreach (string partPath in partFiles)
{
using (FileStream partStream = new FileStream(partPath, FileMode.Open, FileAccess.Read))
{
partStream.CopyTo(outputStream);
}
Console.WriteLine($"[合并] 已处理: {Path.GetFileName(partPath)}");
}
}
Console.WriteLine($"合并完成: {outputPath}");
long mergedSize = new FileInfo(outputPath).Length;
// 完整性校验
if (verifyIntegrity)
{
string infoPath = Path.Combine(partDirectory, $"{fullBaseName}.info");
if (File.Exists(infoPath))
{
var metadata = ParseInfoFile(infoPath);
string expectedMd5 = metadata["MD5"];
string actualMd5 = CalculateFileMD5(outputPath);
if (string.Equals(expectedMd5, actualMd5, StringComparison.OrdinalIgnoreCase))
{
Console.WriteLine($"[校验] MD5 一致: {actualMd5}");
}
else
{
Console.WriteLine($"[警告] MD5 不一致! 期望: {expectedMd5}, 实际: {actualMd5}");
}
}
else
{
Console.WriteLine("[校验] 未找到 .info 文件,跳过 MD5 校验");
}
}
return mergedSize;
}
/// <summary>
/// 从文件名中提取分块序号(如 file.zip.part3 → 3)
/// </summary>
private static int ExtractPartNumber(string filePath, string fullBaseName)
{
string fileName = Path.GetFileNameWithoutExtension(filePath);
// 文件名格式: fullBaseName.partN
string suffix = fileName.Substring(fullBaseName.Length + 1); // ".partN" → "partN"
if (suffix.StartsWith("part", StringComparison.OrdinalIgnoreCase))
{
string numberPart = suffix.Substring(4);
if (int.TryParse(numberPart, out int number))
return number;
}
throw new InvalidOperationException($"无法解析分块序号: {filePath}");
}
private static Dictionary<string, string> ParseInfoFile(string infoPath)
{
var dict = new Dictionary<string, string>();
foreach (var line in File.ReadAllLines(infoPath))
{
int colonIndex = line.IndexOf(':');
if (colonIndex > 0)
{
string key = line.Substring(0, colonIndex).Trim();
string value = line.Substring(colonIndex + 1).Trim();
dict[key] = value;
}
}
return dict;
}
private static string CalculateFileMD5(string filePath)
{
using (var md5 = MD5.Create())
using (var stream = File.OpenRead(filePath))
{
byte[] hash = md5.ComputeHash(stream);
return BitConverter.ToString(hash).Replace("-", "").ToLowerInvariant();
}
}
}
class Program
{
static void Main(string[] args)
{
Console.WriteLine("=== 文件分割/合并工具 v2.0 ===");
Console.WriteLine("1. 分割文件");
Console.WriteLine("2. 合并文件");
Console.Write("请选择 (1/2): ");
string choice = Console.ReadLine();
try
{
if (choice == "1")
{
Console.Write("源文件路径: ");
string source = Console.ReadLine();
Console.Write("分块大小 (MB,默认 1): ");
string sizeInput = Console.ReadLine();
int mb = string.IsNullOrWhiteSpace(sizeInput) ? 1 : int.Parse(sizeInput);
Console.Write("输出目录 (留空则使用源文件目录): ");
string outputDir = Console.ReadLine();
outputDir = string.IsNullOrWhiteSpace(outputDir) ? null : outputDir;
FileSplitter.SplitFile(source, outputDir, mb * 1024L * 1024L);
}
else if (choice == "2")
{
Console.Write("分块文件目录: ");
string partDir = Console.ReadLine();
Console.Write("原文件名 (如 largefile.zip): ");
string originalName = Console.ReadLine();
Console.Write("合并后输出路径: ");
string output = Console.ReadLine();
Console.Write("是否校验完整性 (y/n, 默认 y): ");
bool verify = Console.ReadLine()?.ToLower() != "n";
FileMerger.MergeFiles(partDir, originalName, output, verify);
}
else
{
Console.WriteLine("无效输入");
}
}
catch (Exception ex)
{
Console.WriteLine($"错误: {ex.Message}");
if (ex.InnerException != null)
Console.WriteLine($"详细信息: {ex.InnerException.Message}");
}
Console.WriteLine("按任意键退出...");
Console.ReadKey();
}
}
FileStream的 Seek方法定位文件位置,避免全文件加载至内存。CopyTo方法(.NET 4.0+)实现流的高效复制,减少缓冲区操作。FileNotFoundException同时提示路径。UnauthorizedAccessException,建议以管理员身份运行。.info文件)。Path.Combine处理路径分隔符( 或/),适配 Windows/Linux/macOS。缓冲区调优默认缓冲区为 4KB,大文件可手动设置更大缓冲区:
using (var partStream = new FileStream(partPath, FileMode.Open, FileAccess.Read, FileShare.Read, 81920))
并行处理(分割时)多个分块可并行写入(需注意磁盘 I/O 竞争):
Parallel.For(0, partCount, i => { /* 写入 part i */ });异步版本采用 ReadAsync / WriteAsync 提升 UI 响应性(适用来 GUI 版本)。
| 功能 | 描述 | 实现难度 |
|---|---|---|
| 图形界面(WPF / WinUI) | 拖拽文件、进度条显示、取消操作 | 中 |
| 加密分割 | 分块采用 AES 加密,合并时解密 | 中 |
| 压缩分割 | 分割前采用 GZip 压缩,减少存储 | 中 |
| 云存储集成 | 分割后自动上传至 S3 / FTP / Azure Blob | 高 |
| 断点续传式合并 | 兼容部分分块缺失时提示 | 低 |
| 多格式兼容 | 兼容 .part、.001、.7z.001 等常用分卷格式 | 低 |
chmod 755)。.info 文件,否则合同时时无法自动校验完整性。本文实现了一个功能完整、生产可用的文件分割与合并工具,核心特点如下所示:
从实现思路看,该工具可直接集成到备份系统、文件传输服务或作为独立命令行工具采用。如需完整项目代码(含 .csproj、单元测试、WPF GUI 示例),可进一步提供。