☰
OpenXml读写Excel实例:不装Office也能生成解析xlsx
2026/9/30 2:56:19 网站建设 项目流程

简介:面向有一定C#基础的.NET开发者,这份资源是OpenXml读写Excel的实例代码资料,利用DocumentFormat.OpenXml命名空间,使程序无需安装Microsoft Office即可直接创建和解析xlsx文件。资料仅包含1个PDF文档,压缩包大小约50KB,内容为完整C#代码,包含测试入口方法以及封装的OpenXmlSDKExporter类,详细覆盖工作表遍历、DataTable与Excel双向转换、共享字符串读取、数字与布尔类型识别、字体边框等样式定义,代码可直接复制并按业务场景调整。已有565人学习下载,多用于服务器端报表输出、Excel数据批量导入等自动化需求。通过阅读这份PDF,可大幅节省查阅OpenXml SDK文档的时间,快速掌握导入导出的核心写法,同时理解SharedStringTable、CellValue、SheetData等底层对象的工作原理,对二次开发和排错也有直接帮助。

1. OpenXml读写Excel实例代码:不装 Office 也能生成和解析 xlsx

做 .NET 后端的人迟早会遇到一个需求:把数据表导出成 Excel。早期不少项目用 COM 组件调 Excel,服务器上装一套 Office,跑慢、崩溃、残留进程都是家常便饭。用 OpenXml 读写 Excel,等于直接操作 xlsx 文件内部那几张 XML,不需要 Office,性能好,还能精确控制样式。这篇文章不讲泛泛的 API 文档,而是按「最小写入 → 读取 → 踩坑 → 封装」的顺序,给你一套能直接落到项目里的实例代码,顺带把最容易翻车的几个点一次性说清。

2. 用 OpenXml 写 Excel:从创建工作簿到第一行数据落盘

2.1 先理解 xlsx 的包装结构:包、部件与 XML 的关系

很多初学者看到 OpenXml 的第一反应是:为什么比 NPOI 啰嗦那么多?因为 OpenXml SDK 不帮你隐藏 xlsx 的真实结构。一个 .xlsx 文件本质是 zip 压缩包,里面有[Content_Types].xml、xl/workbook.xml、xl/worksheets/sheet1.xml、xl/sharedStrings.xml、xl/styles.xml这些部件。SDK 把这些 XML 映射成了强类型类:SpreadsheetDocument对应整个包,WorkbookPart对应xl/workbook.xml,WorksheetPart对应单个 Sheet 的 XML。

理解这个映射关系比背 API 重要。你写代码时操作的是Worksheet、SheetData、Row、Cell,最终落盘时 SDK 会把这些对象序列化成对应位置的 XML。这也解释了为什么 OpenXml 能处理超大文件而 COM 不行:你控制的是 XML 流,不是 Excel 进程。

写代码前先确认 NuGet 包DocumentFormat.OpenXml装好,我这边用的是 2.20 以上版本,3.x 也兼容。下面这份代码是创建空白工作簿并写入一行文字的最小闭环:

using DocumentFormat.OpenXml; using DocumentFormat.OpenXml.Packaging; using DocumentFormat.OpenXml.Spreadsheet; string filePath = "demo.xlsx"; using (SpreadsheetDocument document = SpreadsheetDocument.Create(filePath, SpreadsheetDocumentType.Workbook)) { WorkbookPart workbookPart = document.AddWorkbookPart(); workbookPart.Workbook = new Workbook(); WorksheetPart worksheetPart = workbookPart.AddNewPart<WorksheetPart>(); worksheetPart.Worksheet = new Worksheet(new SheetData()); Sheets sheets = workbookPart.Workbook.AppendChild(new Sheets()); sheets.AppendChild(new Sheet() { Id = workbookPart.GetIdOfPart(worksheetPart), SheetId = 1, Name = "Sheet1" }); workbookPart.Workbook.Save(); SheetData sheetData = worksheetPart.Worksheet.GetFirstChild<SheetData>(); Row row = new Row { RowIndex = 1 }; Cell cell = new Cell { CellReference = "A1", DataType = CellValues.InlineString }; cell.AppendChild(new InlineString(new Text("OpenXml 写入成功"))); row.AppendChild(cell); sheetData.AppendChild(row); worksheetPart.Worksheet.Save(); }

