前言:业务背景与代码概述
在工业视觉检测场景中,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";
}
}
}
}


