C#教程

关注公众号 jb51net

关闭
首页 > 软件编程 > C#教程 > C#文件分割与合并工具

基于C#实现的文件分割与合并工具

作者:fie8889

文件分割与合并是数据管理中的基础操作,常用于大文件传输(如邮件附件限制)、分布式存储(拆分后存储至多个介质)、数据备份(分卷压缩)等场景,本工具基于 C# 语言开发实现的文件分割与合并工具,需要的朋友可以参考下

一、系统概述

文件分割与合并是数据管理中的常见操作,广泛应用于以下场景:

本工具基于 C# 语言开发,采用 .NET 6.0 运行时,利用 System.IO 命名空间实现二进制文件流操作。核心特性包括:

二、核心设计思路

2.1 分割策略

SourceFile: archive.zip
FileSize: 104857600
PartCount: 10
ChunkSize: 1048576
MD5: a1b2c3d4e5f6...

2.2 合并策略

三、实现步骤与代码

3.1 开发环境

3.2 文件分割实现

3.2.1 核心函数:SplitFile

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();
        }
    }
}

3.3 文件合并实现(完整版,含校验)

3.3.1 核心函数:MergeFiles

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();
        }
    }
}

3.4 主程序(增强版命令行交互)

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();
    }
}

四、关键技术点

4.1 二进制流高效处理

4.2 异常处理与校验

4.3 跨平台兼容性

五、性能优化建议

缓冲区调优默认缓冲区为 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 等常见分卷格式

七、最佳实践与注意事项

八、总结

本文实现了一个功能完整、生产可用的文件分割与合并工具,核心特点如下:

该工具可直接集成到备份系统、文件传输服务或作为独立命令行工具使用。如需完整项目代码(含 .csproj、单元测试、WPF GUI 示例),可进一步提供。

您可能感兴趣的文章:
阅读全文