简介:这份资源面向具备一定C#基础的开发者,聚焦于用C#读取CSV与Excel两类表格文件的常见实现方式,帮助解决数据导入、表格展示等实际开发中的读取难题。资源包内含1个doc文档,压缩包约34KB,以文字讲解配合代码片段的形式,梳理了OleDbConnection连接字符串的配置要点、OleDbDataAdapter填充DataTable的流程,以及通过DataGridView绑定数据源展示结果的思路,并区分了CSV与Excel在Provider和Extended Properties上的差异。内容还涉及HDR、FMT、IMEX等参数的含义,便于读者理解列名识别与混合数据类型处理。目前已有260人学习,适合需要快速上手表格文件读取、对照代码排查连接与查询问题的初中级开发者参考。
1. 一个 WinForm 小工具背后的真实需求:C# 读取 CSV 与 Excel 到底难在哪
上周有个做设备数据采集的朋友丢给我一个压缩包,说里面就一个CSVORXLS.cs,问能不能直接塞进产线看板里用。我打开一看,典型的 WinForm 单窗体程序:一个DataGridView,一段OleDbConnection连接字符串,CSV 和 Excel 走两个分支。代码不长,但把 C# 读取 CSV EXCEL 文件这件事里最容易翻车的几个点全踩了一遍——路径截取、扩展名判断、HDR/IMEX 参数、Sheet 名硬编码。这份资源的价值不在于代码多优雅,而在于它是一份能跑起来的“最小可复现样本”:你把它拖进 Visual Studio,改两行路径就能看到数据进表格,然后顺着它的坑一个个填。适合谁?手上有零散 CSV/Excel 报表、需要快速做个导入预览界面的 .NET 桌面开发者,尤其是还在维护老 WinForm 项目、机器上装着 Office 的那批人。下面我按“先跑通、再拆参数、最后排雷”的顺序,把这份源码包拆给你看。
2. 把源码跑起来:OleDb 连接字符串与 DataTable 填充的完整链路
2.1 先看清这份代码的骨架
这份CSVORXLS窗体在构造函数里就把活全干了:判断路径后缀是 CSV 还是其他,然后分别拼连接字符串、开连接、用OleDbDataAdapter.Fill灌进DataTable,最后绑到DataGridView。它没有分层、没有异常处理、没有资源释放,但链路是完整的——这对复现来说反而是好事,因为你能一眼看到数据从磁盘到控件的每一步。我一般会先把它原样跑通,确认环境没问题,再动手改。
复现前先确认三件事:项目目标框架选 .NET Framework(4.x 即可,别用 .NET Core,System.Data.OleDb在 Core 上要额外装包且行为有差异);机器上装了 Microsoft Office 或者至少装了 Access Database Engine;DataGridView控件从工具箱拖一个到窗体上,命名保持默认dataGridView1和dataGridView2,否则代码里的引用对不上。
2.2 CSV 分支:连接字符串到底在说什么
CSV 分支的核心是这一行:
// CSV 走 text 驱动,Data Source 必须是目录而不是文件 string connStr = "Provider=Microsoft.Jet.OLEDB.4.0;Data Source=" + filePath + ";Extended Properties='text;HDR=YES;FMT=Delimited;'";这里有个反直觉的点:读 CSV 时Data Source填的是文件所在目录,不是文件本身。所以原代码先用LastIndexOf("\\")截出目录,再用csvDir.Replace(filePath, "")把文件名抠出来当表名。Extended Properties里三个参数各管一摊:HDR=YES表示首行当列名,FMT=Delimited表示按分隔符解析(默认逗号,制表符或竖线要在同目录放一个schema.ini声明)。SQL 语句就是select * from 文件名,注意表名不带扩展名、不带路径。
// 表名 = 文件名去掉目录部分,OleDb 把 CSV 当一张表看 string commandText = "select * from " + csvDir.Replace(filePath, ""); OleDbConnection olconn = new OleDbConnection(connStr); olconn.Open(); OleDbDataAdapter odp = new OleDbDataAdapter(commandText, olconn); DataTable dt = new DataTable(); odp.Fill(dt); dataGridView1.AutoGenerateColumns = true; dataGridView1.DataSource = dt.DefaultView;AutoGenerateColumns = true让控件按DataTable的列自动建列,省得手写列定义;dt.DefaultView而不是dt,是为了后续能直接加排序和筛选。这段跑通后你会看到 CSV 内容整齐进表,但先别高兴——中文列名、空值、混合类型这三关在后面等着。
2.3 Excel 分支:Sheet 名是硬编码的重灾区
Excel 分支换了个驱动和扩展属性:
// Excel 走 Excel 驱动,Data Source 是文件本身,Sheet 名写在 SQL 里 string connStr = "Provider=Microsoft.Jet.OLEDB.4.0;Data Source=" + csvDir + ";Extended Properties='Excel 8.0;HDR=YES;IMEX=1;';"; string commandText = "select * from [test1$]"; // test1 是 Sheet 名,$ 不能少 OleDbConnection olconn = new OleDbConnection(connStr); olconn.Open(); OleDbDataAdapter odp = new OleDbDataAdapter(commandText, olconn); DataTable dt = new DataTable(); odp.Fill(dt); dataGridView2.DataSource = dt.DefaultView;[test1$]里的test1是工作表名,$是 OleDb 识别 Sheet 的固定后缀,少了它直接报“找不到表”。Excel 8.0对应 .xls(97-2003 格式),读 .xlsx 要换成Excel 12.0 Xml并把 Provider 换成Microsoft.ACE.OLEDB.12.0。IMEX=1是“把混合类型列当文本读”,这个参数在 Excel 分支里几乎是必加项,原因下一章细说。原代码把 Sheet 名写死成test1,实际用的时候必须改成动态获取,否则换个文件就崩——这是这份源码最需要你动手改的地方。
2.4 两个分支的差异对照
| 维度 | CSV 分支 | Excel 分支 |
|---|---|---|
| Provider | Microsoft.Jet.OLEDB.4.0 | Jet 4.0(.xls)/ ACE 12.0(.xlsx) |
| Data Source | 文件所在目录 | 文件完整路径 |
| 表名来源 | 文件名(去扩展名) | 工作表名 +$ |
| 关键扩展属性 | text;HDR;FMT=Delimited | Excel 8.0;HDR;IMEX=1 |
| 常见失败点 | 目录拼错、分隔符不符 | Sheet 名写死、驱动位数不匹配 |
这张表建议贴在工位上,因为 90% 的“未找到可安装的 ISAM”和“找不到表”都能对着它定位。跑通两个分支后,你已经拿到了一个能用的导入预览器,接下来才是真正花时间的部分:把参数调对、把边界兜住。
3. HDR、IMEX、FMT 三个参数:读错数据的根源都在这
3.1 HDR 决定第一行是数据还是列名
HDR=YES时 OleDb 把首行当列名,HDR=NO时首行也是数据,列名自动生成F1、F2……原代码在 CSV 分支注释里留了HDR=NO的备选,说明作者意识到有些 CSV 没有表头。判断标准很简单:打开文件看第一行是不是字段名。如果是纯数据,必须设HDR=NO,否则第一行数据会被吞掉当列名,你会在表格里发现少了一行却查不出原因——这种“静默丢数据”比报错更坑。
设成HDR=NO后,DataGridView里列头会变成F1、F2,原代码注释里那几行Columns[0].HeaderText = "编号"就是干这个的:手动把列头改回业务含义。注意Columns索引从 0 开始,且必须在DataSource赋值之后才能访问,顺序反了会抛空引用。
// HDR=NO 时列名是 F1/F2,绑定后手动改列头 dataGridView2.DataSource = dt.DefaultView; dataGridView2.Columns[0].HeaderText = "编号"; dataGridView2.Columns[1].HeaderText = "高度"; dataGridView2.Columns[2].HeaderText = "宽度";3.2 IMEX=1 是混合类型列的唯一解药
Excel 里一列既有数字又有文本(比如“编号”列里混了个“N/A”),OleDb 默认按前 8 行采样推断类型,推断成数字后遇到文本就返回NULL,你会看到一片空白而不是原始值。IMEX=1强制该列按文本读,代价是数字也变成字符串,后续要int.Parse得自己转。这个参数只对 Excel 驱动有效,CSV 的 text 驱动不认它——原代码在 CSV 分支注释里写IMEX=1其实是无效的,属于抄参数抄串了,别跟着学。
提示:
IMEX=1不是万能的。它只影响“混合类型”的判定,如果整列前 8 行都是数字、第 9 行才出现文本,采样阶段仍可能判成数字。稳妥做法是在同目录放schema.ini显式声明每列类型,或者干脆用IMEX=1加全列文本读入再在 C# 里做类型转换。
3.3 FMT=Delimited 与 schema.ini 的分隔符控制
FMT=Delimited告诉 text 驱动“按分隔符切”,默认分隔符是逗号。如果 CSV 是分号或制表符分隔,光改连接字符串不够,得在 CSV 同目录建一个schema.ini:
[data.csv] Format=Delimited(;) ColNameHeader=True CharacterSet=936Format=Delimited(;)指定分号,CharacterSet=936解决中文乱码(GBK 编码的 CSV 不加这行会读出问号)。schema.ini的文件名必须和 CSV 完全一致,放在同一目录,OleDb 会自动读取。这是处理非标准 CSV 最省事的办法,比手写解析器稳。
3.4 用 DataTable 承接后做类型清洗
不管走哪个分支,Fill出来的DataTable列类型往往是string(尤其加了IMEX=1之后)。如果下游要算数值,得遍历转换:
// 把文本列转成数值列,转换失败给默认值而不是抛异常 foreach (DataRow row in dt.Rows) { int height; if (!int.TryParse(row["高度"].ToString(), out height)) height = 0; // 空值或脏数据兜底 row["高度"] = height; }TryParse而不是Parse,是因为真实报表里空单元格、N/A、带单位的“12cm”太常见,Parse一抛异常整个导入就断了。这一步放在Fill之后、绑定之前,属于“读进来再洗干净”的常规套路。
4. 避坑与排查:五条血泪经验
4.1 报“未在本地计算机上注册 Microsoft.Jet.OLEDB.4.0”
现象:程序在开发机跑得好好的,拷到产线机器上一点就抛这个异常。原因:Jet 4.0 只有 32 位版本,而项目编译成了 AnyCPU 且在 64 位系统上以 64 位进程运行,找不到 32 位驱动。解决:把项目平台目标改成 x86,或者改用Microsoft.ACE.OLEDB.12.0并安装对应位数的 Access Database Engine(32 位和 64 位不能共存,装之前先卸干净)。我一般直接锁 x86,省得跟驱动位数纠缠。
4.2 读 Excel 报“找不到表 test1$”
现象:明明文件里有 Sheet,SQL 却提示表不存在。原因:Sheet 名写死、带空格或特殊字符、或者文件其实是 .xlsx 却用了Excel 8.0扩展属性。解决:先枚举所有 Sheet 名再拼 SQL,别硬编码。
// 枚举 Sheet 名,避免写死 DataTable schema = olconn.GetOleDbSchemaTable( OleDbSchemaGuid.Tables, new object[] { null, null, null, "TABLE" }); foreach (DataRow r in schema.Rows) Console.WriteLine(r["TABLE_NAME"]); // 输出形如 test1$拿到名字后去掉末尾$再拼进[名字$]。带空格的 Sheet 名在 SQL 里要用方括号包住,[My Sheet$]这样写。
4.3 CSV 中文列名变乱码或问号
现象:CSV 用 Excel 打开正常,程序读出来列名是乱码。原因:文件是 GBK/GB2312 编码,text 驱动默认按系统 ANSI 代码页解析,跨机器就错。解决:在schema.ini里加CharacterSet=936,或者把 CSV 统一转成 UTF-8 带 BOM 再读。后者更彻底,但要注意有些老系统不认 BOM,转换前确认下游。
4.4 数字列读出来全是空
现象:Excel 某列明明是数字,DataTable里却是DBNull。原因:该列混了文本,采样推断成数字类型,文本单元格被置空;或者HDR=NO时列名对不上导致取值错位。解决:加IMEX=1强制文本读入,再在 C# 里TryParse转数值;同时核对HDR设置和实际表头是否一致。
4.5 连接和 DataTable 没释放,文件被占用
现象:导入一次后想删或改那个 Excel,提示“文件正被另一进程使用”。原因:原代码olconn.Open()之后没有Close,OleDbDataAdapter和DataTable也没Dispose。解决:用using包住连接和适配器,Fill完立刻关连接。
// using 保证连接和适配器释放,文件句柄不泄漏 using (OleDbConnection conn = new OleDbConnection(connStr)) using (OleDbDataAdapter adapter = new OleDbDataAdapter(commandText, conn)) { DataTable dt = new DataTable(); adapter.Fill(dt); // Fill 内部会自己开连接 dataGridView1.DataSource = dt.DefaultView; }注意Fill会自动打开和关闭连接,所以外面不用再手动Open,原代码那句olconn.Open()其实是多余的,还容易忘关。
5. 从能跑到好用:动态 Sheet 名、异常兜底与一次导入的完整封装
把上面这些坑填完,代码已经能应付大部分真实报表了,但离“敢交给同事用”还差一层封装。我一般会把读取逻辑抽成一个静态方法,输入文件路径、输出DataTable,异常在方法内消化成可读提示,调用方只管绑表格。核心改动有三处:扩展名判断用Path.GetExtension而不是EndsWith(避免test.csv.bak这种误判);Sheet 名动态枚举取第一个;所有 OleDb 操作套try-catch并把OleDbException的Message原样带出来,因为它的报错信息其实挺具体,藏着反而难排查。
// 统一入口:按扩展名分流,异常转成可读提示 public static DataTable ReadTable(string path) { string ext = Path.GetExtension(path).ToLower(); string connStr, sql; if (ext == ".csv") { string dir = Path.GetDirectoryName(path); string name = Path.GetFileName(path); connStr = "Provider=Microsoft.Jet.OLEDB.4.0;Data Source=" + dir + ";Extended Properties='text;HDR=YES;FMT=Delimited;'"; sql = "select * from [" + name + "]"; } else { connStr = "Provider=Microsoft.ACE.OLEDB.12.0;Data Source=" + path + ";Extended Properties='Excel 12.0 Xml;HDR=YES;IMEX=1;'"; sql = null; // Sheet 名待枚举 } try { using (OleDbConnection conn = new OleDbConnection(connStr)) { if (sql == null) { conn.Open(); DataTable schema = conn.GetOleDbSchemaTable( OleDbSchemaGuid.Tables, new object[] { null, null, null, "TABLE" }); string sheet = schema.Rows[0]["TABLE_NAME"].ToString(); sql = "select * from [" + sheet + "]"; } using (OleDbDataAdapter adapter = new OleDbDataAdapter(sql, conn)) { DataTable dt = new DataTable(); adapter.Fill(dt); return dt; } } } catch (OleDbException ex) { throw new InvalidOperationException("读取失败:" + ex.Message, ex); } }这段代码把 CSV 表名用方括号包住(文件名带空格也不怕),Excel 取第一个 Sheet,ACE 12.0同时兼容 .xls 和 .xlsx。调用方一行dataGridView1.DataSource = ReadTable(path).DefaultView;就完事。验证方法很土但有效:准备四个文件——标准 CSV、分号分隔 CSV、带混合类型的 .xls、多 Sheet 的 .xlsx,逐个喂进去看表格对不对、中文有没有乱码、数字列是不是空。四个都过,基本就能上产线了。
最后说个习惯。早些年我图省事,连接字符串直接抄网上的,IMEX抄到 CSV 分支、Data Source填成文件路径,结果在客户现场调了一下午才发现是参数串了。从那以后我每次接 OleDb 读文件的活,都强制先跑一遍“四文件验证”,并且把连接字符串按 CSV/Excel 分开写在两个常量里,绝不混用。这份源码包最大的意义不是它多完善,而是它把两个分支摆在一起,让你一眼看清差异在哪、坑埋在哪。希望帮到你。
本文还有配套的精品资源,点击获取