☰
C# WinForm用NPOI实现Excel导入导出完整指南
2026/10/13 20:37:42 网站建设 项目流程

做WinForm开发的朋友,只要写过管理系统,八成都会接到一个需求:把Excel里的数据导进系统,或者把系统数据导成Excel给客户。很多人一开始觉得这功能简单,不就是读个文件写个文件吗,结果真做起来,格式乱、文件被锁、大数据卡死、日期变成了数字,问题一个接一个。

我这些年用C# WinForm做过不少Excel导入导出的功能,从最早用Office组件,到后来换成纯开源的NPOI方案,踩过的坑算是比较多了。这篇文章就把我在实际项目里总结的完整套路、关键代码和排查经验都整理出来,给正在做类似功能的朋友做个参考。

1. 选型:为什么放着COM不用,偏选NPOI

1.1 先说说我用过的三种方案

很多人在网上搜“WinForm导入导出Excel”,第一眼看到的大概率是微软自带的Microsoft.Office.Interop.Excel,也就是大家常说的COM方式。这个方案我以前也用过,说实话,功能确实强,毕竟是Excel的真身,但问题也不少。

在WinForm客户端里用Interop.Excel,程序员的机器上可能一切正常,因为自家电脑装了Office。但把程序部署到客户那边,麻烦就来了:客户机器装了WPS没装Office,代码跑不起来;装了Office但版本不对,类型库引用异常;即使都装好了,每次调用Excel进程都会在后台起一个进程,操作完之后忘了释放,任务管理器里躺着一堆EXCEL.EXE,时间久了系统越来越卡。我最早的一个项目就因为这个被客户吐槽过好几次,后来才彻底放弃COM方案。

第二个方案是ExcelDataReader,这个库专门做Excel读取,速度很快,尤其适合大数据量的读取场景。但它的短板也很明显:导出功能基本没有,或者说不完善。如果业务只需要导入不需要导出,它是个好选择,可我的项目要导入也要导出,总不能为了一个功能引入两个库来来回回折腾。

第三个方案就是我现在一直用的NPOI,纯粹托管的开源库,不需要安装Office,读写都支持,而且.xls和.xlsx两种格式都能处理。用它部署到客户机器上,不用装任何东西,直接把DLL放过去就能跑,省心很多。我把三种方案对比了一下,NPOI在WinForm这种内网部署场景里综合优势是最明显的。

方案需要安装Office读取能力导出能力部署复杂度我的评价
Interop.Excel是强强高,进程残留问题多只适合简单工具,不适合长期维护
ExcelDataReader否强弱低纯读取场景可选
NPOI否强强低WinForm场景首选

1.2 理解NPOI的对象模型是第一步

NPOI刚上手的时候,很多人会被它的API结构绕晕,因为Excel文件本身是分层的,NPOI也照着这个结构做了对应的类。我画个简单的类比你一下就懂了:IWorkbook是一整本Excel工作簿,ISheet是工作簿里的一张工作表,IRow是工作表里的某一行,ICell是这一行里的一个单元格。它们的关系就是书本、书页、行线、格子的关系,一层套一层。

而且NPOI针对不同版本的Excel文件,给出了不同的实现类:HSSFWorkbook对应老版的.xls文件,XSSFWorkbook对应新版.xlsx文件。写代码的时候要注意这个区别,因为你用哪种Workbook创建出来的文件格式就不一样,直接决定后缀名应当是什么。

在实际开发中,推荐用WorkbookFactory.Create方法去创建Workbook实例,它能根据文件流自动识别是.xls还是.xlsx,返回对应的实现类,这样就不用自己写一堆判断逻辑了,读取文件时非常省事。

2. 导入:从弹窗选文件到数据落库

2.1 导入功能拆解和读取代码

导入Excel的场景,最常见的流程是这样的:用户点一个“导入”按钮,弹出OpenFileDialog选文件,程序读取Excel内容,把每行数据映射到一个业务对象或者DataTable,然后校验、入库,最后把结果反馈给用户。

我一般会把读取Excel的核心逻辑封装成一个方法,输入是文件路径,输出是一个DataTable。使用DataTable作为中间层有个好处,后面不管是绑定到DataGridView展示,还是逐行处理入库,都很方便。

下面这段是我常用的读取Excel到DataTable的代码,逻辑比较完整:

using NPOI.SS.UserModel; using NPOI.HSSF.UserModel; using NPOI.XSSF.UserModel; using NPOI.SS.Util; public DataTable ReadExcelToDataTable(string filePath, string sheetName = null) { DataTable dt = new DataTable(); using (FileStream fs = new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite)) { IWorkbook workbook = WorkbookFactory.Create(fs); // 取指定Sheet,没指定就取第一个 ISheet sheet = sheetName == null ? workbook.GetSheetAt(0) : workbook.GetSheet(sheetName); if (sheet == null) throw new Exception($"未找到名为{sheetName}的工作表"); // 读取表头,作为DataTable的列名 IRow headerRow = sheet.GetRow(0); if (headerRow == null) throw new Exception("Excel文件为空,第一行没有表头数据"); for (int i = 0; i < headerRow.LastCellNum; i++) { ICell cell = headerRow.GetCell(i); string columnName = cell?.ToString()?.Trim(); if (string.IsNullOrEmpty(columnName)) { columnName = $"列{i + 1}"; } // 如果DataTable里已经有同名列,做一下去重 while (dt.Columns.Contains(columnName)) { columnName = columnName + "_1"; } dt.Columns.Add(columnName, typeof(string)); } // 循环读取数据行,跳过第一行表头 for (int rowIndex = 1; rowIndex <= sheet.LastRowNum; rowIndex++) { IRow row = sheet.GetRow(rowIndex); if (row == null) continue; // 跳过完全空的行 DataRow dr = dt.NewRow(); for (int colIndex = 0; colIndex < headerRow.LastCellNum; colIndex++) { ICell cell = row.GetCell(colIndex); dr[colIndex] = cell == null ? "" : cell.ToString(); } dt.Rows.Add(dr); } workbook.Close(); } return dt; }

这段代码里有两个细节我要重点说一下。

第一个是FileShare.ReadWrite。很多人写文件流只用FileMode.Open和FileAccess.Read,没注意FileShare这个参数。如果你的客户习惯一边用Excel开着文件,一边去点程序里的导入按钮,不给FileShare.ReadWrite的话,程序就会报“文件正在被另一个进程使用”的错误。加了FileShare.ReadWrite,至少Excel以只读方式打开时,我们的程序还能读到文件内容。

第二个是sheet.LastRowNum的边界问题。在NPOI里,LastRowNum是从0开始计的行号,也就是说如果文件只有表头一行,LastRowNum是0。我上面的循环从1开始,正好跳过了表头,一直到LastRowNum,这个边界是对的。但如果你拿到的文件里有些行被格式刷过但没有任何数据,GetRow仍然可能返回非null,这时要用row.Cells.Count判断一下有没有真实单元格,必要时再加一层过滤。

2.2 导入过程的校验和容错

导入功能真正的复杂度不在读取数据,而在校验。真实业务里,用户提供的Excel往往千奇百怪,列名对不上、必填项为空、日期格式五花八门、数字列里混着文本,这些都是家常便饭。如果不对这些脏数据做处理,直接往数据库里灌,最后一定是各种异常和脏数据。

我的做法是把校验集中到一个方法里,逐行检查并把错误信息收集起来,统一反馈给用户。错误信息要精确到“第几行第几列是什么问题”,用户才能快速定位并修改原表。比如“第5行第3列:日期格式不正确,应为yyyy-MM-dd格式”。

这里有一个关键的细节,代码里的行号和数据严格讲是有偏差的:Excel的第一行是表头,第二行才是数据第一行,而NPOI里rowIndex从0开始,所以第0行的数据实际是Excel的第2行。给用户报错误行号的时候,一定要做+1再+1的转换。我见过不少同事在这里算错,让用户对着Excel找了半天都找不到问题行。

对于必填项、类型转换这类校验,我建议在DataTable装配阶段就做,因为这时候所有值都还是字符串,类型转换的失败信息最直白。比如把字符串转int失败,就收集一行“第X行第Y列:数字格式不正确”。

另外,导入完成后最好给个汇总反馈,比如“成功导入500条,失败8条”,不要只给一句冷冰冰的“导入成功”。失败的那几条应该单独展示,甚至可以把原始数据和错误原因写进一个临时DataGridView,方便用户直接看,这是提升体验的好习惯。

3. 导出:让数据乖乖变成一份漂亮Excel

3.1 基础导出:先造一份能看的表格

导出Excel和导入相反,是把系统里的数据写到文件里。最常用的数据源是DataGridView的绑定的DataTable,也可能是List<实体类>,不管哪种,先转成统一的表格结构再填充。

下面是一个从DataTable导出到xlsx的基础代码:

public void ExportDataTableToExcel(DataTable dt, string savePath) { IWorkbook workbook = new XSSFWorkbook(); // xlsx格式 ISheet sheet = workbook.CreateSheet("Sheet1"); // 创建表头样式:加粗、居中、背景色 ICellStyle headerStyle = workbook.CreateCellStyle(); IFont headerFont = workbook.CreateFont(); headerFont.IsBold = true; headerStyle.SetFont(headerFont); headerStyle.Alignment = HorizontalAlignment.Center; headerStyle.VerticalAlignment = VerticalAlignment.Center; headerStyle.FillForegroundColor = NPOI.HSSF.Util.HSSFColor.Grey25Percent.Index; headerStyle.FillPattern = FillPattern.SolidForeground; // 写表头 IRow headerRow = sheet.CreateRow(0); for (int j = 0; j < dt.Columns.Count; j++) { ICell cell = headerRow.CreateCell(j); cell.SetCellValue(dt.Columns[j].ColumnName); cell.CellStyle = headerStyle; } // 写数据行 for (int i = 0; i < dt.Rows.Count; i++) { IRow row = sheet.CreateRow(i + 1); for (int j = 0; j < dt.Columns.Count; j++) { ICell cell = row.CreateCell(j); object value = dt.Rows[i][j]; if (value == null || value == DBNull.Value) { cell.SetCellValue(string.Empty); } else if (value is DateTime dateValue) { cell.SetCellValue(dateValue.ToString("yyyy-MM-dd")); } else { cell.SetCellValue(value.ToString()); } } } // 自动调整列宽 for (int j = 0; j < dt.Columns.Count; j++) { sheet.AutoSizeColumn(j); } using (FileStream fs = new FileStream(savePath, FileMode.Create, FileAccess.Write)) { workbook.Write(fs); } workbook.Close(); }

创建Workbook时用的是XSSFWorkbook,这决定了生成的文件格式是.xlsx。如果你的业务需要兼容老版本Excel,就要改成HSSFWorkbook,同时保存对话框中过滤器的扩展名也要跟着改成.xls。

这里有一个非常典型的错误需要提醒:很多人创建Workbook的时候没想好格式,或者干脆统一用XSSFWorkbook生成.xlsx,但保存时用户手动把扩展名改成了.xls,结果Excel打开就报“文件格式和扩展名不匹配”。处理方案是,在保存时通过扩展名来决定用哪种Workbook,或者干脆限制SaveFileDialog只能保存指定的扩展名,把替换扩展名的选项取消掉。

3.2 导出样式与细节:数字、日期、列宽

上面那段代码里,所有单元格都用字符串填充,实际业务中这个做法不够用。比如导出的金额列,用户希望点进去看到的是数字,能参与Excel的求和计算,而不是一坨文本。日期列希望是真正的日期格式,而不是一个看不出类型的字符串。这些诉求涉及NPOI的CellStyle和数据格式。

先说数字。设置单元格为数字格式,要用IDataFormat设置单元格样式的DataFormat。下面这段代码可以把单元格设为保留两位小数的数字格式:

ICellStyle numStyle = workbook.CreateCellStyle(); numStyle.DataFormat = workbook.CreateDataFormat().GetFormat("0.00"); ICell cell = row.CreateCell(0); cell.SetCellValue(1234.56); cell.CellStyle = numStyle;

再说日期。Excel里的日期本质上也是数字,只是通过显示格式变成我们看到的日期。NPOI里定义一个日期格式的单元格样式,然后再写入DateTime值:

ICellStyle dateStyle = workbook.CreateCellStyle(); dateStyle.DataFormat = workbook.CreateDataFormat().GetFormat("yyyy-MM-dd HH:mm:ss"); ICell cell = row.CreateCell(1); cell.SetCellValue(DateTime.Now); cell.CellStyle = dateStyle;

如果不设置DataFormat,直接把DateTime转成字符串“2024/1/5 10:30:00”塞进去,用户拿到Excel后用这个字段做日期筛选或者排序,就会遇到各种奇怪的问题,这是我在实际项目中测试筛选时发现的。

自动列宽那个方法AutoSizeColumn虽然方便,但有个大坑:它是通过遍历整列所有单元格来计算最大宽度的,数据只有几百行时没问题,数据量到几千行以上,整个导出过程会明显变慢,几万行时就是灾难。我的经验是,如果数据量小可以用AutoSizeColumn,数据量大就手动设置固定列宽,比如:

sheet.SetColumnWidth(0, 15 * 256); sheet.SetColumnWidth(1, 25 * 256);

这里的单位是1/256个字符宽度,15*256就是15个字符的宽度。这个值是我自己试出来的,比较适合中文字符的显示宽度,你可以按需调整。

4. 进阶:大文件、进度条和模板导出

4.1 数据量一大就把界面卡死的问题

WinForm做导入导出,最常见的抱怨就是“数据一多,程序就卡死了”。原因很简单,读写Excel的操作是在UI线程里同步执行的,数据量越大,耗时越长,界面就一直处于“未响应”状态,用户等得心焦,甚至会误以为程序崩溃了。

解决思路是用后台线程做耗时操作,同时用进度条给用户反馈。我推荐用Task.Run加IProgress 的组合,原因很简单,它可以在不需要自己写线程同步代码的情况下,安全地更新UI控件。

下面是导出数据时更新进度条的典型写法:

private async void btnExport_Click(object sender, EventArgs e) { SaveFileDialog save = new SaveFileDialog(); save.Filter = "Excel文件|*.xlsx"; if (save.ShowDialog() != DialogResult.OK) return; btnExport.Enabled = false; progressBar1.Value = 0; progressBar1.Visible = true; IProgress<int> progress = new Progress<int>(p => { progressBar1.Value = p; }); try { await Task.Run(() => { ExportWithProgress(dt, save.FileName, progress); }); MessageBox.Show("导出完成", "提示", MessageBoxButtons.OK, MessageBoxIcon.Information); } catch (Exception ex) { MessageBox.Show("导出失败:" + ex.Message, "错误", MessageBoxButtons.OK, MessageBoxIcon.Error); } finally { btnExport.Enabled = true; progressBar1.Visible = false; } }

在ExportWithProgress方法中,每写完100行就调用一次progress.Report,把进度百分比报出去,这样UI线程上的进度条就会平滑更新。注意IProgress 的Report方法会自动把回调封送到UI线程上,所以不用担心跨线程访问控件抛异常。

这里我说一个坑:进度百分比千万不要基于循环次数除以总行数来算。因为导出过程不只是写数据,还要创建Workbook、设置样式、最后write到文件流,这些前期工作也耗时。正确的百分比应该是把整个过程分成几个阶段,比如“初始化10%”“写数据10%-80%”“保存文件80%-100%”。这样用户在进度条上看到的行为更合理,不会出现进度条卡在0%半天的疑惑。

4.2 模板导出才是报表的正确姿势

我在实际项目里发现,不少导出场景要的不仅仅是数据,还要求输出一份“客户指定的格式”,比如固定的表头、公司Logo、固定的纸张设置、某些固定位置填固定的说明文字。这种需求如果纯靠代码一个单元格一个单元格去搭建样式,工作量巨大,而且客户后续改一个字体、加个边框都会找你改代码,非常痛苦。

我的做法是,先让业务人员用Excel手工做一份模板文件,里面排好版式、设置好表头样式、合并好单元格、写好固定文本,只留下需要动态填充的空白位置。然后NPOI打开这个模板文件,往指定位置填入数据,保存为新的文件。这样样式问题变成了模板问题,后续样式变更只需要修改模板,不需要改代码。

模板导出的实现思路很简单:

public void ExportWithTemplate(DataTable dt, string templatePath, string savePath) { IWorkbook workbook; using (FileStream fs = new FileStream(templatePath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite)) { workbook = WorkbookFactory.Create(fs); } ISheet sheet = workbook.GetSheetAt(0); // 模板第5行开始填充数据,前面是固定表头区域 int startRow = 5; for (int i = 0; i < dt.Rows.Count; i++) { IRow row = sheet.GetRow(startRow + i); if (row == null) row = sheet.CreateRow(startRow + i); row.CreateCell(0).SetCellValue(dt.Rows[i]["Name"].ToString()); row.CreateCell(1).SetCellValue(dt.Rows[i]["Amount"].ToString()); // 其他列依次填充 } using (FileStream fs = new FileStream(savePath, FileMode.Create, FileAccess.Write)) { workbook.Write(fs); } workbook.Close(); }

这里要注意一个细节:用WorkbookFactory.Create读取.xlsx模板后直接写入新文件是可行的,但如果是.xls模板,由于HSSFWorkbook的样式机制和XSSF不同,模板中有些样式在写入过程可能不会保留。如果客户对版式要求很高,我建议统一使用.xlsx模板,这样兼容性问题最少。

还有一点,模板里如果有下拉列表或者数据验证,NPOI读取时可能无法完全还原,这个我在做某个设备的模板导出时也遇到过。如果模板过于复杂,建议导出后用Excel重新验证一次模板中的控件是否全部保留。

4.3 合并单元格、下拉列表这类额外需求

合并单元格在Excel导出里很常见,尤其是标题跨列居中那种场景。NPOI操作合并单元格非常直接,用CellRangeAddress指定区域即可:

sheet.AddMergedRegion(new CellRangeAddress(0, 0, 0, 5)); IRow titleRow = sheet.CreateRow(0); titleRow.CreateCell(0).SetCellValue("员工信息汇总表");

这里第一个参数是起始行,第二个是结束行,第三个是起始列,第四个是结束列,都是0开始的索引。上面这段代码就是把第一行的第0列到第5列合并成了一个单元格。合并完单元格之后需要注意一点:只应该给合并区域左上角的那个单元格赋值,其他单元格写入的数据不会显示,这是个很容易踩的小坑。

下拉列表的需求,一般是为了限制用户填表时只能选固定选项。NPOI里用XSSFDataValidationHelper来实现。下面是给某列设置下拉选项的代码:

string[] options = { "启用", "禁用", "待定" }; var helper = new XSSFDataValidationHelper((XSSFSheet)sheet); var constraint = helper.CreateExplicitListConstraint(options); var validation = helper.CreateValidation(constraint, new CellRangeAddressList(1, 100, 2, 2)); sheet.AddValidationData(validation);

ArrayFormula、图片、图表、OLE对象这些Excel高级功能,NPOI虽然部分支持,但实现复杂度较高、效果不一定好。如果业务上有这些需求,我建议评估是否能通过模板导出解决,不要让代码去硬造复杂Excel对象,性价比太低。

5. 高频问题排查实录

5.1 我自己遇到的坑和一些排查技巧

每个做导入导出的人,都会积累一笔血泪账。我这里把最高频的几个问题拎出来集中排查,帮大家避坑。

第一个是“读取Excel时文件被占用”。这个现象一般发生在用户用Excel打开了文件,然后在程序里点导入的时候。解决方法是:打开文件流时用FileShare.ReadWrite,也就是我上面代码里写的那样。这样即使文件在Excel里打开,只要没有被独占锁定,我们都可以读。

第二个是“导出的日期列,用Excel打开后变成了一串数字”。比如2024-03-15变成45090,这是Excel把日期存成数字序列的默认行为。原因就是写入单元格时没有给它指定日期格式的CellStyle。处理办法就是给日期列设置DataFormat,强制显示为日期格式,这个我在前面已经写了详细代码。

第三个是“读取单元格的数值,变成了科学计数法”。这是因为在NPOI里读取数值类型单元格后,我们通常把double直接ToString,而double的默认字符串化在某些情况下会是科学计数法形式。解决办法是统一转成decimal,再用“0.########”之类的格式字符串格式化,或者使用decimal.Parse后ToString("0.####")。

第四个是“CSV文件导入乱码”。NPOI本身不支持CSV文件的读写,它只认真正的Excel格式。如果你需要支持CSV导入,基本原理是把文件按编码规则读出文本,再去解析。最常见的坑是:用户直接用Excel新建并另存为CSV,这个文件是ANSI编码的,而程序默认用UTF-8读取,结果中文全部乱码。处理方法是读取CSV文件时检测编码,如果带BOM就用UTF-8,否则按GBK读取。这里不展开代码,但方向就是这样。

5.2 常见问题速查表

把上面说的这些整理成一张速查表,方便同事们直接查。

现象可能原因处理方案
导入时报文件被占用文件流没加FileShare.ReadWrite打开时用FileShare.ReadWrite
Excel打开导出的文件提示格式不对文件内容格式与扩展名不匹配根据扩展名选择HSSF或XSSFWorkbook
日期列显示为数字缺少日期格式CellStyle设置DataFormat为日期格式
数字变成科学计数法double.ToString默认格式转成decimal再用格式字符串
数据量大时界面假死UI线程同步处理用Task.Run加IProgress
单元格内容全是#####列宽设置过窄用SetColumnWidth调整列宽
导入时把1月读成了1/1/2024单元格写入时自动格式化为日期检查模板单元格格式,设为文本
读取到公式本身不是计算结果单元格类型是Formula用CachedFormulaResultType获取缓存结果
CSV中文乱码编码判断错误根据BOM检测编码,无BOM按GBK
导出数据量过大时内存溢出XSSFWorkbook全部缓存在内存数据量极大时用流式写入或拆分导出

5.3 一个追查思路借鉴:公式单元格的坑

公式单元格在处理导入导出时是比较隐蔽的问题。假设系统里导出一张带合计行的Excel,合计是用Excel公式算出来的,比如“=SUM(B2:B10)”,那么当另一个同事用NPOI读这张表时,默认读到的是这个单元格的类型是Formula,ToString返回的是公式字符串,而不是计算结果。

解决办法是,在读取单元格前先判断CellType,如果是Formula类型,就去取它的缓存计算结果:

if (cell.CellType == CellType.Formula) { var cachedType = cell.CachedFormulaResultType; if (cachedType == CellType.Numeric) value = cell.NumericCellValue; else if (cachedType == CellType.String) value = cell.StringCellValue; else value = cell.ToString(); }

CachedFormulaResultType是上次Excel保存时计算结果的类型。但要注意,如果你用程序生成的文件里带有公式,但从未被Excel打开过,这个缓存值可能不存在,这时读缓存会得到0或者空。这个坑的深层含义是:公式不是万能的好东西,能在程序里算好再写入,就不要依赖Excel公式的缓存结果,否则下游读取方很容易出问题。

6. 写在最后的实操心得

6.1 我的几个小原则

做了这么多次Excel功能,我的体会大概可以总结成几条。

第一条,能用NPOI绝不碰COM。WinForm项目,尤其是要部署到不同客户环境的,COM方案的风险在于不知道客户机器上有啥、没啥,纯托管的NPOI能最大程度消除环境不确定性。

第二条,数据传输不要直接冲UI控件去,尽量用DataTable作为中间层。DataTable切换数据源容易,也方便做校验和转换,后续换成CSV导入导出也不会牵一发动全身。

第三条,导入导出所有错误信息,一定要精确到行号和列名,不要只给一个笼统的“解析失败”。因为处理Excel的用户往往不是开发者,他们面对一串技术异常信息根本不知道从哪下手,精确到“第6行第3列:日期格式错误”才能让他们直接把问题解决掉。

第四条,文件流用完就释放,用using或者手动Close。文件读写有个特性,不释放的话临时文件会越来越多,程序跑久了还会出现莫名其妙打不开文件的情况。我见过同事在循环里导出多个文件,忘了释放workbook,结果导出30个文件后程序开始报资源不足,排查了半天才发现是这个原因。

另外我建议把导入导出的逻辑从窗体代码里剥离出来,放到一个独立的ExcelHelper类中,方法签名只暴露DataTable和文件路径。好处很多:单元测试方便,后续做自动化回归也方便,万一哪天业务要求改成CSV导入,只需要新增一个实现类,窗体层代码基本不用动。

6.2 后续扩展的一些想法

有些项目到后期会提出“导出数据量太大,一次能撑得住吗”。Excel单表上限是1048576行,看起来挺大,但实际真到几十万行的时候,XSSFWorkbook在内存中构建会非常消耗资源,动不动就是上GB的内存占用,容易触发OOM。

如果遇到这种极端场景,我的建议不是硬撑,而是考虑使用流式写入,NPOI对XSSFWorkbook提供了SXSSFWorkbook这种流式实现,只在需要的时候把当前窗口的行写到磁盘,避免一次性把所有行缓存到内存。它和普通XSSFWorkbook的区别有点像分批处理,消耗低,但有代价:不支持读取模板中的部分对象,也没有完全保留所有样式能力,只能作为特殊大数据量场景的降级方案。

如果数据量真的到了百万行级别,我个人的建议是直接把数据导出成CSV格式,另存为.csv文件,用Excel照样能打开,生成速度快,内存占用低,是性价比最高的方案。特别是现在很多下游系统本身就支持CSV格式导入,不比Excel差。

最后再说一句我在实际项目中体会最深的东西:导入导出功能的重点从来不在库的API记了多少,而在于对各种边界情况的处理是否让人省心。把异常提示写得清楚,把格式理顺,把进度条做了,把一个看似普通的Excel功能做得顺滑稳当,这才算真正把功能做完整了。希望这篇文章能帮你少踩点坑。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询