
简介这份C#读取CSV与Excel文件的示例文档面向具备C#基础、正在使用WinForms处理表格数据的开发者重点解决读取CSV和Excel时连接字符串配置、数据填充与界面展示等常见问题。压缩包仅含1个doc文件大小约34KB内容集中便于快速查阅和本地验证。文档围绕OLE DB连接与适配器、DataTable展开涵盖CSV与Excel不同提供程序的选择、HDR、IMEX、FMT等扩展属性设置、按扩展名分支处理、SQL查询语句编写以及DataGridView自动生成列和数据源绑定并给出可直接参考的完整代码片段。示例中还通过路径判断分别处理CSV和XLS注释提示有列名与无列名场景读者可据此调整字段名、分隔符、工作表名和文件路径减少反复试错。已有260人学习适合需要快速搭建读取流程、理解OLE DB读取差异或排查表格数据显示问题的初、中级开发者参考。1. 一份 30 行的 OleDb 示例暴露了 C# 读取 CSV EXCEL 的四个边界C# 读取 CSV、EXCEL 文件最省事的写法就是挂一个 OleDbConnection把整个文件当成一张表来 select。手头这份示例代码走的就是这条路先用EndsWith(CSV)判断扩展名再分别拼出文本驱动和工作簿驱动的连接串OleDbDataAdapter.Fill灌进 DataTable最后直接绑到 DataGridView 上。三十行跑通演示看着很舒服。可一旦放进真实项目第三天就开始翻车本地读得干干净净的表换台机器整列变成 DBNullxlsx 还没打开就抛“未在本地计算机上注册提供程序”编号列里001变成1日期列变成一串 45000 多的数字。这份资源的定位很清楚——它是一条“零依赖读表”的路线不引第三方库只用系统里的 OLE DB 提供程序把 CSV 和 Excel 统一抽象成 SQL 表来查。适合维护老 WinForms 系统、需要给客户机器快速做个导入预览、又不想为读两张表引入一堆 NuGet 包的场景。往下读之前先接受一个前提OleDb 读表能跑通不难难的是把它在别人机器上跑通难的是让列类型别被猜错。2. 连接字符串逐段拆Provider、Data Source、Extended Properties 怎么填示例代码里两段连接串看上去只是换了个字符串实际上每一段都对应一个会出问题的开关。这一章把三段拆开讲每段配一个能直接抄的参数改法先把“为什么这么写”立住后面排错才有依据。2.1 Provider 的选择Jet 4.0 与 ACE 不是替代关系Microsoft.Jet.OLEDB.4.0是随老系统一路带过来的文本/工作簿驱动它只认.xls和分隔文本不认.xlsx而且只有 32 位版本。这一点非常关键只要你的进程编译成 AnyCPU 并跑在 64 位运行时上加载 Jet 就会抛“未在本地计算机上注册”的异常代码一个字没错环境直接判死刑。Microsoft.ACE.OLEDB.12.0以及更新版本的 ACE 提供程序同时提供 32 位和 64 位版本能读.xls、.xlsx和分隔文本。代价是它不一定在目标机器上存在需要装一个可再发行组件包而且装 32 位还是 64 位必须和你的进程位数对上两个版本不能共存于同一个位数下随意混用。我一般的选法是新写的导入功能一律用 ACE进程位数在项目属性里明确固定下来别留 AnyCPU只有在维护十几年前的老系统、客户机器上压根不允许装任何组件时才退回 Jet并且强制把目标平台设成 x86。这条经验值不少血泪——最典型的翻车是开发机装过完整办公套件所以两个引擎都有测试和生产是干净的服务器一跑就炸。2.2 Data Source 的语义CSV 指目录Excel 指文件这是示例代码里最容易看漏的一处。读 CSV 时Data Source 填的是目录不是文件本身文件名是当作表名出现在 SQL 里的。原代码里那句取目录的写法是对的int ipos csvDir.LastIndexOf(\\); string filePath csvDir.Substring(0, ipos 1); // 截出目录末尾带反斜杠 string connStr ProviderMicrosoft.Jet.OLEDB.4.0;Data Source filePath ;Extended Propertiestext;HDRYES;FMTDelimited;;但下面这句拼 SQL 就有隐患了string commandText select * from csvDir.Replace(filePath, ); // 得到的可能是 test1.csvReplace是按字符串全量替换的如果文件名片段在目录路径里也出现过会被一起替换掉SQL 直接变形。正确做法是用Path.GetFileName取文件名再用方括号包起来下面第 3 章会讲为什么必须有方括号string fileName Path.GetFileName(csvDir); string commandText select * from [ fileName ];读 Excel 时语义反过来Data Source 就是完整的文件路径表名是工作表名加一个$例如[test1$]。示例里写死select * from [test1$]只能读那张叫 test1 的表换个工作簿就废这是演示代码和工程代码的分水岭。2.3 Extended Properties 三个开关HDR、FMT、IMEX这一串用单引号包起来的分号参数是 OleDb 读表的核心配置也是最容易被随手复制导致行为不对的地方。HDR决定首行是不是列名。HDRYES时首行被当作字段名从第二行开始出数据HDRNO时驱动自己生成F1、F2、F3这样的列名首行也当数据返回。示例代码里注释掉的那几行dataGridView2.Columns[0].HeaderText 编号就是配合HDRNO用的——没有列名只能手工改表头。FMT只对文本驱动有效常见取值是Delimited按分隔符、CSVDelimited按逗号、TabDelimited按制表符。特别提醒一句玄学点FMTDelimited用的是系统区域设置里的列表分隔符在某些区域配置下它不是逗号。所以只要你的 CSV 结构是固定的宁可在 schema.ini 里显式声明也别指望Delimited猜对这块后面单独讲。IMEX是混合类型开关取值 0/1/2。读文件场景基本只用IMEX1含义是“把含有混合类型数字、日期、字符串混在一列的列整体当文本读”。不加这个参数驱动会按采样结果给整列定一个类型一旦某列前几行是数字、后面冒出个N/A那一格就会变成 DBNull看着像数据丢了实际上是类型转换失败被吞了。// Excel 场景的典型写法xlsx 用 Excel 12.0 Xmlxls 用 Excel 8.0 string connStr ProviderMicrosoft.ACE.OLEDB.12.0;Data Source filePath ;Extended PropertiesExcel 12.0 Xml;HDRYES;IMEX1;;注意Extended Properties内部的引号用单引号分号后面不要随手加空格某些驱动版本对多余空格很敏感会报“无法识别的扩展属性”。2.4 一个能跑的最小骨架把上面的结论收拢下面这段是去掉界面耦合、可以直接塞进工具类的版本两种格式统一入口using System.Data; using System.Data.OleDb; using System.IO; public static DataTable ReadTable(string path) { string ext Path.GetExtension(path).ToLowerInvariant(); string connStr; string sql; if (ext .csv || ext .txt || ext .tab) { // 文本驱动Data Source 是目录表名是带扩展名的文件名 string dir Path.GetDirectoryName(Path.GetFullPath(path)); string file Path.GetFileName(path); connStr ProviderMicrosoft.ACE.OLEDB.12.0;Data Source dir ;Extended Propertiestext;HDRYES;FMTCSVDelimited;IMEX1;; sql select * from [ file ]; } else if (ext .xls) { connStr ProviderMicrosoft.ACE.OLEDB.12.0;Data Source path ;Extended PropertiesExcel 8.0;HDRYES;IMEX1;; sql select * from [Sheet1$]; } else { connStr ProviderMicrosoft.ACE.OLEDB.12.0;Data Source path ;Extended PropertiesExcel 12.0 Xml;HDRYES;IMEX1;; sql select * from [Sheet1$]; } using (var conn new OleDbConnection(connStr)) using (var adapter new OleDbDataAdapter(sql, conn)) { var dt new DataTable(); adapter.Fill(dt); return dt; } }逐项说明Path.GetFullPath是必要的相对路径在GetDirectoryName时可能返回空串连接串会变成Data Source;直接报错using包住连接和适配器避免文件句柄没释放导致 Excel 里打不开这个文件adapter.Fill一次把结果灌进 DataTable行数超十万时会明显吃内存这条路线不适合超大文件后面第 6 章给替代方案。3. CSV 落地schema.ini 与目录级连接的两处硬约束CSV 这条线看起来最简单实际上问题密度最高。原因在于文本驱动没有任何类型元数据可依赖一切靠猜猜分隔符、猜编码、猜列类型、猜有没有表头。想让它别猜就得把答案写进 schema.ini。3.1 表名带点必须加方括号先把示例代码里那个最容易复现的错误说清楚。select * from test1.csv会抛语法错误因为 SQL 解析器把.当成了库名和表名的分隔符。文本驱动的表名就是文件全名带扩展名所以必须包上方括号-- 错误写法解析器会认为 test1 是库、csv 是表 select * from test1.csv -- 正确写法 select * from [test1.csv]同理文件名里带空格、带中文、带括号时方括号也必须加。这一点和 Excel 的表名规则一致统一处理就行。3.2 FMTDelimited 的玄学与显式声明的价值前面提过FMTDelimited依赖系统区域设置里的列表分隔符。我在一台欧洲区域配置的机器上遇到过 CSV 用分号分隔、代码里写Delimited后整行被当成一个字段返回的情况排查了半小时才定位到区域设置。工程上的做法有两个一是把FMT写死成CSVDelimited逗号或TabDelimited制表符适用于格式确定的下游系统导出文件。二是完全不用 FMT 猜改用 schema.ini 显式声明FormatCSVDelimited或FormatDelimited(;)把分隔符钉死。; schema.ini必须放在 CSV 所在目录下文件名固定 [order_2024.csv] FormatDelimited(;) ColNameHeaderTrue CharacterSet936 MaxScanRows0 DateTimeFormatyyyy-MM-dd Col1OrderNo Text Width 32 Col2CustomerName Text Width 64 Col3Amount Double Col4CreatedOn DateTime这份 ini 的每一项都有明确用途。[order_2024.csv]段名必须和文件名完全一致包括大小写和扩展名对不上就等于没写。FormatDelimited(;)指定分号作为分隔符也可以写CSVDelimited表示逗号。CharacterSet936对应 GBK65001对应 UTF-8这一项决定了中文字段会不会变乱码。MaxScanRows0表示全表扫描来推断类型默认只扫前 8 行这是“整列变 null”的头号元凶。Col1、Col2这些显式列定义一旦写了驱动就完全按你声明的类型走不再采样猜测Text Width n的宽度也要给够超宽会被截断。需要注意schema.ini 只对文本驱动生效而且必须与 CSV 同目录。很多项目是把上传的文件落到临时目录再读那就得把 schema.ini 一起复制过去或者在临时目录里动态生成一份。3.3 编码问题BOM、GBK 和首列名污染CharacterSet不写时会用系统默认编码中文环境下通常是 GBK于是 UTF-8 的 CSV 读出来中文全是问号。反过来把CharacterSet65001写在 GBK 文件上也一样炸。可行的判断方式是读文件头几个字节做 BOM 检测没有 BOM 再按启发式判断把前几 KB 按 UTF-8 严格解码抛异常就退回 GBK。static int DetectCodePage(string path) { byte[] head new byte[4]; using (var fs File.OpenRead(path)) fs.Read(head, 0, 4); if (head[0] 0xEF head[1] 0xBB head[2] 0xBF) return 65001; // UTF-8 BOM if (head[0] 0xFF head[1] 0xFE) return 1200; // UTF-16 LE return 936; // 兜底 GBK }带 BOM 的 UTF-8 还有个副作用HDRYES时首列列名会带上一个不可见的 BOM 字符界面上看不出来代码里按列名取值却取不到。处理办法是在拿到 DataTable 之后统一清洗列名把\uFEFF和首尾空白一起去掉这一条后面第 4 章给具体写法。3.4 目录级连接的副作用Data Source 指向目录这件事意味着同目录下所有同扩展名文件都在这个连接的可见范围内。这带来两个实际问题一是权限如果目录是共享目录或者用户临时目录可能读到不该读的文件二是误读select * from [a.csv]写错文件名时某些驱动版本不会报错而是返回空表排查时容易误判成文件没数据。我的习惯是读取前先File.Exists校验一次读完立刻释放连接临时文件用完就删。另外文本驱动识别的扩展名是有限集合.csv、.txt、.tab等把.csv改名成.dat再读会直接报“找不到可安装的 ISAM”这个报错跟引擎没关系纯粹是扩展名不在白名单里很容易误判成环境问题。4. Excel 落地工作表名、列名清洗与类型推断Excel 这条线的难点不在连接串在于工作簿的结构是用户随手画的表名带空格、首行是标题合并单元格、中间夹杂空行、同一列上半截是数字下半截是文字。OleDb 把这些统统当成一张“表”你得自己去适应。4.1 表名即工作表名$不能少Excel 驱动的表名规则是[工作表名$]末尾的美元符号必须有漏了就报“找不到表”。工作表名带空格或中文时同样用方括号包住string sheetName 订单明细; string sql select * from [ sheetName $];示例代码里select * from [test1$]就是把工作表名写死了。实际项目里工作表名往往由用户决定比较稳的做法是先查架构信息拿到真实表名再拼 SQL。4.2 先读架构再拼 SQLOleDbConnection.GetOleDbSchemaTable能列出工作簿里所有“表”过滤掉系统表和命名区域剩下的就是以$结尾的工作表public static Liststring GetSheetNames(string excelPath) { var result new Liststring(); string connStr ProviderMicrosoft.ACE.OLEDB.12.0;Data Source excelPath ;Extended PropertiesExcel 12.0 Xml;HDRYES;IMEX1;; using (var conn new OleDbConnection(connStr)) { conn.Open(); DataTable schema conn.GetOleDbSchemaTable(OleDbSchemaGuid.Tables, null); foreach (DataRow row in schema.Rows) { string name row[TABLE_NAME].ToString(); // 只保留工作表排除命名区域和系统表 if (name.EndsWith($) !name.StartsWith(_xlnm)) result.Add(name.TrimEnd($)); } } return result; }OleDbSchemaGuid.Tables返回的 TABLE_NAME 已经带了$拼 SQL 时要补回方括号所以取名字时先TrimEnd($)再统一拼接避免出现[[Sheet1$]$]这种畸形写法。命名区域在架构里也会出现但它不以$结尾被上面的条件过滤掉了。这里有个边界工作表名本身可能包含$之外的怪字符比如单引号。拼接 SQL 时如果表名来自用户输入务必做一次白名单校验只允许字母、数字、中文、下划线和空格别把用户输入直接拼进 SQL 字符串。4.3 列名清洗重复列、空列名和 BOMExcel 驱动对列名的处理有一堆默认行为全都得在拿到 DataTable 之后兜住。第一行有两列都叫“金额”时驱动会给第二列自动改名成F2之类的占位名某列首行为空时同样落到F1、F2首列带 BOM 时列名前面多一个不可见字符。这些列名直接绑到界面上就是一片混乱。static void NormalizeColumns(DataTable dt) { var used new HashSetstring(StringComparer.OrdinalIgnoreCase); for (int i 0; i dt.Columns.Count; i) { DataColumn col dt.Columns[i]; // 去掉 BOM、首尾空白、全角空格 string name col.ColumnName.Replace(\uFEFF, ).Trim().Trim(\u3000); if (string.IsNullOrEmpty(name)) name 列 (i 1); string final name; int suffix 2; while (!used.Add(final)) // 重名时追加序号 final name _ suffix; col.ColumnName final; } }这段逻辑看着琐碎但不做的话后面按列名取值的代码会在某些文件上静默取到 null。清洗放在Fill之后、绑定界面之前是一步不能省的固定动作。4.4 类型推断8 行采样与前导零Excel 驱动推断列类型只扫描前若干行默认 8 行由引擎的 TypeGuessRows 决定。一列前 8 行都是数字、第 9 行开始出现未知这种文本结果就是前 8 行正常、后面的行整列变 null。加了IMEX1能把混合列按文本读但它同样受采样行数限制——采样时没看到混合就仍然按数字处理。比类型推断更烦的是格式丢失。001这种编号列被推断成数字后变成12024-01-05被推断成日期后变成 OADate 数字长的身份证号变成科学计数法。根治办法是在读取时就不让它推断给需要原样保留的列在 SQL 里显式转换或者干脆在源头上用 schema 方式约束。Excel 没有 schema.ini能用的手段是把查询写成select Format(编号,000) as 编号, ...这类表达式但格式函数在不同引擎上兼容性一般。我的常规做法是两步一是IMEX1常态化打开把绝大多数混合列按文本读回来二是在 DataTable 上做一次“回读校验”对已知的编号、证件号、日期列按字符串重新格式化把前导零和日期格式补回去。这属于应用层的兜底比和驱动较劲省事得多。4.5 绑定到 DataGridView 的两个细节示例代码里用的是dataGridView1.DataSource dt.DefaultView而不是直接赋dt。DefaultView 支持排序和筛选界面上的列头点击排序才不会抛异常如果后续要做过滤也是在这个视图上设RowFilter。dataGridView1.AutoGenerateColumns true; dataGridView1.DataSource dt.DefaultView; dataGridView1.AllowUserToAddRows false; // 只读预览时关掉末尾空行HDRNO的场景要额外处理列名是F1、F2得手工改名并且关掉排序因为文本列排序规则和数值列不一样用户点了会看到奇怪的顺序。示例代码里注释掉的SortMode DataGridViewColumnSortMode.NotSortable就是干这个的只不过它写在了dataGridView2上——复制粘贴时控件名没改这是这类示例代码最常见的低级错误照着抄的时候一定逐个核对控件名。5. 避坑排查五个真实翻车现场与对应改法这一章按“现象 → 原因 → 解决”记五条都是我在这条路线上实打实踩过的覆盖环境和数据两类。遇到报错先在这里对一遍能省掉大半排查时间。5.1 环境与引擎类故障现象一本地跑得好好的换台机器就抛“未在本地计算机上注册 Microsoft.Jet.OLEDB.4.0 提供程序”。原因基本是两个进程是 64 位而 Jet 只有 32 位或者目标机器上压根没装对应的引擎。开发机往往装过完整办公套件两个引擎都有所以永远复现不出来。解决分两步走。先把项目目标平台从 AnyCPU 改成明确的 x86 或 x64别让它随运行时漂移再把 Provider 换成 ACE并在部署文档里写清楚需要安装对应位数的可再发行组件。如果客户环境不允许装组件就固定 x86 加 Jet并且确认系统里那个老引擎还在。现象二xlsx 文件报“外部表不是预期的格式”。原因是拿 Jet 或Excel 8.0去读 xlsx。Jet 只认老的二进制工作簿格式扩展属性写Excel 8.0时也只能读 xls。解决是按扩展名分流.xls用Excel 8.0.xlsx用Excel 12.0 Xml两者都走 ACE。不要图省事统一写Excel 12.0某些版本的驱动在读老 xls 时会有兼容性抖动。5.2 数据解析类故障现象三CSV 里某一列大部分是数字夹杂几个N/A读出来整列大量 DBNull。原因是文本驱动按前 8 行采样定类型采样窗口里全是数字就定成数值列后面遇到非数字转换失败直接给 DBNull不报错。解决办法是双管齐下连接串加IMEX1同时在 schema.ini 里把MaxScanRows0打开并显式声明该列类型为Text。IMEX1单独用并不保险它受采样行数约束只有显式列定义才是确定性的。现象四编号列前导零丢失001读成1日期列变成45296这样的整数。原因同上类型被推断成数值或日期格式信息在转换过程中丢了。解决办法是让这些列以文本读回来。CSV 在 schema.ini 里声明Col1Code Text Width 32Excel 没有 ini 可用就在读取后做一次格式化回填把int值按D3补零把 OADate 用DateTime.FromOADate转回日期再按业务格式输出。现象五字段里有逗号读出来列整体右移最后一列全是 null。原因是 CSV 字段里的逗号被当成分隔符而字段本身没有用引号包裹。文本驱动支持引号包裹但如果源文件生成时就没加引号任何驱动都救不了。解决办法是先判断数据质量抽样看几行确认字段里是否存在裸逗号。存在就用TextFieldParser这类按引号规则解析的读取器替代 OleDb或者要求上游系统导出时统一加引号。别指望在 OleDb 层做兜底那是解析器该干的事。6. 封装与验证把读表逻辑收进一个可复用导入器上面几章的内容最终要收敛成一个入口否则每个窗体里都复制一遍连接串改一个参数要动十个文件。我的做法是抽一个静态类对外只暴露ReadTable(path)和ReadSheet(path, sheetName)两个方法内部负责扩展名分流、编码检测、列名清洗和异常包装。异常包装这一步别省把OleDbException原样抛给界面用户看到的是一串英文加错误码包装成“文件格式不支持或缺少驱动请确认文件未被占用”这类可读信息排障效率差好几倍。方案适用文件依赖主要限制OleDb Jetcsv、xls系统自带仅 32 位不支持 xlsx新系统可能已移除OleDb ACEcsv、xls、xlsx需装可再发行组件位数必须与进程一致TextFieldParsercsv、txt.NET 自带不支持 Excel需自己处理类型开源 CSV 库csvNuGet 包只解决 CSVExcel 仍需另找方案验证环节我固定跑三步。第一步对行数DataTable.Rows.Count与文件实际数据行数比对差一行通常是表头或空行被吃掉了。第二步抽样比对随机取三行把关键字段和用记事本打开看到的值逐字比重点看前导零、日期、负数和小数位。第三步是异常路径传一个不存在的文件、一个被 Excel 占用的文件、一个空文件确认包装后的提示能说清问题。最后一句话是血泪经验凡是涉及外部文件的导入功能我从不相信“我这边能读就行”。每次提交前强制在 32 位和 64 位两个进程、两个不同区域设置的机器上各跑一遍把 schema.ini 一起打进去把引擎依赖写进部署清单。这个习惯是从一次上线当天客户机器缺组件、四十多个人干等着导入数据的那次事故之后养成的。希望帮到你。本文还有配套的精品资源点击获取