第16章-大文件处理与性能优化

第十六章:大文件处理与性能优化

16.1 大文件处理概述

16.1.1 内存挑战

处理大型Excel文件时,常规方法可能导致内存问题:

  • XSSFWorkbook:将整个文件加载到内存,百万行数据可能占用数GB内存
  • HSSFWorkbook:有65536行的限制
  • 内存溢出(OutOfMemoryException)风险

16.1.2 解决方案

方案 适用场景 优点 缺点
SXSSFWorkbook 大量数据写入 低内存占用 只能写入,不能读取
SAX事件解析 大量数据读取 流式读取,内存低 实现复杂
分批处理 通用场景 简单易用 需要多次IO

16.2 SXSSFWorkbook流式写入

16.2.1 基本使用

using NPOI.XSSF.Streaming;
using NPOI.XSSF.UserModel;
using NPOI.SS.UserModel;

/// <summary>
/// 使用SXSSFWorkbook写入大量数据
/// </summary>
public static void WriteLargeExcel(string filePath, int rowCount)
{
    // 创建SXSSF工作簿(默认内存保留100行)
    SXSSFWorkbook workbook = new SXSSFWorkbook();
    
    // 或指定内存保留行数
    // SXSSFWorkbook workbook = new SXSSFWorkbook(null, 500);  // 保留500行
    
    ISheet sheet = workbook.CreateSheet("大数据");
    
    // 创建表头
    IRow headerRow = sheet.CreateRow(0);
    headerRow.CreateCell(0).SetCellValue("序号");
    headerRow.CreateCell(1).SetCellValue("名称");
    headerRow.CreateCell(2).SetCellValue("数值");
    headerRow.CreateCell(3).SetCellValue("日期");
    
    // 写入大量数据
    Random rand = new Random();
    for (int i = 1; i <= rowCount; i++)
    {
        IRow row = sheet.CreateRow(i);
        row.CreateCell(0).SetCellValue(i);
        row.CreateCell(1).SetCellValue($"Item-{i}");
        row.CreateCell(2).SetCellValue(rand.NextDouble() * 10000);
        row.CreateCell(3).SetCellValue(DateTime.Now.AddDays(-rand.Next(365)));
        
        // 进度显示
        if (i % 100000 == 0)
        {
            Console.WriteLine($"已写入 {i:N0} 行...");
        }
    }
    
    // 保存文件
    using (FileStream fs = new FileStream(filePath, FileMode.Create))
    {
        workbook.Write(fs);
    }
    
    // 清理临时文件
    workbook.Dispose();
    
    Console.WriteLine($"完成!共写入 {rowCount:N0} 行数据");
}

16.2.2 自定义内存设置

/// <summary>
/// 创建自定义配置的SXSSF工作簿
/// </summary>
public static SXSSFWorkbook CreateOptimizedWorkbook(int rowsInMemory = 100)
{
    // 基于XSSFWorkbook创建
    XSSFWorkbook xssfWorkbook = new XSSFWorkbook();
    
    // 创建SXSSF工作簿,指定内存保留行数
    SXSSFWorkbook sxssfWorkbook = new SXSSFWorkbook(xssfWorkbook, rowsInMemory);
    
    // 启用压缩临时文件(减少磁盘占用)
    sxssfWorkbook.CompressTempFiles = true;
    
    return sxssfWorkbook;
}

16.2.3 SXSSFWorkbook注意事项

// 1. SXSSF限制:
// - 不能读取已写入的行(已刷新到磁盘)
// - 不支持某些功能(如自动调整列宽对已刷新行)
// - 必须调用Dispose()清理临时文件

// 2. 自动调整列宽需要在刷新前调用
sheet.AutoSizeColumn(0);  // 仅对内存中的行有效

// 3. 显式刷新行到磁盘
((SXSSFSheet)sheet).FlushRows(100);  // 刷新保留最近100行

// 4. 使用完毕后清理
workbook.Dispose();

