简介:这份资源面向具备一定C#基础的开发者,聚焦于使用C#读取CSV与Excel文件这一常见数据处理需求,帮助解决表格数据导入、解析与界面展示中的实际问题。压缩包内共1个doc文档,约34KB,以文字讲解配合代码示例的形式呈现,便于边看边练。内容围绕OleDbConnection与OleDbDataAdapter两类核心对象展开,涵盖CSV连接字符串的构造、HDR与FMT等扩展属性的含义、通过Fill方法将数据填充至DataTable,以及借助DataGridView自动生成列并绑定数据源完成展示;同时对比了读取Excel时ACE OLEDB提供程序与Excel 8.0连接串的差异,并给出判断文件扩展名后分别处理CSV与Excel的分支思路。目前已有260人学习,适合需要快速掌握C#表格文件读取技巧、对照代码调试的读者参考。
1. 别再用 Excel 打开 CSV 了:C# 读取 CSV 与 Excel 的选型分水岭
很多做数据导入的同行都遇到过这种场景:运营丢过来一个 200MB 的 CSV,说“用 Excel 打开卡死了,你帮我看看”,结果你双击打开,Excel 转圈三分钟,最后只显示了 104 万行——后面的数据被静默截断了。更坑的是,有些字段里带逗号、带换行、带前导零,Excel 一保存,前导零全没了,身份证号变成科学计数法。这就是为什么在 C# 里读取 CSV 和 Excel,第一件事不是写代码,而是先分清你面对的到底是哪种文件、用哪种方式读。
这个标题“C#读取CSV EXCEL文件”背后其实藏着两条完全不同的技术路线:CSV 是纯文本,本质是字符流解析,核心难点在分隔符转义和编码;Excel(.xlsx)是 ZIP 压缩的 XML 包,本质是结构化文档解析,核心难点在依赖库选型和内存占用。选错了路线,轻则性能翻车,重则数据静默丢失。这篇文章面向的是需要把表格数据接进业务系统的 .NET 开发者,不管你是刚接触 File.ReadAllLines 的新手,还是已经在用 EPPlus、ClosedXML 的老手,下面这些选型理由、参数配置和踩坑记录,都是我一个个项目趟出来的。
2. CSV 读取:从 File.ReadAllLines 到 CsvHelper 的三种姿势
2.1 为什么 Split(',') 是最容易翻车的写法
新手最直觉的写法是读一行、按逗号切分:
// 危险写法:遇到带引号的字段直接崩 var lines = File.ReadAllLines("data.csv"); foreach (var line in lines) { var parts = line.Split(','); Console.WriteLine(parts[0]); // 字段里含逗号就错位 }这段代码在简单数据上能跑,但只要出现"张三, 李四",25这种带引号包裹的字段,Split 就会把它切成三段,后面所有列全部错位。CSV 的 RFC 4180 标准里,字段可以用双引号包裹,引号内的逗号、换行、双引号(用两个双引号转义)都不算分隔符。手写解析器要处理这些状态机逻辑,非常容易漏掉边界情况。我一般会直接上 CsvHelper,除非项目有严格的零依赖要求。
2.2 用 CsvHelper 做映射读取的最小可跑代码
先装包,然后在代码里定义映射类:
dotnet add package CsvHelperusing CsvHelper; using CsvHelper.Configuration; using System.Globalization; // 定义列映射,避免依赖列顺序 public class UserRecord { public int Id { get; set; } public string Name { get; set; } public DateTime BirthDate { get; set; } public decimal Salary { get; set; } } public sealed class UserRecordMap : ClassMap<UserRecord> { public UserRecordMap() { Map(m => m.Id).Name("用户编号"); Map(m => m.Name).Name("姓名"); Map(m => m.BirthDate).Name("出生日期").TypeConverterOption.Format("yyyy-MM-dd"); Map(m => m.Salary).Name("月薪"); } } // 读取入口 var config = new CsvConfiguration(CultureInfo.InvariantCulture) { HasHeaderRecord = true, MissingFieldFound = null, // 缺列不抛异常 BadDataFound = null, // 脏数据跳过而非中断 TrimOptions = TrimOptions.Trim, // 自动去首尾空格 DetectDelimiter = true // 自动嗅探分隔符 }; using var reader = new StreamReader("users.csv", Encoding.UTF8); using var csv = new CsvReader(reader, config); csv.Context.RegisterClassMap<UserRecordMap>(); var records = csv.GetRecords<UserRecord>().ToList();逻辑说明:ClassMap把中文表头映射到属性,列顺序变了也不影响;TypeConverterOption.Format指定日期格式,避免不同区域设置下解析失败。参数上,MissingFieldFound = null让缺列时返回默认值而不是抛异常,适合上游表结构不稳定的场景;BadDataFound = null会跳过格式错误的行,但要注意这会静默丢数据,生产环境建议改成记录日志。DetectDelimiter = true会多读几行做嗅探,大文件下有一点点开销,如果确定是逗号可以关掉。
2.3 编码、分隔符、大文件流式读取的三个关键参数
CSV 最常见的翻车是中文乱码。国内很多系统导出的 CSV 是 GBK 编码,用 UTF-8 读会变成问号。判断方法:用十六进制编辑器看文件头有没有 BOM(EF BB BF 是 UTF-8 BOM)。没有 BOM 又不能确定编码时,可以注册编码提供程序:
// .NET Core 默认不支持 GBK,需要注册 Encoding.RegisterProvider(CodePagesEncodingProvider.Instance); using var reader = new StreamReader("gbk_data.csv", Encoding.GetEncoding("GBK"));分隔符方面,欧洲区域常用分号,有些系统用制表符。DetectDelimiter能自动处理,但如果你知道确切分隔符,直接指定Delimiter = ";"更快更稳。
大文件必须流式读,不要ToList()一次性加载。CsvHelper 的GetRecords<T>()本身就是惰性迭代,配合foreach逐行处理,内存占用恒定:
foreach (var record in csv.GetRecords<UserRecord>()) { // 逐行入库或处理,内存不随文件增长 ProcessRecord(record); }注意:
GetRecords返回的是迭代器,如果在迭代过程中又去操作同一个 CsvReader 实例,会抛异常。需要嵌套读取时,用两个独立的 reader。
3. Excel 读取:EPPlus、ClosedXML、NPOI 怎么选
3.1 .xlsx 和 .xls 的底层差异决定了库的选型
.xlsx 是 Office Open XML 格式,本质是一个 ZIP 包,里面是一堆 XML 文件(sheet1.xml、sharedStrings.xml 等)。.xls 是老的二进制 BIFF 格式,结构完全不同。这个差异直接决定了库的选型:EPPlus 和 ClosedXML 只支持 .xlsx,NPOI 两者都支持。如果你的上游还在产出 .xls,那基本只能选 NPOI。如果确定都是 .xlsx,EPPlus 的 API 更现代,ClosedXML 更轻量。
另一个关键点是许可证。EPPlus 从 5.0 开始改成商业许可,非商业用途免费但商业项目需要购买。ClosedXML 和 NPOI 都是 MIT 许可,商用无压力。我一般在新项目里优先 ClosedXML,除非需要 EPPlus 的高级图表功能。
3.2 用 ClosedXML 读取带公式和合并单元格的表
dotnet add package ClosedXMLusing ClosedXML.Excel; using var workbook = new XLWorkbook("report.xlsx"); var sheet = workbook.Worksheet(1); // 第一个工作表 // 读取已使用区域,避免遍历整个 104 万行 var range = sheet.RangeUsed(); foreach (var row in range.Rows()) { // 合并单元格只有左上角有值,其余为空白 var cellValue = row.Cell(1).GetString(); var numberValue = row.Cell(2).GetDouble(); // 公式单元格取计算值,需要 Excel 打开过才有缓存值 var formulaCell = row.Cell(3); var cachedValue = formulaCell.CachedValue; Console.WriteLine($"{cellValue} | {numberValue} | {cachedValue}"); }逻辑说明:RangeUsed()返回实际有数据的区域,比RowsUsed()更精确,能避开格式残留导致的空行。合并单元格是 Excel 读取的经典坑:一个跨 3 行的合并单元格,只有第一行有值,后两行读出来是空。处理办法是记录上一个非空值做填充,或者用sheet.MergedRanges先拿到合并区域再逐格赋值。公式单元格的CachedValue只有在文件被 Excel 保存过、缓存了计算结果时才有效,程序生成的 xlsx 往往没有缓存值,这时只能读到公式字符串。
3.3 用 NPOI 兼容 .xls 并控制内存的配置项
dotnet add package NPOIusing NPOI.SS.UserModel; using NPOI.XSSF.UserModel; // .xlsx using NPOI.HSSF.UserModel; // .xls IWorkbook workbook; using (var fs = new FileStream("data.xls", FileMode.Open, FileAccess.Read)) { // 根据扩展名或文件头选择实现 workbook = new HSSFWorkbook(fs); // .xls 用 HSSF } var sheet = workbook.GetSheetAt(0); var rowEnumerator = sheet.GetRowEnumerator(); while (rowEnumerator.MoveNext()) { var row = (IRow)rowEnumerator.Current; if (row == null) continue; var cell = row.GetCell(0); // 按单元格类型取值,避免 InvalidOperationException var value = cell?.CellType switch { CellType.String => cell.StringCellValue, CellType.Numeric => cell.NumericCellValue.ToString(), CellType.Boolean => cell.BooleanCellValue.ToString(), _ => string.Empty }; }逻辑说明:NPOI 的GetCell可能返回 null,直接取StringCellValue会抛异常,必须先判类型。CellType.Numeric下日期也是数字,需要用DateUtil.IsCellDateFormatted(cell)判断后再转日期。内存控制上,NPOI 提供SXSSFWorkbook用于写大文件,但读大文件没有流式 API,整个 workbook 会加载进内存。一个 50MB 的 xlsx 解压后可能占几百 MB 内存,读超大 Excel 建议先用工具转成 CSV 再走 CSV 流程。
4. 避坑与排查:读取表格文件时最常见的五类翻车
4.1 中文乱码:现象是问号或方块,根因在编码声明
现象:CSV 里的中文读出来是????或锟斤拷。原因:文件实际是 GBK 编码,但 StreamReader 默认用 UTF-8 解码。解决:先检测 BOM,无 BOM 时用Encoding.GetEncoding("GBK"),并记得注册CodePagesEncodingProvider。Excel 文件一般不存在这个问题,因为 xlsx 内部统一是 UTF-8 XML。
4.2 日期变成数字:Excel 的日期本质是序列号
现象:读 Excel 日期列,拿到的是45000这种数字。原因:Excel 内部用 1900-01-01 起的序列号存日期,格式化只是显示层。解决:用DateUtil.IsCellDateFormatted(cell)判断,是日期则调cell.DateCellValue。ClosedXML 里用cell.GetDateTime()更省事。注意 1900 年闰年 bug:Excel 认为 1900 是闰年,序列号 60 对应不存在的 1900-02-29,处理 1900 年前后的数据要留意。
4.3 大文件 OOM:一次性加载是内存杀手
现象:读 100MB 的 xlsx 时进程内存飙到 2GB 然后 OutOfMemoryException。原因:EPPlus、ClosedXML、NPOI 都会把整个工作簿加载进内存。解决:CSV 用流式迭代;Excel 如果超过 20MB,先用后台任务转 CSV,或者用ExcelDataReader这个库,它支持只读流式读取,内存占用低很多。
4.4 前导零丢失:字符串被自动转成数字
现象:工号00123读出来变成123。原因:CSV 里没加引号,解析器按数字处理;Excel 里单元格格式是常规,也会丢零。解决:CSV 侧在映射里把该列声明为 string;Excel 侧读cell.ToString()而不是NumericCellValue,或者读之前把列格式设成文本。
4.5 空行与尾随分隔符:多出莫名其妙的空记录
现象:CSV 末尾多出一行全空记录,或者 Excel 的RangeUsed包含大量空行。原因:文件末尾有换行符,或者格式残留让 Excel 认为区域很大。解决:CSV 配置里加IgnoreBlankLines = true;Excel 侧遍历时判断row.IsEmpty()跳过,或者用sheet.RowsUsed(r => r.CellsUsed().Any())过滤。
5. 把读取封装成可复用的导入管道:一个具体技巧
前面讲的都是单点读取,实际项目里你需要的是一个能根据文件类型自动分流的导入管道。我一般会写一个TabularReader工厂,输入文件路径,输出IEnumerable<Dictionary<string, object>>,上层业务不用关心底层是 CSV 还是 Excel。
public static class TabularReader { public static IEnumerable<Dictionary<string, object>> Read(string path) { var ext = Path.GetExtension(path).ToLowerInvariant(); return ext switch { ".csv" => ReadCsv(path), ".xlsx" => ReadXlsx(path), ".xls" => ReadXls(path), _ => throw new NotSupportedException($"不支持的文件类型: {ext}") }; } private static IEnumerable<Dictionary<string, object>> ReadCsv(string path) { using var reader = new StreamReader(path, DetectEncoding(path)); using var csv = new CsvReader(reader, new CsvConfiguration(CultureInfo.InvariantCulture) { HasHeaderRecord = true, IgnoreBlankLines = true, MissingFieldFound = null }); if (!csv.Read()) yield break; csv.ReadHeader(); var headers = csv.HeaderRecord; while (csv.Read()) { var row = new Dictionary<string, object>(); for (int i = 0; i < headers.Length; i++) row[headers[i]] = csv.GetField(i); yield return row; } } private static Encoding DetectEncoding(string path) { using var fs = File.OpenRead(path); Span<byte> bom = stackalloc byte[3]; var read = fs.Read(bom); if (read >= 3 && bom[0] == 0xEF && bom[1] == 0xBB && bom[2] == 0xBF) return Encoding.UTF8; // 无 BOM 时按 GBK 兜底,国内 CSV 常见 Encoding.RegisterProvider(CodePagesEncodingProvider.Instance); return Encoding.GetEncoding("GBK"); } }这个管道的核心价值在于:编码检测前置、空行过滤、缺列容错都收在一处,业务层拿到的永远是干净的字典序列。yield return保证流式,不会因为封装就退化成一次性加载。
验证方法上,我习惯准备三个测试文件:一个带 BOM 的 UTF-8 CSV、一个无 BOM 的 GBK CSV、一个含合并单元格和公式的 xlsx。每次改完读取逻辑,先跑这三个文件,看中文、日期、前导零、空行四个指标是否正常。这个习惯帮我拦下过好几次“本地测试没问题、上线就乱码”的事故。
最后说个血泪教训:永远不要相信上游说的“格式很规范”。我见过一个 CSV,前 1000 行是逗号分隔,第 1001 行开始变成制表符,原因是导出程序中途换了配置。所以DetectDelimiter这类防御性配置,宁可多花一点性能也要开着。读取表格文件这件事,写代码只占三成,剩下七成都在跟各种不规范的数据格式斗智斗勇。希望帮到你。
本文还有配套的精品资源,点击获取