
简介面向.NET开发人员的OpenXml读写Excel文件实例代码资源核心演示如何在不安装微软Office的前提下借助OpenXml SDK完成xlsx工作簿的导入与导出适用于信息系统中常见的报表生成、批量数据转换、服务端文档自动化处理等场景。资源仅包含1个PDF文档体积约50KB文档中提供了两个关键方法读取方向会逐个遍历工作表与行使用单元格引用识别列并通过共享字符串表、数据类型的正则判断正确还原文本、数值等写入方向则支持将多个DataTable导出为独立工作表同时展示了字体、填充、边框、单元格格式等样式表对象的构造方式。整体代码可读性强、关键API均有注释尤其适合需要从零开始使用OpenXml、或希望在此基础上封装自有Excel工具的初级与中级C#开发人员快速上手。目前已有565人学习下载内容精炼能有效缩短查阅官方文档和调试的时间。1. 不装Office也能批量改ExcelOpenXml读写xlsx的落地思路很多开发者在服务器上批量处理Excel时第一反应是装一套Office然后调用COM组件结果进程卡死、权限报错、格式错乱还经常因为Excel设置了弹窗导致任务挂在那不动。OpenXml走的是另一条路它不启动Excel进程而是直接读写xlsx文件内部的XML结构所以适合在无Office环境、Linux服务器、CI流水线里做Excel的读写和批量生成。标题里的“OpenXml读写Excel实例代码”对应的就是一套不依赖Office、用C#就能把xlsx读进来、写出去、还能改样式的完整方案。适合被COM折磨的后端开发者也适合所有需要在服务端处理Excel批量导入导出、模板填充的数据工程场景。2. 读懂xlsx再动手OpenXml读写Excel为什么能绕开Office进程2.1 一个xlsx文件就是一个zip包单元格、样式、共享字符串都拆开了存要上手OpenXml第一件事是忘掉“Excel文件”这个黑匣子印象。xlsx其实就是一个zip压缩包里面按固定目录结构放着若干XML文件xl/workbook.xml记录工作簿里有几个Sheetxl/worksheets/sheet1.xml存每个Sheet的实际单元格数据xl/sharedStrings.xml存所有重复使用的文本xl/styles.xml存字体、颜色、边框等样式。打开文件、读单元格、改样式本质就是在解压、解析XML、按规则改完再压缩回去。下面是一个简化过的sheet1.xml片段你看一眼就知道单元格在底层长什么样sheetData row r1 c rA1 tsv0/v/c c rB1v100/v/c c rC1 tinlineStrist项目A/t/is/c /row /sheetData这里的c代表cellr是坐标v是值t是数据类型。ts表示这个单元格的内容在共享字符串表里v里存的是索引号tinlineStr表示文本直接写在单元格里不写t时默认是数字。理解了这套结构OpenXml的很多API行为就顺理成章了读文本要去查共享字符串表写文本要选inline还是共享方式写数字不用打引号日期则要配合样式才能正常显示。2.2 OpenXml与COM、NPOI、EPPlus的选型对比什么时候该换它市面上的Excel处理框架很多但选型要看你的运行环境。常见做法是对比这几个方案COM组件、NPOI、EPPlus、OpenXml SDK。COM是微软官方的老路子能调Excel所有功能但它要求目标机器装了Office而且进程回收、权限配置、并发调用全是坑服务器上尤其容易出幺蛾子。NPOI是Apache POI的.NET移植版不依赖Office环境友好但处理样式和复杂模板时API偏老有些Excel新特性支持得慢。EPPlus功能丰富写复杂报表很顺手但商业使用时要注意授权范围不是无条件的免费。OpenXml SDK的优势在于它由微软维护、MIT协议开源不依赖Office安装直接对应xlsx文件格式本身。它的代价是API比较底层你得知道workbook、worksheet、cell、style这些概念之间的关系。我的判断标准是如果你只在Windows桌面且机器上保证有Office用COM没问题如果要做服务端批处理、模板填充、数据导入导出优先考虑OpenXml SDK或EPPlus如果要维护老项目或在意授权风险OpenXml是最稳的底牌。OpenXml就像Excel处理框架里最接近文件底层的一层其他框架多数也是在它的格式规范上做封装。2.3 环境准备安装SDK并识别两种打开方式环境准备很简单NuGet搜DocumentFormat.OpenXml装上就行Visual Studio里用包管理器命令行用dotnet也可以Install-Package DocumentFormat.OpenXmldotnet add package DocumentFormat.OpenXml安装完成后代码里需要引入两个命名空间using DocumentFormat.OpenXml.Packaging; using DocumentFormat.OpenXml.Spreadsheet;Packaging下面放着SpreadsheetDocument这种文档容器类负责打开和创建的入口Spreadsheet下面放着Workbook、Worksheet、SheetData、Cell这些和Excel结构对应的类型。打开文档最常用的方法是SpreadsheetDocument.Open(path, isEditable)第二个参数传false是只读适合查数据传true是可写适合原地修改。还有一个Create方法专门用来新建文件后面的实例代码会依次用到。3. 读Excel实例代码从Workbook到单元格值的三个必经步骤3.1 只读打开文档用SpreadsheetDocument.Open(path, false)拿到工作表清单读取Excel的第一步是用只读方式打开文档这样不会给文件加锁也不会有“文件正被另一个进程占用”的副作用。我一般先把文件复制到临时目录再打开尤其是文件在共享盘或别人正在编辑时这个习惯能省掉很多玄学问题。下面是最小可用的读取遍历代码using DocumentFormat.OpenXml.Packaging; using DocumentFormat.OpenXml.Spreadsheet; string path D:\data\sample.xlsx; using (SpreadsheetDocument doc SpreadsheetDocument.Open(path, false)) { WorkbookPart workbookPart doc.WorkbookPart; foreach (Sheet sheet in workbookPart.Workbook.Sheets) { Console.WriteLine($工作表: {sheet.Name}); WorksheetPart wsPart workbookPart.GetPartById(sheet.Id) as WorksheetPart; SheetData sheetData wsPart.Worksheet.GetFirstChildSheetData(); foreach (Row row in sheetData.ElementsRow()) { foreach (Cell cell in row.ElementsCell()) { Console.Write(${cell.CellReference} {cell.CellValue?.Text} ); } Console.WriteLine(); } } }这段代码的逻辑分三层先通过WorkbookPart拿工作簿信息再通过GetPartById(sheet.Id)把逻辑Sheet映射到具体的WorksheetPart最后从Worksheet下取SheetData。需要说明的是SheetData才是真正装行的容器Workbook.Sheets只是工作表的路由表两者不要搞混。cell.CellReference返回A1风格的坐标调试时很有用cell.CellValue是单元格值对象如果单元格是空的它可能是null直接.Text会抛空引用所以正式代码里要做null判断。3.2 遍历单元格并处理SharedString中文文本不藏在单元格里上面的代码只能满足“全是数字”的文件。一旦遇到中文你会发现读出来的值是0、1这样的整数而不是原文。这不是数据丢了而是它按共享字符串表存了。xlsx里为了让文件更小所有重复文本会收进SharedStringTable单元格里只留索引。OpenXml不会替你“翻译”必须自己查表。处理共享字符串的读取方法一般长这样private static string GetCellValue(WorkbookPart workbookPart, Cell cell) { if (cell.CellValue null || string.IsNullOrEmpty(cell.CellValue.Text)) return string.Empty; if (cell.DataType CellValues.SharedString) { SharedStringTablePart sharedStringPart workbookPart .GetPartsOfTypeSharedStringTablePart() .FirstOrDefault(); if (sharedStringPart null) return string.Empty; int index int.Parse(cell.CellValue.Text); SharedStringItem item sharedStringPart.SharedStringTable .ElementsSharedStringItem() .ElementAt(index); return item.InnerText; } return cell.CellValue.Text; }这段代码有一个关键分支cell.DataType是SharedString时才去查共享字符串表查表时把单元格里的Text转成int索引再从SharedStringItem集合里取对应项。InnerText会拼接富文本片段比逐个取Text元素更省事。如果你读出来的单元格文本有换行或者拆成多段的富文本用InnerText基本都能还原不会漏字。参数上要注意GetPartsOfTypeSharedStringTablePart().FirstOrDefault()在文件没有共享字符串时返回null必须先判空再操作否则直接炸。3.3 读取指定单元格的通用方法判空、日期和数字类型还原批处理场景里更多时候不是遍历全表而是定点取某个坐标的值比如只读B2和D5。我习惯封装一个按引用取值的辅助方法配合前面的GetCellValue一起用private static string GetCellByReference(WorksheetPart wsPart, WorkbookPart wbPart, string reference) { Cell cell wsPart.Worksheet .GetFirstChildSheetData() .DescendantsCell() .FirstOrDefault(c string.Equals( c.CellReference?.Value, reference, StringComparison.OrdinalIgnoreCase)); if (cell null) return string.Empty; return GetCellValue(wbPart, cell); }调用时只要写GetCellByReference(wsPart, wbPart, B2)就行。这里有两个容易踩的点一是坐标比较要用OrdinalIgnoreCase因为Excel不区分大小写而XML里的引用偶尔会写成小写二是DescendantsCell()返回的是懒序列在超大表上频繁调用会有性能损耗如果只需要读几个固定单元格这个写法完全够用如果要跑全量统计建议回到上一节的Row遍历逐行处理而不是反复全表搜索。日期和数字也有坑。日期在xlsx里本质是一个数字序列号加了一个日期格式样式才能显示成2025-06-01。所以如果你读出来是46256.0之类的浮点字符串不要惊讶需要自己判断单元格的StyleIndex对应的数字格式再决定转成日期还是保留数值。最简单的方式是先用double.TryParse看能不能转成double如果列头或业务约定这一列是日期再把这个数字序列号转换成DateTime。我的经验是读取时不要试图“智能判断”按业务列的约定去转换比靠样式猜测可靠得多。提示如果文件里的单元格是公式cell.CellValue可能为空或者只有上次Excel计算后缓存的值。OpenXml本身不计算公式这一点到第5章再细说。4. 写Excel实例代码新建工作簿、填数据、挂样式一步到位4.1 新建xlsx并写入数据Create、WorkbookPart与WorksheetPart的配合新建文件比读取多几步核心是先创建工作簿再挂工作表最后往SheetData里塞行和单元格。下面是一段可运行的写入代码生成一个大单子格式的表格using DocumentFormat.OpenXml.Packaging; using DocumentFormat.OpenXml.Spreadsheet; string path D:\output\新建报表.xlsx; using (SpreadsheetDocument doc SpreadsheetDocument.Create(path, SpreadsheetDocumentType.Workbook)) { WorkbookPart workbookPart doc.AddWorkbookPart(); workbookPart.Workbook new Workbook(new Sheets()); WorksheetPart wsPart workbookPart.AddNewPartWorksheetPart(); wsPart.Worksheet new Worksheet(new SheetData()); Sheets sheets workbookPart.Workbook.GetFirstChildSheets(); sheets.AppendChild(new Sheet { Name 报表1, SheetId 1, Id workbookPart.GetIdOfPart(wsPart) }); SheetData sheetData wsPart.Worksheet.GetFirstChildSheetData(); Row headerRow new Row { RowIndex 1 }; headerRow.AppendChild(new Cell { DataType CellValues.InlineString, CellValue new CellValue(项目名) }); headerRow.AppendChild(new Cell { DataType CellValues.Number, CellValue new CellValue(数量) }); sheetData.AppendChild(headerRow); workbookPart.Workbook.Save(); }逻辑拆开看就三步AddWorkbookPart建工作簿AddNewPartWorksheetPart建工作表Sheet挂到Sheets集合里并设置Name、SheetId、Id三个属性。Id必须用workbookPart.GetIdOfPart(wsPart)来取不能自己随便填否则打开文件会报“无法找到部件”的错误。单元格赋值时文本类型建议显式指定InlineString数字指定Number这样Excel打开后不会出现“文本型数字”的绿色三角标。写完数据最后要Save()不保存的话改动的XML不会落盘。4.2 给单元格加背景色、边框和字体Stylesheet对象的接线只往xlsx里塞值很容易但交付给业务方看的表通常要带表头背景色、边框、加粗字体。OpenXml的样式体系比NPOI别扭它要先建一个WorkbookStylesPart在Stylesheet里一次性定义好字体、填充、边框和单元格格式然后单元格通过StyleIndex引过去。下面是我常用的最小样式配置WorkbookStylesPart stylesPart workbookPart.AddNewPartWorkbookStylesPart(); stylesPart.Stylesheet new Stylesheet( new Fonts( new Font(), new Font(new FontName { Val 微软雅黑 }) ), new Fills( new Fill(), // index 0: 默认无填充 new Fill(new PatternFill { PatternType PatternValues.Solid, ForegroundColor new ForegroundColor { Rgb FFD9E1F2 }, BackgroundColor new BackgroundColor { Indexed 64 } }) ), new Borders( new Border(), new Border( new LeftBorder(), new RightBorder(), new TopBorder(), new BottomBorder() ) ), new CellStyleFormats(new CellFormat()), new CellFormats( new CellFormat(), // index 0: 默认格式 new CellFormat // index 1: 你自己的样式 { FontId 1, FillId 1, BorderId 1, ApplyFont true, ApplyFill true, ApplyBorder true } ) );这段代码看起来啰嗦但每个索引都是有讲究的Fonts、Fills、Borders、CellFormats都是从0开始的集合所以你想在单元格样式表里用“第二套字体”就要保证FontId 1指向Fonts集合的第2项。ForegroundColor.Rgb是ARGB格式FFD9E1F2就是浅蓝色。配置好之后写表头的时候给Cell加一行StyleIndex 1单元格就会套用这套样式。第一次写容易漏了CellStyleFormats或者集合顺序不对文件生成后一打开Excel就提示“需要修复”这是最常见的翻车现场。4.3 修改现有文件以可写方式打开并原地更新单元格批量场景里新建文件只是一半需求另一半是改模板。比如你现在有一个做好的空白模板只需要往指定格子填数据。这种需求用OpenXml做反而比COM简单因为不用“打开Excel再定位单元格”直接改XML才是真正的原地更新using (SpreadsheetDocument doc SpreadsheetDocument.Open(path, true)) { WorkbookPart workbookPart doc.WorkbookPart; Sheet sheet workbookPart.Workbook.Sheets .ElementsSheet() .FirstOrDefault(s s.Name 报表1); if (sheet null) return; WorksheetPart wsPart workbookPart.GetPartById(sheet.Id) as WorksheetPart; Cell cell wsPart.Worksheet .DescendantsCell() .FirstOrDefault(c c.CellReference A1); if (cell ! null) { cell.CellValue new CellValue(已更新); cell.DataType CellValues.InlineString; wsPart.Worksheet.Save(); } }SpreadsheetDocument.Open(path, true)的第二个参数true是关键表示以可写模式打开。找到目标单元格后直接替换CellValue并重新设置DataType最后调用Worksheet.Save()把改动写回文件。这里有个细节如果原单元格是一个共享字符串类型的单元格只改CellValue不改DataTypeExcel会把它当索引去查共享字符串表结果就是一串退不回去的数字或乱码。所以凡是改文本我都会同时把DataType设置为InlineString彻底避开共享字符串表的缓存陷阱。5. OpenXml读写Excel的踩坑排查5个常见翻车点与解决办法5.1 打开文件即报OOM或文件损坏多半不是你代码的锅现象SpreadsheetDocument.Open抛OutOfMemoryException或者打开后Excel提示文件损坏。原因最常见的是文件本身被Excel进程占用只读模式下打开一个正被独占写锁的文件SDK读取一半得到不完整流还有一种情况是你写入时用了错误的结构比如SheetData后面又追加了非法节点XML结构不满足schema。解决读取前先复制文件到临时路径再打开写入代码写完后用OpenXmlValidator验证一遍发现结构错误立刻修正。别在using块里用doc.Clone()之类的操作它会把整个包加载进内存大文件很容易OOM。5.2 读出来是数字0而不是中文忘了查共享字符串表现象遍历单元格看到DataType SharedString的单元格读.CellValue.Text返回0或1这种小数字中文全丢了。原因这个数字是共享字符串索引不是内容。之前第3章讲过xlsx为了压缩重复文本把字符串统一放到SharedStringTable里。OpenXml不会自动翻译索引。解决统一走带共享字符串处理的GetCellValue方法不要直接读CellValue.Text当结果。写代码时记住读文本前先判断cell.DataType CellValues.SharedString是索引就去查表这是最稳妥的路径。5.3 写进去的日期显示成数字缺少类型声明和数字格式现象写入2025-06-01后打开Excel看到的是45845或者2025-06-01变成了左对齐文本。原因xlsx的日期本来就是数字序列号需要把单元格声明为日期类型并套上日期格式样式Excel才显示成人话。直接用CellValues.Date再塞yyyy-MM-dd字符串很多版本里不生效因为底层还是期望序列号。解决先把DateTime转成ToOADate()的double值写入CellValue然后在样式表里配一个数字格式yyyy-mm-dd通过StyleIndex引用。Cell dateCell new Cell { DataType CellValues.Number, CellValue new CellValue(dt.ToOADate().ToString(CultureInfo.InvariantCulture)), StyleIndex dateStyleIndex };dateStyleIndex对应的CellFormat里要设置NumberFormatId指向自定义数字格式这一步漏了就是纯数字。5.4 公式单元格读不到值OpenXml不会帮你算现象用OpenXml读取一个SUM(A1:A10)的公式单元格CellValue为null或者拿到的是上次Excel保存时缓存的值。批量读取时发现某些“实时计算”的格子全是空。原因xlsx文件里公式本身存的是表达式结果只是缓存。OpenXml SDK是格式读写库不是计算引擎它不会执行公式。解决两种路子。如果公式是上游Excel已经算好并保存的直接读缓存值如果数据是自己生成的建议在写入阶段就把计算结果一并写进去不要只写公式。如果必须由公式计算读完后自己用程序算一遍结果再覆盖写入别指望SDK替你算。这条是我踩得最狠的最初做报表导出时发现Excel里显示正常服务端读出来全是空排查半天才意识到公式根本没缓存。5.5 大文件写入越来越慢对象模型的开销比你想象的大现象用AppendChild逐行写几万行数据内存涨得飞快写入时间从几秒变成几分钟。原因SpreadsheetDocument默认走DOM式对象模型每创建一个Cell、Row对象都在托管堆上占内存行数大了频繁分配和GC性能自然崩。解决数据量超过一万行就别用SheetData.AppendChild改用第6章的OpenXmlWriter流式写入。它的写法和XML直接写节点类似不保留对象树内存占用基本稳定。数据量更大时可以考虑分段多次Save或者拆成多个Sheet分页写避免单表膨胀。6. 再上一个台阶用OpenXmlWriter批量写、用Validator做写后验证6.1 用OpenXmlWriter绕开对象模型大表写入的内存瓶颈解法OpenXmlWriter是OpenXml SDK里的流式写入器适用场景就是大批量生成xlsx。它不构建对象树而是把节点直接写进XML流内存占用从随行数线性增长变成基本恒定。我一般超过1万行就换这个方案using var doc SpreadsheetDocument.Create(path, SpreadsheetDocumentType.Workbook); var workbookPart doc.AddWorkbookPart(); workbookPart.Workbook new Workbook(new Sheets()); var wsPart workbookPart.AddNewPartWorksheetPart(); using (OpenXmlWriter writer OpenXmlWriter.Create(wsPart)) { writer.WriteStartElement(new Worksheet()); writer.WriteStartElement(new SheetData()); for (int r 1; r 50000; r) { writer.WriteStartElement(new Row { RowIndex (uint)r }); writer.WriteElement(new Cell { CellReference $A{r}, DataType CellValues.Number, CellValue new CellValue(r.ToString()) }); writer.WriteEndElement(); } writer.WriteEndElement(); writer.WriteEndElement(); }这个写法有两个必须记住的点一是WriteStartElement和WriteEndElement必须成对出现Worksheet套SheetDataSheetData套Row层级错了生成的文件打不开二是WriteElement只适合单个单元格这种叶子节点不能再往里套。循环里不要打印日志I/O会拖慢速度。6.2 写后校验OpenXmlValidator能查出哪些“等会儿Excel再修”的错写完后不要直接交付先用OpenXmlValidator过一遍。它能检查出很多只有Excel打开时才报的“文件损坏、需要修复”类问题比如节点顺序错误、非法属性、缺少必要子元素。using DocumentFormat.OpenXml.Validation; var validator new OpenXmlValidator(FileFormatVersions.Office2010); foreach (ValidationErrorInfo error in validator.Validate(wsPart)) { Console.WriteLine(${error.Path?.XPath}: {error.Description}); }如果这段代码输出了任何错误都说明文件结构不满足规范。我的习惯是写入逻辑每次改完都先跑校验再落盘。校验通过不代表Excel里视觉上好看但代表文件结构合法用户打开时不会先看到一个“是否尝试修复”的弹窗。6.3 把校验和写入接进批量流水线时我现在的固定习惯我现在处理服务端Excel读写固定流程是读场景先复制文件到临时目录再只读打开所有文本读取走共享字符串处理分支写场景先定义样式表再写数据超过1万行换OpenXmlWriter交付前用Validator校验每个WorksheetPart。最早做批量导出时我偷懒跳过了校验结果用户打开文件报损坏排查半天是Stylesheet里漏配了一个CellStyleFormats。自那以后校验一步再也不敢省。项目里如果还有富文本、合并单元格、冻结窗格的需求也建议在写好基础读写后一个一个加进样式表里并用Validator跑一遍再放行。这套路子看上去步骤多但真正上线后出问题的概率会低很多希望帮到你。本文还有配套的精品资源点击获取