这段代码有三个关键点。第一,Sheet里的Id必须通过workbookPart.GetIdOfPart(worksheetPart)获取,这是包内部的关系 ID,自己乱填数字,Excel 打开会报“无法访问文件”。第二,字符串单元格我用的是InlineString而不是CellValues.String,前者把内容直接写进单元格 XML,省去维护共享字符串表,也不会在 Excel 里出现“此单元格中的文本为文本格式”的绿角标。第三,CellReference建议手动设置,不设置某些第三方表格软件读不到列位置。

2.2 逐行写入数据与单元格坐标计算

上一节的写法只适合演示。实际业务里你面对的是几十上百列的 DataTable,手动拼CellReference太累,得写一个列名转换函数。Excel 列名是 A、B、C……Z、AA、AB,这本质是 26 进制但跳过了 0,转换逻辑要小心:

private static string GetColumnName(uint columnIndex) { string name = string.Empty; while (columnIndex > 0) { columnIndex--; name = (char)('A' + columnIndex % 26) + name; columnIndex /= 26; } return name; }

有了它,写一整行就容易了。下面这段把DataTable的表头和第一行数据写入 Sheet:

uint rowIndex = 1; Row headerRow = new Row { RowIndex = rowIndex }; for (int col = 0; col < dataTable.Columns.Count; col++) { Cell headerCell = new Cell { CellReference = GetColumnName((uint)(col + 1)) + rowIndex, DataType = CellValues.InlineString }; headerCell.AppendChild(new InlineString(new Text(dataTable.Columns[col].ColumnName))); headerRow.AppendChild(headerCell); } sheetData.AppendChild(headerRow); rowIndex = 2; Row dataRow = new Row { RowIndex = rowIndex }; for (int col = 0; col < dataTable.Columns.Count; col++) { object value = dataTable.Rows[0][col]; if (value is int || value is long || value is decimal) { dataRow.AppendChild(new Cell { CellReference = GetColumnName((uint)(col + 1)) + rowIndex, CellValue = new CellValue(value.ToString()), DataType = CellValues.Number }); } else { Cell textCell = new Cell { CellReference = GetColumnName((uint)(col + 1)) + rowIndex, DataType = CellValues.InlineString }; textCell.AppendChild(new InlineString(new Text(value.ToString()))); dataRow.AppendChild(textCell); } } sheetData.AppendChild(dataRow);

注意数字单元格的写法:CellValue直接赋数字字符串,但不要给DataType或给CellValues.Number。这里有个反直觉的细节:OOXML 规范里数字单元格默认就是 number 类型,所以你可以只填CellValue,但显式声明类型更稳妥。日期列就不能这么写了,日期在 OOXML 底层是个序列号,需要特殊样式配合,我放到 2.3 节一起说。

字符串值要记得处理null,数据库里的DBNull转出来是空串,直接ToString()不会报错,但你得决定空值是写空单元格还是写占位符。我的习惯是保留空单元格,这样读回去的时候能区分“没填过”和“填了空字符串”。

2.3 给单元格加样式:字体、背景色、列宽与合并单元格

OpenXml 的样式不走单元格直接设置,而是走styles.xml里的样式索引。你要先建一个WorkbookStylesPart,往Stylesheet里塞字体、填充、边框和单元格格式,然后给Cell的StyleIndex赋索引值。这是初学者最容易晕的地方,也是文件损坏的重灾区。

最小可用样式表如下:

