欢迎光临
我们一直在努力

从CSV到数据库:C#文件流读取与数据清洗的完整实现方案

前言:业务背景与代码概述

在工业视觉检测场景中,CCD 设备会生成大量 CSV 格式的检测数据,需要定期导入 MES 系统数据库做质量追溯与数据分析。本文基于一段项目中真实使用的 C# 代码,从文件读取、数据清洗、格式转换到数据库插入全流程拆解,分析其设计思路,指出潜在坑点,并给出工业级优化方案。

一、整体架构与执行流程

1. 代码结构总览
  • 命名空间:ImportData
  • 状态枚举:ImportResult(统一导入结果状态码)
  • 核心类:ReadFile
  • 核心方法:EReadCCDData(静态方法,负责完整导入流程)
  • 辅助方法:CleanField(字段清洗)、FormatDetectTime(时间格式适配)
2. 完整执行流程图(文字版)

传入文件路径+文件名

校验目录是否存在 → 不存在则返回状态D并记录日志

校验CSV文件是否存在 → 不存在则返回状态F并记录日志

初始化计数器(总行数/成功数/失败数/行号)

GB18030编码打开文件,跳过第一行表头

逐行读取CSV:
空行直接跳过
按逗号分割字段,校验字段数≥19
清洗所有字段(去空格+单引号转义)
格式化检测时间(适配SQL Server datetime)
拼接INSERT SQL语句
执行数据库插入
成功/失败分别计数并记录日志

输出导入统计日志,返回最终状态

二、核心代码逐段深度解析

1. 导入状态枚举设计

internal enum ImportResult
{
DirectoryNotExist = 'D',
FileNotExist = 'F',
FileReady = 'M',
Success = 'C',
Error = 'E'
}

解析:

  • 用字符枚举定义所有导入状态,通过ref string isOK参数对外传递结果;
  • 状态码覆盖了【目录异常、文件异常、就绪、成功、错误】全场景,便于上层调用方根据状态做后续逻辑;
  • 设计技巧:用字符作为枚举值,转字符串后可直接作为状态标识。
2. 文件前置校验逻辑

string fullFilePath = Path.Combine(filePath, $"{fileName}.csv");
if (!Directory.Exists(filePath))
{
isOK = ((char)ImportResult.DirectoryNotExist).ToString();
DataInfo.RunLogInfo($"导入失败:目录不存在 {filePath}");
return;
}
if (!File.Exists(fullFilePath))
{
isOK = ((char)ImportResult.FileNotExist).ToString();
DataInfo.RunLogInfo($"导入失败:文件不存在 {fileName.Trim()}.csv");
return;
}

解析:

  • 用Path.Combine拼接路径,避免手动拼接斜杠的跨平台 / 格式错误;
  • 先校验目录、再校验文件,分层校验;
  • 校验失败返回并记录日志。
3. CSV 文件读取与编码处理

using (var reader = new StreamReader(fullFilePath, Encoding.GetEncoding("GB18030")))
{
// 跳过表头
if (!reader.EndOfStream)
{
reader.ReadLine();
lineNumber = 1;
}
while (!reader.EndOfStream)
{
string line = reader.ReadLine();
lineNumber++;
// …后续处理
}
}

解析:

  • 使用了 StreamReader 并显式指定了 Encoding.GetEncoding("GB18030")。很多工业设备导出的中文CSV文件并非UTF-8,而是GB2312或GB18030,如果不指定编码,读取中文会出现乱码。
4. 数据校验与字段清洗

if (string.IsNullOrWhiteSpace(line))
{
continue;
}
string[] values = line.Split(',');
if (values.Length < 19)
{
failCount++;
DataInfo.RunLogInfo($"跳过第{lineNumber}行:字段数量不足");
continue;
}

配合CleanField辅助方法:

private static string CleanField(string value)
{
return value == null ? string.Empty : value.Trim().Replace("'", "''");
}