16.3 大文件读取优化

16.3.1 SAX事件解析

using NPOI.XSSF.EventModel;
using NPOI.OpenXml4Net.OPC;
using System.Xml;

/// <summary>
/// 使用SAX方式读取大型Excel文件
/// </summary>
public class LargeExcelReader
{
    /// <summary>
    /// SAX方式读取Excel
    /// </summary>
    public static void ReadLargeExcel(string filePath, Action<int, List<string>> rowHandler)
    {
        using (var package = OPCPackage.Open(filePath, PackageAccess.READ))
        {
            XSSFReader reader = new XSSFReader(package);
            SharedStringsTable sst = reader.GetSharedStringsTable();
            
            // 获取工作表
            var sheets = reader.GetSheetsData();
            while (sheets.MoveNext())
            {
                Stream sheetStream = sheets.Current;
                ReadSheet(sheetStream, sst, rowHandler);
            }
        }
    }
    
    private static void ReadSheet(Stream sheetStream, SharedStringsTable sst,
        Action<int, List<string>> rowHandler)
    {
        using (XmlReader reader = XmlReader.Create(sheetStream))
        {
            int rowIndex = 0;
            List<string> rowData = new List<string>();
            
            while (reader.Read())
            {
                if (reader.NodeType == XmlNodeType.Element)
                {
                    if (reader.Name == "row")
                    {
                        rowIndex = int.Parse(reader.GetAttribute("r") ?? "0");
                        rowData.Clear();
                    }
                    else if (reader.Name == "c")
                    {
                        string cellType = reader.GetAttribute("t");
                        string cellValue = "";
                        
                        if (reader.ReadToDescendant("v"))
                        {
                            string value = reader.ReadElementContentAsString();
                            
                            if (cellType == "s")  // 共享字符串
                            {
                                int idx = int.Parse(value);
                                cellValue = sst.GetItemAt(idx).ToString();
                            }
                            else
                            {
                                cellValue = value;
                            }
                        }
                        
                        rowData.Add(cellValue);
                    }
                }
                else if (reader.NodeType == XmlNodeType.EndElement && reader.Name == "row")
                {
                    rowHandler(rowIndex, rowData);
                }
            }
        }
    }
}

// 使用示例
// LargeExcelReader.ReadLargeExcel("large.xlsx", (rowNum, cells) => {
//     Console.WriteLine($"行 {rowNum}: {string.Join(", ", cells)}");
// });

16.3.2 分批读取

/// <summary>
/// 分批读取Excel数据
/// </summary>
public static IEnumerable<List<object[]>> ReadExcelInBatches(string filePath, int batchSize = 1000)
{
    using (FileStream fs = new FileStream(filePath, FileMode.Open, FileAccess.Read))
    {
        IWorkbook workbook = new XSSFWorkbook(fs);
        ISheet sheet = workbook.GetSheetAt(0);
        
        int totalRows = sheet.LastRowNum + 1;
        int currentRow = 0;
        
        while (currentRow < totalRows)
        {
            List<object[]> batch = new List<object[]>();
            
            int endRow = Math.Min(currentRow + batchSize, totalRows);
            for (int i = currentRow; i < endRow; i++)
            {
                IRow row = sheet.GetRow(i);
                if (row == null) continue;
                
                object[] rowData = new object[row.LastCellNum];
                for (int j = 0; j < row.LastCellNum; j++)
                {
                    ICell cell = row.GetCell(j);
                    rowData[j] = GetCellValue(cell);
                }
                batch.Add(rowData);
            }
            
            currentRow = endRow;
            yield return batch;
        }
    }
}

private static object GetCellValue(ICell cell)
{
    if (cell == null) return null;
    