WorkbookStylesPart stylesPart = workbookPart.AddNewPart<WorkbookStylesPart>(); stylesPart.Stylesheet = new Stylesheet( new Fonts( new Font(new FontSize { Val = 11 }, new FontName { Val = "微软雅黑" }) ), new Fills( new Fill(new PatternFill { PatternType = PatternValues.None }), new Fill(new PatternFill { PatternType = PatternValues.Gray125 }), new Fill(new PatternFill { PatternType = PatternValues.Solid, ForegroundColor = new ForegroundColor { Rgb = "FFF2F2F2" } }) ), new Borders( new Border() ), new CellStyleFormats( new CellFormat() ), new CellFormats( new CellFormat { FormatId = 0, FontId = 0, FillId = 0, BorderId = 0 }, new CellFormat { FormatId = 0, FontId = 0, FillId = 2, BorderId = 0, ApplyFill = true } ) ); stylesPart.Stylesheet.Save();

这里有两个必须记住的规则。第一,Fills集合里第一个元素必须是PatternValues.None,第二个必须是PatternValues.Gray125,这是 OOXML 规范的硬性要求,少了任何一个,Excel 打开就会提示文件损坏并尝试修复。第二,CellFormats里的索引从 0 开始,第 0 个是默认格式,后面的才是你用StyleIndex引用的格式。上面代码里下标 1 的格式有浅灰背景色,给单元格加样式就是赋值StyleIndex = 1。

列宽和合并单元格是另两个独立部件。列宽要插到SheetData前面,合并单元格要追加到SheetData后面,顺序错了也会被 Excel 判定为结构非法:

Columns columns = new Columns(); columns.AppendChild(new Column { Min = 1, Max = 3, Width = 18, CustomWidth = true }); worksheetPart.Worksheet.InsertBefore(columns, worksheetPart.Worksheet.GetFirstChild<SheetData>()); MergeCells mergeCells = new MergeCells(); mergeCells.AppendChild(new MergeCell { Reference = new StringValue("A1:C1") }); worksheetPart.Worksheet.AppendChild(mergeCells);

Min和Max是列号范围,类型是 double,这里 1 到 3 表示 A 到 C 列统一宽 18;合并单元格的Reference是区域字符串。给表头加背景色时要注意,合并单元格的StyleIndex只对合并区域的左上角单元格生效,其它被合并的单元格即便有值,Excel 也不会显示。

日期列的样式比较特殊,它是“数字格式 + 数值”的组合。给CellFormat加一个NumberFormatId = 14,再把DateTime转成 OADate 数值写入:

CellFormat dateFormat = new CellFormat { FormatId = 0, FontId = 0, FillId = 0, BorderId = 0, NumberFormatId = 14, ApplyNumberFormat = true };

写入时用DateTime.ToOADate()转成 double 字符串。这套做法的兼容性最好,因为 Excel 和 WPS 对日期格式 ID 14 的支持都很稳定。

3. 用 OpenXml 读 Excel 的两种姿势:DOM 全量与流式读取

3.1 打开工作簿:可写和只读两种模式怎么选

SpreadsheetDocument.Open有两个常用重载,第二参数isEditable决定能不能写。只读场景传false,SDK 会以只读方式打开 zip 包,不会加载可写资源,性能和安全性都好。需要修改文件再另存时传true,但有个细节:打开后所有修改必须Save(),否则关掉using作用域时改动直接丢掉。

读取前先定位 Sheet。一个工作簿可能有很多 Sheet,workbookPart.Workbook.Sheets里存的是 Sheet 元数据,真正的数据在对应的WorksheetPart里。这两者的关系靠Id绑定:

using (SpreadsheetDocument document = SpreadsheetDocument.Open("demo.xlsx", false)) { WorkbookPart workbookPart = document.WorkbookPart; Sheet sheet = workbookPart.Workbook.Sheets.Elements<Sheet>() .FirstOrDefault(s => s.Name == "Sheet1"); if (sheet == null) return; WorksheetPart worksheetPart = (WorksheetPart)workbookPart.GetPartById(sheet.Id); SheetData sheetData = worksheetPart.Worksheet.GetFirstChild<SheetData>(); }

拿到SheetData后,遍历Row和Cell就能读取全部内容。这里要注意,GetPartById的入参是sheet.Id,不是 Sheet 名称,这个 ID 就是写入时GetIdOfPart生成的关系 ID。如果你拿到的是别人生成的文件,ID 可能是一长串 GUID 或rId1,不需要关心,直接引用即可。