解析:

  • 空行自动跳过;
  • 校验字段数量,防止后续数组索引越界异常;
  • CleanField做了两件关键事:① 去除字段前后空格,解决 CSV 导出时的多余空格问题;② 单引号转义(' → ''),这是 SQL 字符串拼接的基础防注入处理,避免字段中的单引号导致 SQL 语法错误
5. 时间格式适配

private static string FormatDetectTime(string timeStr)
{
if (string.IsNullOrWhiteSpace(timeStr))
return "NULL";
try
{
timeStr = timeStr.Trim();
int lastColonIndex = timeStr.LastIndexOf(':');
if (lastColonIndex > 0)
{
timeStr = timeStr.Substring(0, lastColonIndex) + "." + timeStr.Substring(lastColonIndex + 1);
}
return $"'{timeStr.Replace("'", "''")}'";
}
catch
{
return "NULL";
}
}

解析:

  • 解决工业设备 CSV 的典型问题:很多 CCD 设备导出的时间格式是2025-08-15 10:16:15:384(毫秒用冒号分隔),而 SQL Server 的datetime类型要求毫秒用点分隔,因此通过替换最后一个冒号为小数点做格式兼容;
  • 异常降级处理:时间格式解析失败时返回NULL,不让单行时间异常导致整行数据丢弃,兼顾数据完整性和容错性;
  • 同样做了单引号转义,保证 SQL 拼接的语法安全。
6. 数据库插入与统计

string sql = $@"
INSERT INTO CCDDATA
(时间,总结果,偏移值X,…极耳面积,WorkTime)
VALUES
({detectTime}, '{totalResult}', …, '{DateTime.Now:yyyy-MM-dd HH:mm:ss}')";
DBAccess.EAI.ClsGlobal.objDataConnect.DataExecute(sql, DBKey.MES);
successCount++;
isOK = ((char)ImportResult.Success).ToString();

解析:

  • 直接拼接 INSERT 语句,通过封装好的DBAccess数据库访问类执行,指定DBKey.MES数据库实例,符合多数据库场景的设计;
  • WorkTime字段写入当前系统时间,作为数据入库时间,用于追溯导入时机;
  • 维护totalLines/successCount/failCount三个计数器,结束后输出统计日志,便于监控导入效果。

三、实操效果截图

图 1:CCD设备导出原始 CSV 文件

设备导出原始 CSV 文件,首行为表头,共 19 列业务字段;时间格式为2026-06-25 17:10:23:567,毫秒使用冒号分隔,SQL Server 无法直接识别,代码专门做格式转换处理。

图1

图 2:上位机操作运行界面

 程序操作主界面,填入文件路径、文件名后点击导入按钮,日志框实时输出校验、解析、统计信息;本次测试导入 178 条数据,成功 178 条,失败 0 条,最终状态码 C 代表导入完成。

图2

图 3:数据库的截图界面

完成 CSV 逐行解析与 SQL 插入执行后,所有 CCD 检测数据会持久化写入 SQL Server 的CCDDATA数据表。下图为本次导入后的数据库实际运行效果,与上位机界面日志的统计结果完全对应。

图3

四、总结

这段代码是一个典型的工业数据采集脚本,它解决了编码兼容和特殊格式清洗这两个最头疼的问题。

这段代码展示了文件流操作和字符串处理的基础;我们可以在此基础上引入参数化查询和批量提交机制,将其升级为一个高性能的数据同步工具。

希望这篇文章的分析对你的开发工作有所帮助!

五、附录:项目原始完整源码

using System;
using System.IO;
using DBAccess.EAI;
using System.Text;
using System.Windows.Forms;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Threading.Tasks;

namespace ImportData
{
internal enum ImportResult
{
DirectoryNotExist = 'D',
FileNotExist = 'F',
FileReady = 'M',
Success = 'C',
Error = 'E'
}

class ReadFile
{
public static void EReadCCDData(string filePath, string fileName, ref string isOK)
{
string fullFilePath = Path.Combine(filePath, $"{fileName}.csv");

if (!Directory.Exists(filePath))
{
isOK = ((char)ImportResult.DirectoryNotExist).ToString();
DataInfo.RunLogInfo($"导入失败:目录不存在 – {filePath}");
return;
}
if (!File.Exists(fullFilePath))
{
isOK = ((char)ImportResult.FileNotExist).ToString();
DataInfo.RunLogInfo($"导入失败:文件不存在 – {fileName.Trim()}.csv");
return;
}
isOK = ((char)ImportResult.FileReady).ToString();
DataInfo.RunLogInfo($"开始导入文件:{fullFilePath}");

int totalLines = 0;
int successCount = 0;
int failCount = 0;
int lineNumber = 0;

using (var reader = new StreamReader(fullFilePath, Encoding.GetEncoding("GB18030")))
{
// 跳过表头
if (!reader.EndOfStream)
{
reader.ReadLine();
lineNumber = 1;
}

while (!reader.EndOfStream)
{
string line = reader.ReadLine();
lineNumber++;
totalLines++;

if (string.IsNullOrWhiteSpace(line))
{
continue;
}

try
{
string[] values = line.Split(',');
if (values.Length < 19)
{
failCount++;
DataInfo.RunLogInfo($"跳过第{lineNumber}行:字段数量不足");
continue;
}

// 格式化检测时间,适配SQL Server datetime格式
string detectTime = FormatDetectTime(values[0]);

string totalResult = CleanField(values[1]);
string offsetX = CleanField(values[2]);
string offsetY = CleanField(values[3]);
string offsetR = CleanField(values[4]);
string poleLength = CleanField(values[5]);
string poleWidth = CleanField(values[6]);
string tabHeight = CleanField(values[7]);
string tabWidth = CleanField(values[8]);
string tabPosition = CleanField(values[9]);
string topLeftAngle = CleanField(values[10]);
string topRightAngle = CleanField(values[11]);
string bottomLeftAngle = CleanField(values[12]);
string bottomRightAngle = CleanField(values[13]);
string topLeftCorner = CleanField(values[14]);
string topRightCorner = CleanField(values[15]);
string bottomLeftCorner = CleanField(values[16]);
string bottomRightCorner = CleanField(values[17]);
string tabArea = CleanField(values[18]);

// WorkTime 保留为数据导入的系统时间
string sql = $@"
INSERT INTO CCDDATA
(时间,总结果,偏移值X,偏移值Y,偏移值R,极片长度,极片宽度,极耳高度,极耳宽度,极耳位置,
左上夹角,右上夹角,左下夹角,右下夹角,左上缺角,右上缺角,左下缺角,右下缺角,极耳面积,WorkTime)
VALUES
({detectTime}, '{totalResult}', '{offsetX}', '{offsetY}', '{offsetR}', '{poleLength}', '{poleWidth}',
'{tabHeight}', '{tabWidth}', '{tabPosition}', '{topLeftAngle}', '{topRightAngle}',
'{bottomLeftAngle}', '{bottomRightAngle}', '{topLeftCorner}', '{topRightCorner}',
'{bottomLeftCorner}', '{bottomRightCorner}', '{tabArea}',
'{DateTime.Now:yyyy-MM-dd HH:mm:ss}')";

DBAccess.EAI.ClsGlobal.objDataConnect.DataExecute(sql, DBKey.MES);
successCount++;
isOK = ((char)ImportResult.Success).ToString();
}
catch (Exception ex)
{
failCount++;
DataInfo.RunLogInfo($"处理第{lineNumber}行失败:{ex.Message}");
}
}
}

DataInfo.RunLogInfo($"文件导入完成:{fullFilePath}");
DataInfo.RunLogInfo($"总行数:{totalLines},成功:{successCount},失败:{failCount},状态:{isOK}");
}

/// <summary>
/// 统一字段清洗:去空白 + 单引号转义
/// </summary>
private static string CleanField(string value)
{
return value == null ? string.Empty : value.Trim().Replace("'", "''");
}

/// <summary>
/// 检测时间格式化:将CSV的冒号毫秒格式转为SQL可识别的点分隔毫秒格式
/// 例:2025-08-15 10:16:15:384 → '2025-08-15 10:16:15.384'
/// 格式异常时返回 NULL,不影响整行数据插入
/// </summary>
private static string FormatDetectTime(string timeStr)
{
if (string.IsNullOrWhiteSpace(timeStr))
return "NULL";

try
{
timeStr = timeStr.Trim();
// 将最后一个冒号替换为小数点,适配SQL datetime标准格式
int lastColonIndex = timeStr.LastIndexOf(':');
if (lastColonIndex > 0)
{
timeStr = timeStr.Substring(0, lastColonIndex) + "." + timeStr.Substring(lastColonIndex + 1);
}
// 加单引号返回,直接拼接SQL
return $"'{timeStr.Replace("'", "''")}'";
}
catch
{
// 格式异常返回NULL,避免整行插入失败
return "NULL";
}
}
}
}

赞(0)
未经允许不得转载:171主机测评 » 从CSV到数据库:C#文件流读取与数据清洗的完整实现方案
分享到: 更多 (0)

评论 抢沙发

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址