    return cell.CellType switch
    {
        CellType.Numeric => DateUtil.IsCellDateFormatted(cell) 
            ? cell.DateCellValue : cell.NumericCellValue,
        CellType.String => cell.StringCellValue,
        CellType.Boolean => cell.BooleanCellValue,
        CellType.Formula => cell.CachedFormulaResultType == CellType.Numeric 
            ? cell.NumericCellValue : cell.StringCellValue,
        _ => null
    };
}

16.4 性能优化技巧

16.4.1 样式复用

/// <summary>
/// 样式缓存管理器
/// </summary>
public class StyleCache
{
    private readonly IWorkbook _workbook;
    private readonly Dictionary<string, ICellStyle> _cache = new();
    
    public StyleCache(IWorkbook workbook)
    {
        _workbook = workbook;
    }
    
    public ICellStyle GetOrCreate(string key, Action<ICellStyle> configureStyle)
    {
        if (!_cache.TryGetValue(key, out ICellStyle style))
        {
            style = _workbook.CreateCellStyle();
            configureStyle(style);
            _cache[key] = style;
        }
        return style;
    }
    
    // 预定义样式
    public ICellStyle HeaderStyle => GetOrCreate("header", s => {
        s.Alignment = HorizontalAlignment.Center;
        IFont font = _workbook.CreateFont();
        font.IsBold = true;
        s.SetFont(font);
    });
    
    public ICellStyle NumberStyle => GetOrCreate("number", s => {
        IDataFormat format = _workbook.CreateDataFormat();
        s.DataFormat = format.GetFormat("#,##0.00");
    });
    
    public ICellStyle DateStyle => GetOrCreate("date", s => {
        IDataFormat format = _workbook.CreateDataFormat();
        s.DataFormat = format.GetFormat("yyyy-MM-dd");
    });
}

// 使用示例
StyleCache cache = new StyleCache(workbook);
for (int i = 0; i < 100000; i++)
{
    IRow row = sheet.CreateRow(i);
    row.CreateCell(0).CellStyle = cache.NumberStyle;  // 复用样式
    row.CreateCell(1).CellStyle = cache.DateStyle;
}

16.4.2 减少对象创建

/// <summary>
/// 高效写入大量数据
/// </summary>
public static void EfficientWrite(ISheet sheet, List<object[]> data)
{
    // 预先获取样式
    IWorkbook workbook = sheet.Workbook;
    ICellStyle defaultStyle = workbook.CreateCellStyle();
    
    for (int i = 0; i < data.Count; i++)
    {
        IRow row = sheet.CreateRow(i);
        object[] rowData = data[i];
        
        for (int j = 0; j < rowData.Length; j++)
        {
            ICell cell = row.CreateCell(j);
            
            // 直接设置值,避免类型判断
            object value = rowData[j];
            if (value is string s)
                cell.SetCellValue(s);
            else if (value is double d)
                cell.SetCellValue(d);
            else if (value is DateTime dt)
                cell.SetCellValue(dt);
            else if (value is bool b)
                cell.SetCellValue(b);
            else if (value != null)
                cell.SetCellValue(value.ToString());
        }
    }
}

16.4.3 并行处理

/// <summary>
/// 并行处理多个工作表
/// </summary>
public static void ProcessSheetsInParallel(string filePath)
{
    using FileStream fs = new FileStream(filePath, FileMode.Open, FileAccess.Read);
    IWorkbook workbook = new XSSFWorkbook(fs);
    
    // 并行处理各工作表
    Parallel.For(0, workbook.NumberOfSheets, i =>
    {
        ISheet sheet = workbook.GetSheetAt(i);
        ProcessSheet(sheet);
    });
}

private static void ProcessSheet(ISheet sheet)
{
    // 处理单个工作表的逻辑
    for (int i = sheet.FirstRowNum; i <= sheet.LastRowNum; i++)
    {
        IRow row = sheet.GetRow(i);
        // 处理行数据...
    }
}

16.4.4 内存管理