3.2 处理单元格类型:共享字符串、数字、日期与公式

单元格的取值不能直接读CellValue.Text,因为不同类型存储形式完全不同。最常见的是共享字符串:单元格里只有一个索引数字,真正的内容在SharedStringTable里。读取逻辑要先判断DataType,再决定去哪个部件查:

SharedStringTable sharedStringTable = workbookPart.SharedStringTablePart?.SharedStringTable; foreach (Row row in sheetData.Elements<Row>()) { foreach (Cell cell in row.Elements<Cell>()) { string value = string.Empty; if (cell.CellValue != null) { value = cell.CellValue.Text; } if (cell.DataType != null && cell.DataType.Value == CellValues.SharedString) { if (sharedStringTable != null && int.TryParse(value, out int index)) { value = sharedStringTable.ElementAt(index).InnerText; } } Console.WriteLine($"{cell.CellReference}: {value}"); } }

这段逻辑有三个坑。第一,CellValue为 null 不代表单元格为空,它可能是InlineString类型,内容藏在InlineString子节点里。第二,共享字符串的InnerText会把富文本的所有 run 拼起来,这是刻意为之,因为 SDK 的SharedStringItem可能包含多个Run,直接取Text会丢内容。第三,cell.DataType.Value在 SDK 3.x 里是EnumValue<CellValues>,判等时先判断DataType != null再取 Value,否则空引用异常。

数字单元格最简单,CellValue.Text就是数值字符串。但日期单元格是例外:它在 XML 里存的是数字,真正显示成日期是靠StyleIndex指向的NumberFormatId。也就是说,你读日期单元格拿到的可能是一个45012.5这样的浮点字符串,必须结合样式表还原。

3.3 读取样式索引:拿到一个单元格的真实显示值

很多场景需要知道单元格“实际上是什么”。比如读到一个数字,它是日期、百分比还是普通数字?判断依据是 styles.xml 里的CellFormat。每个Cell的StyleIndex指向CellFormats集合中的一个CellFormat,其中NumberFormatId决定显示格式:

using (SpreadsheetDocument document = SpreadsheetDocument.Open("demo.xlsx", false)) { WorkbookPart workbookPart = document.WorkbookPart; WorkbookStylesPart stylesPart = workbookPart.WorkbookStylesPart; CellFormats cellFormats = stylesPart.Stylesheet.CellFormats; // 假设已经定位到某个 Cell 并拿到它的 StyleIndex uint styleIndex = cell.StyleIndex?.Value ?? 0; CellFormat format = cellFormats.ElementAt((int)styleIndex); uint numFmtId = format.NumberFormatId?.Value ?? 0; if (numFmtId >= 14 && numFmtId <= 22) { DateTime dateValue = DateTime.FromOADate(double.Parse(cell.CellValue.Text)); } }

这个方案对常见日期格式有效,但自定义格式就麻烦了。Excel 的NumberFormatId从 164 开始是自定义编号,具体格式字符串存在numFmts集合里。想完整还原所有格式,要把NumberFormatId == 164时查NumberingFormats里的FormatCode,再做判断。实际业务中大多数只需要日期和数字的区分,14 到 22 的区间覆盖了 Excel 内置日期格式,够用。

流式读取是另一个必须掌握的姿势。上面用sheetData.Elements<Row>()会把整个 Sheet 的 DOM 树载入内存,几十万行的文件能吃掉上 GB 内存。改用OpenXmlReader可以按行加载,内存占用降到几十 MB:

using (OpenXmlReader reader = OpenXmlReader.Create(worksheetPart)) { while (reader.Read()) { if (reader.ElementType == typeof(Row)) { Row row = (Row)reader.LoadCurrentElement(); // 当前行已完整载入内存,处理完即释放 } } }

LoadCurrentElement返回强类型Row,后续逻辑和普通遍历完全一致。这就是读取端的主要差别:小文件用 DOM 图省事,大文件切换流式,代码结构几乎不用改。

4. OpenXml 读写 Excel 高频踩坑:文件修复、内存暴涨与空值

4.1 生成的文件提示需要修复:查 Stylesheet 的 Fills 与 Fonts 数量

现象:代码跑完没报错,Excel 打开却提示“发现部分内容有问题,是否尝试恢复”。点击恢复后文件能打开,但样式全丢。

原因:绝大多数是Stylesheet不合规。常见的有三类:Fills不足两个,缺少PatternValues.None和PatternValues.Gray125开头;CellFormat里引用了不存在的FillId或FontId;Fonts集合为空或顺序异常。

解决:Stylesheet的集合数量与顺序必须严格遵守 OOXML 规范。Fonts第一个是默认字体,Fills前两个如上所述,CellFormats第一个是默认格式。我后来养成了写完文件立刻跑一次OpenXmlValidator的习惯,它能直接指出是哪个部件哪个属性不合法,不用每次猜。

4.2 几十万行写入内存暴涨:别把所有 Row 塞进 SheetData

现象:写 5 万行数据一切正常,换到 30 万行,内存从 200MB 一路涨到 1.5GB,最后 OutOfMemory。

原因:sheetData.AppendChild(row)会把每个Row、Cell构建成强类型对象挂在内存树里,所有数据攒齐后Save()才释放。这是 DOM 方式的固有缺陷。

解决:换成OpenXmlWriter流式写入。它直接向 zip 流里写 XML,一行写完整批释放:

using (SpreadsheetDocument document = SpreadsheetDocument.Create("big.xlsx", SpreadsheetDocumentType.Workbook)) { WorkbookPart workbookPart = document.AddWorkbookPart(); workbookPart.Workbook = new Workbook(); WorksheetPart worksheetPart = workbookPart.AddNewPart<WorksheetPart>(); Sheets sheets = workbookPart.Workbook.AppendChild(new Sheets()); sheets.AppendChild(new Sheet() { Id = workbookPart.GetIdOfPart(worksheetPart), SheetId = 1, Name = "Sheet1" }); workbookPart.Workbook.Save(); using (OpenXmlWriter writer = OpenXmlWriter.Create(worksheetPart)) { writer.WriteStartElement(new Worksheet()); writer.WriteStartElement(new SheetData()); for (int i = 1; i <= 300000; i++) { writer.WriteStartElement(new Row { RowIndex = (uint)i }); writer.WriteElement(new Cell { CellReference = "A" + i, DataType = CellValues.Number, CellValue = new CellValue(i.ToString()) }); writer.WriteEndElement(); } writer.WriteEndElement(); writer.WriteEndElement(); } }

注意OpenXmlWriter.Create(worksheetPart)这里传的是WorksheetPart,不是Worksheet。WriteStartElement只能传强类型元素,SDK 会根据传入的元素生成正确的 XML 标签。这个写法不能配合SheetData对象使用,因为流式写入和 DOM 构建是两条路,混用会重复序列化。

4.3 单元格读取为空:区分公式、共享字符串与空单元格

现象:代码遍历单元格,CellValue.Text读到空字符串,但 Excel 里明明显示数字。

原因:三种情况常被混淆。公式单元格的CellValue存的是缓存结果,如果文件由纯代码生成且没有计算缓存,它就是空;共享字符串单元格的CellValue存的是索引数字,不是真实内容;真正空单元格的CellValue是 null,不是string.Empty。

解决:读取顺序应该是先判断CellValue == null,再判断DataType是否为共享字符串,最后处理公式。公式单元格还要额外读CellFormula节点判断里面有没有内容。一个稳妥的取值辅助函数会把InlineString、共享字符串、数值三种类型都覆盖到。

4.4 中文内容打开乱码:检查命名空间与编码处理

现象:代码写入“客户名称”,用 WPS 打开正常,Excel 打开变成乱码;或者反过来。

原因:多半不是编码问题,而是 XML 转义不规范。用字符串拼接生成<c>节点时,特殊字符如&、<、>没转义,Excel 解析 XML 失败后尝试从错误中恢复,结果就是乱码或内容截断。

解决:不要手写 XML,全程用InlineString(new Text(value))。Text类构造时会自动做 XML 转义,这是 SDK 帮你兜底的地方。另外,如果从数据库取到的字符串本身带着换行符,Excel 里会显示成方块,需要把\r\n转成&#10;或Environment.NewLine,直接塞进CellValue也会遇到类似问题。

4.5 WPS 打开正常但 Excel 报错:关注 workbook.xml 的引用关系

现象:同一个文件,WPS 打开完美,Excel 打开提示“Excel 无法打开文件,因为文件格式或文件扩展名无效”。

原因:Excel 的校验比 WPS 严格。常见原因是workbook.xml里的Sheets引用了不存在的WorksheetPart,或者Content_Types漏声明了部件。还有一种隐蔽情况:封装时复制了模板文件,workbook.xml里残留了原文件的definedNames引用。

解决:用 OpenXmlValidator 校验能定位绝大多数问题。另外要注意workbook.xml是Workbook对象序列化的结果,不要手动改 XML;所有 Sheet 引用关系的增删都通过Sheets.AppendChild和GetIdOfPart完成,SDK 会同步维护rels文件,手写 XML 很难保证这些关联一致。

5. 把 OpenXml 封装成自己的 Excel 工具类:从能用变成好维护

5.1 一个可复用的 DataTable 导入导出封装

写了三遍读写逻辑后,我意识到真正该沉淀的是一套小型封装。下面是精简版导出函数,覆盖表头、数据行、数字和文本两种单元格:

public static void Export(DataTable data, string filePath, string sheetName = "Sheet1") { using (SpreadsheetDocument document = SpreadsheetDocument.Create(filePath, SpreadsheetDocumentType.Workbook)) { WorkbookPart workbookPart = document.AddWorkbookPart(); workbookPart.Workbook = new Workbook(); WorksheetPart worksheetPart = workbookPart.AddNewPart<WorksheetPart>(); worksheetPart.Worksheet = new Worksheet(new SheetData()); Sheets sheets = workbookPart.Workbook.AppendChild(new Sheets()); sheets.AppendChild(new Sheet { Id = workbookPart.GetIdOfPart(worksheetPart), SheetId = 1, Name = sheetName }); workbookPart.Workbook.Save(); SheetData sheetData = worksheetPart.Worksheet.GetFirstChild<SheetData>(); sheetData.AppendChild(BuildHeaderRow(data)); for (int r = 0; r < data.Rows.Count; r++) { sheetData.AppendChild(BuildDataRow(data.Rows[r], r + 2)); } worksheetPart.Worksheet.Save(); } }

对应的两个辅助方法:

private static Row BuildHeaderRow(DataTable data) { Row row = new Row { RowIndex = 1 }; for (int col = 0; col < data.Columns.Count; col++) { Cell cell = new Cell { CellReference = GetColumnName((uint)(col + 1)) + "1", DataType = CellValues.InlineString }; cell.AppendChild(new InlineString(new Text(data.Columns[col].ColumnName))); row.AppendChild(cell); } return row; } private static Row BuildDataRow(DataRow dataRow, uint rowIndex) { Row row = new Row { RowIndex = rowIndex }; for (int col = 0; col < dataRow.Table.Columns.Count; col++) { object value = dataRow[col]; if (value == null || value == DBNull.Value) { continue; } if (value is int || value is long || value is decimal || value is double) { row.AppendChild(new Cell { CellReference = GetColumnName((uint)(col + 1)) + rowIndex, DataType = CellValues.Number, CellValue = new CellValue(Convert.ToString(value, CultureInfo.InvariantCulture)) }); } else { Cell cell = new Cell { CellReference = GetColumnName((uint)(col + 1)) + rowIndex, DataType = CellValues.InlineString }; cell.AppendChild(new InlineString(new Text(value.ToString()))); row.AppendChild(cell); } } return row; }

这个封装有几个实用细节。Convert.ToString(value, CultureInfo.InvariantCulture)避免了小数点在德国等地区变成逗号导致 Excel 识别不了;DBNull跳过不创建Cell,读回来时空单元格语义清晰;表头固定占第 1 行,数据行号从 2 开始。这个版本没加样式,想加表头背景色,就按 2.3 节的StyleIndex思路,给表头的所有Cell统一赋索引。

5.2 用 OpenXmlValidator 校验输出文件

写完文件后跑一次校验,是预防“Excel 打不开”最直接的手段。OpenXmlValidator是 SDK 自带的校验器,不需要额外包:

OpenXmlValidator validator = new OpenXmlValidator(); var errors = validator.Validate(document); foreach (ValidationErrorInfo error in errors) { Console.WriteLine($"Part: {error.Part?.Uri}"); Console.WriteLine($"Path: {error.Path?.XPath}"); Console.WriteLine($"Description: {error.Description}"); }

Validate方法接收SpreadsheetDocument或任意部件类型,返回错误集合。注意它只校验 OOXML 语义,不会检查“这个日期格式是否符合业务预期”。我在 CI 里放了一个测试项目,每次改动导出代码就跑一遍,专门生成几百行带样式、合并单元格、日期的 xlsx,再断言errors为空。这个习惯帮我挡住了至少三次线上事故。

5.3 NPOI、MiniExcel、COM 组件与 OpenXml 的边界对照

选择困难症在群里每隔一阵就会发作,我把实际感受放一张表里:

方案Office 依赖内存表现样式能力上手成本适合场景
OpenXml SDK无流式写入时可控精确到 XML 级别较高复杂模板、大数据量、服务端定制
NPOI无XSSF 全量在内存中等中等中小数据量、社区生态成熟
MiniExcel无好较弱低快速导入导出、无样式需求
COM 组件必须安装 Office差强低不建议生产环境使用

我个人的分界线是:需要锁定的表头样式、合并单元格、固定列宽,用 OpenXml 或 NPOI;纯数据搬运,不碰样式,MiniExcel 写起来最快;至于 COM 组件,只在客户明确要求生成 xls 老格式且无替代方案时才考虑。OpenXml兜了一批我对 NPOI 不满的点:它的类型映射更贴近 OOXML 规范,出问题时能顺着 XML 结构排查,而不是在黑匣子里猜。

6. 数据校验与批量写入的进阶习惯:文件能打开才算交付

写 OpenXml 代码这么久,我最大的教训是:不要相信“代码没报错”就等于“文件没问题”。xlsx 是 zip 包加 XML 的组合体,SDK 的强类型替你挡掉了很多低级错误,但样式表合规、部件引用、编码处理这些边界,只有打开文件那一刻才算数。所以我现在每个导出功能交付前,强制自己走三条验证:先跑OpenXmlValidator看语义错误;再用 Excel 和 WPS 各开一次;最后写一段独立读取程序把生成的文件读回来,比对关键单元格的值。这套流程走完,才算真正交付。

还有一个影响效率的习惯:把GetColumnName、MakeTextCell、MakeNumberCell这类基础函数收进一个静态类,所有 Excel 操作共用一份。不同项目复制粘贴代码是踩坑的根源,因为每个项目改着改着就分叉了,某个版本修了日期 bug,另一个版本还带着老写法。我自己曾因为两个项目里两份列名转换函数不一致,导出文件错位到怀疑人生,最后发现一个从 0 开始计数,一个从 1 开始。这种问题不靠细心能解决,只能靠统一封装。

如果你刚接手 OpenXml,建议从最小写入跑通,再逐步加样式、加流式、加读取。这个库的学习曲线陡,但掌握后收益稳定:不依赖 Office、内存可控、文件结构完全透明。真遇到诡异问题,解压 xlsx 直接看 XML,比瞎试 API 参数高效得多。希望这篇实例代码能帮你少踩几个坑。

本文还有配套的精品资源,点击获取

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

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

立即咨询