"把上个月的数据导成 Excel"——上位机最高频的需求,也是最容易翻车的地方:导一次卡三分钟、导出的数字变科学计数法、文件被占用直接报错。翻车的根源多半是方案选错了:用 Office 自动化(Interop)逐格写,等于让程序遥控一个 Excel 进程干活,慢且依赖环境。这篇把三种导出方案的取舍、大数据量的流式写法、后台导出交互一次讲清。

三种方案:先选对,再谈优化

方案 A · 淘汰 Office 自动化(Interop)

调用本机 Excel 进程逐格赋值。1 万行要几分钟,进程残留、COM 泄漏问题一堆,还要求工控机装 Office。除非要复杂图表排版,否则不用。

极慢依赖Office进程残留
方案 B · 默认 NPOI 直接生成 xlsx

纯 .NET 库,不装 Office,直接写 xlsx 二进制格式。支持样式、合并单元格、数字格式,是上位机报表的默认选择。

免装Office支持样式速度快
方案 C · 海量 CSV 纯文本

十几万行以上的原始数据导出,CSV 边查边写流式输出,内存占用恒定。代价是没有样式,且要处理编码和逗号转义。

流式省内存无样式注意编码
对比项InteropNPOICSV
1 万行耗时(实测)120~180 s1.5~3 s0.5~1 s
10 万行内存占用不可用SXSSF 模式约 150 MB恒定 < 50 MB
样式/合并单元格支持支持不支持
环境依赖必须装 Office
适用场景复杂排版(极少用)日常报表默认海量原始数据、给 MES 对接

▲ 耗时是同一台工控机、同一份数据的实测区间。Interop 慢的原因不是代码写得差,而是每个单元格都是一次跨进程 COM 调用——架构决定的,优化不动。

NPOI 大数据量写法:SXSSF 流式,别全量驻留内存

NPOI 默认的 XSSFWorkbook 是把整个工作簿放内存的,10 万行 × 20 列能吃到 1GB 以上,32 位程序直接崩。正确姿势是用 SXSSFWorkbook 流式写入——只保留窗口内的行在内存,写过的行自动刷到临时文件:

using var wb = new SXSSFWorkbook();   // 流式,默认窗口100行
var sheet = wb.CreateSheet("采集记录");

// 表头 + 样式只建一次,复用给所有行(别每行new样式,会爆)
var headStyle = BuildHeadStyle(wb);
var numStyle  = wb.CreateCellStyle();
numStyle.DataFormat = wb.CreateDataFormat().GetFormat("0.00");

var head = sheet.CreateRow(0);
for (int c = 0; c < columns.Length; c++)
{
    var cell = head.CreateCell(c);
    cell.SetCellValue(columns[c]); cell.CellStyle = headStyle;
}

int r = 1;
foreach (var rec in reader)   // DbDataReader 流式读,别先ToList()
{
    var row = sheet.CreateRow(r++);
    row.CreateCell(0).SetCellValue(rec.Time.ToString("yyyy-MM-dd HH:mm:ss"));
    var v = row.CreateCell(1);
    v.SetCellValue(rec.Value); v.CellStyle = numStyle;
    if (r % 5000 == 0) ReportProgress(r, total);  // 每5000行报一次进度
}
using var fs = File.Create(savePath);
wb.Write(fs);   // 最后一次性落盘

两个高频错误要避开:一是查询结果先 ToList() 再写——10 万行对象全堆内存,等于白用流式;应该用 DataReader 边读边写。二是每个单元格 new 一个 CellStyle——样式表会爆,同格式单元格必须复用同一个样式对象。

交互层:后台导出 + 进度条 + 文件占用处理

导出 3 秒以上就必须做成后台任务,否则界面假死,操作工以为程序崩了去按电源。我们的固定交互:

  • 后台线程干活,进度条走 BeginInvoke 节流刷新(每 5000 行或每 200ms 一次,别每行都刷);
  • 导出期间禁用导出按钮,防止连点生成一堆任务;
  • 完成后弹提示带"打开文件"按钮,Process.Start 直接打开,省得客户满硬盘找文件;
  • 文件名带时间戳:采集记录_20260912_1430.xlsx,避免覆盖昨天的导出。
文件占用是现场最高频的报错:用户上次导出的文件还开着,这次同名导出直接抛 IOException。处理策略:捕获异常后提示"文件被占用",并自动在文件名后加序号重试(_2、_3),别让用户对着报错发呆。另外 CSV 导出给国内用户记得写 UTF-8 BOM,不然 Excel 打开中文全是乱码。

收尾:数字格式的坑,比性能更容易被客户发现

性能问题客户只会抱怨"慢",格式问题客户会认为"数据不对",后者更伤信任。三个必查项:

  • 数值列用数字格式写入(SetCellValue(double)),不要转成字符串——字符串列在 Excel 里没法求和,客户一眼就发现;
  • 长编号列(条码、批号)强制文本格式,否则 "000123" 变 "123"、18 位编号变科学计数法,追溯时全对不上;
  • 时间列统一 "yyyy-MM-dd HH:mm:ss" 字符串,避免 Excel 区域设置把日月搞反。

报表导出这事,选型记住一句话:日常报表用 NPOI 流式写,海量原始数据用 CSV,Interop 留给没人用的复杂排版。如果你的系统现在还在用 Interop 逐格写,换成 NPOI 大概只需要半天,换来的是百倍的导出速度和再也不会弹的"Excel 未响应"。