/// <summary>
/// 内存管理最佳实践
/// </summary>
public static void MemoryManagementExample()
{
    // 1. 使用 using 确保资源释放
    using (FileStream fs = new FileStream("file.xlsx", FileMode.Open))
    using (IWorkbook workbook = new XSSFWorkbook(fs))
    {
        // 处理文件...
    }
    
    // 2. 处理大文件时强制GC
    GC.Collect();
    GC.WaitForPendingFinalizers();
    
    // 3. 使用SXSSF时必须Dispose
    SXSSFWorkbook sxssf = new SXSSFWorkbook();
    try
    {
        // 写入操作...
    }
    finally
    {
        sxssf.Dispose();  // 清理临时文件
    }
    
    // 4. 大数据量处理时分批
    const int batchSize = 10000;
    for (int batch = 0; batch < totalBatches; batch++)
    {
        ProcessBatch(batch, batchSize);
        
        // 定期强制GC
        if (batch % 10 == 0)
        {
            GC.Collect();
        }
    }
}

16.5 性能测试与基准

16.5.1 性能测试工具

/// <summary>
/// 简单性能测试
/// </summary>
public static class PerformanceTest
{
    public static void MeasureWritePerformance(int rowCount)
    {
        Console.WriteLine($"测试写入 {rowCount:N0} 行数据...\n");
        
        // 测试普通XSSFWorkbook
        Console.WriteLine("XSSFWorkbook:");
        var sw1 = Stopwatch.StartNew();
        WriteWithXSSF(rowCount, "test_xssf.xlsx");
        sw1.Stop();
        Console.WriteLine($"  耗时: {sw1.Elapsed.TotalSeconds:F2}秒");
        Console.WriteLine($"  内存: {GC.GetTotalMemory(false) / 1024 / 1024}MB\n");
        
        GC.Collect();
        GC.WaitForPendingFinalizers();
        
        // 测试SXSSFWorkbook
        Console.WriteLine("SXSSFWorkbook:");
        var sw2 = Stopwatch.StartNew();
        WriteWithSXSSF(rowCount, "test_sxssf.xlsx");
        sw2.Stop();
        Console.WriteLine($"  耗时: {sw2.Elapsed.TotalSeconds:F2}秒");
        Console.WriteLine($"  内存: {GC.GetTotalMemory(false) / 1024 / 1024}MB\n");
    }
    
    private static void WriteWithXSSF(int rowCount, string filePath)
    {
        using var workbook = new XSSFWorkbook();
        var sheet = workbook.CreateSheet("Data");
        
        for (int i = 0; i < rowCount; i++)
        {
            var row = sheet.CreateRow(i);
            row.CreateCell(0).SetCellValue(i);
            row.CreateCell(1).SetCellValue($"Row {i}");
        }
        
        using var fs = new FileStream(filePath, FileMode.Create);
        workbook.Write(fs);
    }
    
    private static void WriteWithSXSSF(int rowCount, string filePath)
    {
        using var workbook = new SXSSFWorkbook();
        var sheet = workbook.CreateSheet("Data");
        
        for (int i = 0; i < rowCount; i++)
        {
            var row = sheet.CreateRow(i);
            row.CreateCell(0).SetCellValue(i);
            row.CreateCell(1).SetCellValue($"Row {i}");
        }
        
        using var fs = new FileStream(filePath, FileMode.Create);
        workbook.Write(fs);
        workbook.Dispose();
    }
}

16.6 本章小结

本章详细介绍了NPOI处理大文件和性能优化的方法。通过本章学习,你应该掌握:

  • SXSSFWorkbook流式写入的使用
  • SAX事件解析读取大文件
  • 分批处理数据的方法
  • 样式复用减少内存占用
  • 并行处理提高效率
  • 内存管理最佳实践
  • 性能测试和基准方法

大文件处理是企业应用中的常见需求,合理使用这些技术可以有效避免内存问题。


下一章预告:第十七章将介绍企业级应用最佳实践。

posted @ 2025-12-29 10:47  我才是银古  阅读(1)  评论(0)    收藏  举报