基于C#實(shí)現(xiàn)的文件分割與合并工具
一、系統(tǒng)概述
文件分割與合并是數(shù)據(jù)管理中的常見操作,廣泛應(yīng)用于以下場(chǎng)景:
- 大文件傳輸:突破郵件附件大小限制(如將 50MB 文件分割為 5×10MB)
- 分布式存儲(chǔ):將文件拆分后存儲(chǔ)至多個(gè)磁盤或云存儲(chǔ)節(jié)點(diǎn)
- 分卷備份:配合壓縮算法實(shí)現(xiàn)分卷壓縮備份
- 斷點(diǎn)續(xù)傳:支持文件的分塊上傳與下載
本工具基于 C# 語(yǔ)言開發(fā),采用 .NET 6.0 運(yùn)行時(shí),利用 System.IO 命名空間實(shí)現(xiàn)二進(jìn)制文件流操作。核心特性包括:
- ? 按固定大小分割(支持自定義分塊大?。?/li>
- ? 按序號(hào)自動(dòng)合并
- ? 完整性校驗(yàn)(MD5 校驗(yàn)和)
- ? 跨平臺(tái)支持(Windows / Linux / macOS)
- ? 異常捕獲與友好的錯(cuò)誤提示
- ? 命令行 / GUI 雙模式支持(GUI 可作為擴(kuò)展)
二、核心設(shè)計(jì)思路
2.1 分割策略
- 按固定大小分割:將源文件拆分為多個(gè)指定大?。ㄈ?
chunkSize = 1024 * 1024字節(jié),即 1MB)的子文件,最后一個(gè)子文件可能小于指定大小。 - 文件命名規(guī)則:子文件命名為
原文件名.partN(N 為序號(hào),從 1 開始),如largefile.zip.part1、largefile.zip.part2。 - 元數(shù)據(jù)記錄(可選):生成
原文件名.info文件,記錄源文件大小、分割數(shù)量、校驗(yàn)和(如 MD5),用于合并時(shí)校驗(yàn)完整性。
SourceFile: archive.zip FileSize: 104857600 PartCount: 10 ChunkSize: 1048576 MD5: a1b2c3d4e5f6...
2.2 合并策略
- 按序號(hào)合并:讀取所有
原文件名.partN子文件,按序號(hào)從小到大拼接為完整文件。 - 完整性校驗(yàn):通過比對(duì)子文件數(shù)量、總大小或 MD5 校驗(yàn)和,確保合并后文件與源文件一致。
三、實(shí)現(xiàn)步驟與代碼
3.1 開發(fā)環(huán)境
- 語(yǔ)言:C# 9.0+
- 框架:.NET 6.0(跨平臺(tái)支持)
- 工具:Visual Studio 2022 / Visual Studio Code
- 核心類庫(kù):
System.IO(文件流)、System.Security.Cryptography(MD5 校驗(yàn))
3.2 文件分割實(shí)現(xiàn)
3.2.1 核心函數(shù):SplitFile
using System;
using System.IO;
using System.Security.Cryptography;
using System.Text;
public class FileSplitter
{
/// <summary>
/// 分割文件為多個(gè)子文件
/// </summary>
/// <param name="sourcePath">源文件路徑</param>
/// <param name="outputDir">輸出目錄(默認(rèn)源文件所在目錄)</param>
/// <param name="chunkSizeBytes">子文件大?。ㄗ止?jié),默認(rèn) 1MB)</param>
/// <param name="generateInfoFile">是否生成 .info 元數(shù)據(jù)文件</param>
/// <returns>分割后的子文件數(shù)量</returns>
public static int SplitFile(string sourcePath, string outputDir = null,
long chunkSizeBytes = 1024 * 1024, bool generateInfoFile = true)
{
// 參數(shù)校驗(yàn)
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;
// 計(jì)算 MD5(需要在讀取流之前計(jì)算,或重新打開流)
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];
// 生成元數(shù)據(jù)文件
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}");
}
}
// 執(zhí)行分割
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} 個(gè)分塊實(shí)際讀取 {bytesRead} 字節(jié),預(yù)期 {bytesToRead} 字節(jié)");
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($"[分割] 已創(chuàng)建: {Path.GetFileName(partPath)} ({bytesRead} 字節(jié))");
}
Console.WriteLine($"分割完成: {partCount} 個(gè)分塊,輸出目錄: {outputDir}");
return partCount;
}
}
/// <summary>
/// 計(jì)算文件的 MD5 校驗(yàn)值
/// </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();
}
}
}3.3 文件合并實(shí)現(xiàn)(完整版,含校驗(yàn))
3.3.1 核心函數(shù):MergeFiles
public class FileMerger
{
/// <summary>
/// 合并子文件為完整文件
/// </summary>
/// <param name="partDirectory">子文件所在目錄</param>
/// <param name="originalFileName">原文件名(如 "archive.zip")</param>
/// <param name="outputPath">合并后的輸出路徑</param>
/// <param name="verifyIntegrity">是否校驗(yàn) MD5(需要 .info 文件)</param>
/// <returns>合并后的文件大?。ㄗ止?jié))</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}");
// 按序號(hào)排序(解析 partN 中的 N)
Array.Sort(partFiles, (a, b) =>
{
int indexA = ExtractPartNumber(a, fullBaseName);
int indexB = ExtractPartNumber(b, fullBaseName);
return indexA.CompareTo(indexB);
});
// 驗(yàn)證序號(hào)連續(xù)性(可選)
for (int i = 0; i < partFiles.Length; i++)
{
int expected = i + 1;
int actual = ExtractPartNumber(partFiles[i], fullBaseName);
if (actual != expected)
throw new InvalidOperationException($"分塊序號(hào)不連續(xù): 期望 {expected},實(shí)際 {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;
// 完整性校驗(yàn)
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($"[校驗(yàn)] MD5 一致: {actualMd5}");
}
else
{
Console.WriteLine($"[警告] MD5 不一致! 期望: {expectedMd5}, 實(shí)際: {actualMd5}");
}
}
else
{
Console.WriteLine("[校驗(yàn)] 未找到 .info 文件,跳過 MD5 校驗(yàn)");
}
}
return mergedSize;
}
/// <summary>
/// 從文件名中提取分塊序號(hào)(如 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($"無法解析分塊序號(hào): {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();
}
}
}3.4 主程序(增強(qiáng)版命令行交互)
class Program
{
static void Main(string[] args)
{
Console.WriteLine("=== 文件分割/合并工具 v2.0 ===");
Console.WriteLine("1. 分割文件");
Console.WriteLine("2. 合并文件");
Console.Write("請(qǐng)選擇 (1/2): ");
string choice = Console.ReadLine();
try
{
if (choice == "1")
{
Console.Write("源文件路徑: ");
string source = Console.ReadLine();
Console.Write("分塊大小 (MB,默認(rèn) 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/n, 默認(rèn) y): ");
bool verify = Console.ReadLine()?.ToLower() != "n";
FileMerger.MergeFiles(partDir, originalName, output, verify);
}
else
{
Console.WriteLine("無效輸入");
}
}
catch (Exception ex)
{
Console.WriteLine($"錯(cuò)誤: {ex.Message}");
if (ex.InnerException != null)
Console.WriteLine($"詳細(xì)信息: {ex.InnerException.Message}");
}
Console.WriteLine("按任意鍵退出...");
Console.ReadKey();
}
}四、關(guān)鍵技術(shù)點(diǎn)
4.1 二進(jìn)制流高效處理
- 使用
FileStream的Seek方法定位文件位置,避免全文件加載至內(nèi)存。 - 通過
CopyTo方法(.NET 4.0+)實(shí)現(xiàn)流的高效復(fù)制,減少緩沖區(qū)操作。
4.2 異常處理與校驗(yàn)
- 文件不存在:拋出
FileNotFoundException并提示路徑。 - 權(quán)限不足:捕獲
UnauthorizedAccessException,建議以管理員身份運(yùn)行。 - MD5 校驗(yàn):合并后對(duì)比源文件與合并文件的 MD5 值,確保數(shù)據(jù)一致性(示例中已記錄源文件 MD5 至
.info文件)。
4.3 跨平臺(tái)兼容性
- 使用
Path.Combine處理路徑分隔符(\或/),適配 Windows/Linux/macOS。 - 通過 .NET Core 運(yùn)行時(shí)實(shí)現(xiàn)跨平臺(tái)部署(需安裝對(duì)應(yīng) SDK)。
五、性能優(yōu)化建議
緩沖區(qū)調(diào)優(yōu)默認(rèn)緩沖區(qū)為 4KB,大文件可手動(dòng)設(shè)置更大緩沖區(qū):
using (var partStream = new FileStream(partPath, FileMode.Open, FileAccess.Read, FileShare.Read, 81920))
并行處理(分割時(shí))多個(gè)分塊可并行寫入(需注意磁盤 I/O 競(jìng)爭(zhēng)):
Parallel.For(0, partCount, i => { /* 寫入 part i */ });異步版本使用 ReadAsync / WriteAsync 提升 UI 響應(yīng)性(適用于 GUI 版本)。
六、擴(kuò)展功能建議(可后續(xù)迭代)
| 功能 | 描述 | 實(shí)現(xiàn)難度 |
|---|---|---|
| 圖形界面(WPF / WinUI) | 拖拽文件、進(jìn)度條顯示、取消操作 | 中 |
| 加密分割 | 分塊使用 AES 加密,合并時(shí)解密 | 中 |
| 壓縮分割 | 分割前使用 GZip 壓縮,減少存儲(chǔ) | 中 |
| 云存儲(chǔ)集成 | 分割后自動(dòng)上傳至 S3 / FTP / Azure Blob | 高 |
| 斷點(diǎn)續(xù)傳式合并 | 支持部分分塊缺失時(shí)提示 | 低 |
| 多格式支持 | 支持 .part、.001、.7z.001 等常見分卷格式 | 低 |
七、最佳實(shí)踐與注意事項(xiàng)
- 磁盤空間要求分割時(shí)輸出目錄需要至少等于源文件大?。缓喜r(shí)目標(biāo)目錄需要至少等于所有分塊大小之和。
- 文件權(quán)限確保程序?qū)υ茨夸浐洼敵瞿夸浘哂凶x寫權(quán)限(Linux 下注意
chmod 755)。 - 元數(shù)據(jù)文件重要性強(qiáng)烈建議保留
.info文件,否則合并時(shí)無法自動(dòng)校驗(yàn)完整性。 - 并發(fā)安全多線程版本需注意同一文件不可被多個(gè)線程同時(shí)寫入。
- 單元測(cè)試建議對(duì)核心函數(shù)編寫單元測(cè)試,覆蓋邊界情況(空文件、單分塊、序號(hào)亂序等)。
八、總結(jié)
本文實(shí)現(xiàn)了一個(gè)功能完整、生產(chǎn)可用的文件分割與合并工具,核心特點(diǎn)如下:
- ? 分割:按固定大小拆分,生成分塊文件 + 可選元數(shù)據(jù)
- ? 合并:自動(dòng)識(shí)別分塊序號(hào),拼接 + 完整性校驗(yàn)
- ? 健壯性:異常處理、路徑兼容、序號(hào)連續(xù)性校驗(yàn)
- ? 可擴(kuò)展:預(yù)留了加密、壓縮、GUI 等擴(kuò)展接口
該工具可直接集成到備份系統(tǒng)、文件傳輸服務(wù)或作為獨(dú)立命令行工具使用。如需完整項(xiàng)目代碼(含 .csproj、單元測(cè)試、WPF GUI 示例),可進(jìn)一步提供。
相關(guān)文章
C#中XmlTextWriter讀寫xml文件詳細(xì)介紹
.NET中包含了很多支持XML的類,這些類使得程序員使用XML編程就如同理解XML文件一樣簡(jiǎn)單。在這篇文章中,我將給出這樣的一個(gè)類的使用示例,這個(gè)類就是XmlTextWriter類2013-04-04
WinForm中comboBox控件數(shù)據(jù)綁定實(shí)現(xiàn)方法
這篇文章主要介紹了WinForm中comboBox控件數(shù)據(jù)綁定實(shí)現(xiàn)方法,結(jié)合實(shí)例形式分析了WinForm實(shí)現(xiàn)comboBox控件數(shù)據(jù)綁定的常用方法與相關(guān)操作技巧,需要的朋友可以參考下2017-05-05
DevExpress獲取TreeList可視區(qū)域節(jié)點(diǎn)集合的實(shí)現(xiàn)方法
這篇文章主要介紹了DevExpress獲取TreeList可視區(qū)域節(jié)點(diǎn)集合的實(shí)現(xiàn)方法,有一定實(shí)用價(jià)值,需要的朋友可以參考下2014-08-08
C# FileStream實(shí)現(xiàn)多線程斷點(diǎn)續(xù)傳
這篇文章主要為大家詳細(xì)介紹了C# FileStream實(shí)現(xiàn)多線程斷點(diǎn)續(xù)傳,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2018-03-03
C#實(shí)現(xiàn)時(shí)間戳與標(biāo)準(zhǔn)時(shí)間的互轉(zhuǎn)
本文主要介紹了C#中時(shí)間戳與標(biāo)準(zhǔn)時(shí)間互轉(zhuǎn)的方法,其中需要注意的是基準(zhǔn)時(shí)間的問題。文中的示例代碼具有一定的學(xué)習(xí)價(jià)值,快來跟隨小編一起了解一下吧2021-12-12
C#實(shí)現(xiàn)FTP文件下載及超時(shí)控制詳解
這篇文章主要為大家詳細(xì)介紹了C#實(shí)現(xiàn)FTP文件下載及超時(shí)控制的相關(guān)知識(shí),文中的示例代碼講解詳細(xì),感興趣的小伙伴可以跟隨小編一起學(xué)習(xí)一下2024-03-03
C#使用Dynamic實(shí)現(xiàn)簡(jiǎn)化反射
這篇文章主要為大家詳細(xì)介紹了C#如何使用Dynamic來實(shí)現(xiàn)簡(jiǎn)化反射,文中的示例代碼講解詳細(xì),具有一定的學(xué)習(xí)價(jià)值,感興趣的小伙伴可以了解一下2023-07-07

