-
CSV文件处理——C#编写销售数据导入导出
第一部分:C#基础入门
CSV文件处理——销售数据导入导出
实例介绍
CSV(逗号分隔值) 是企业中最常用的轻量级数据交换格式——它比Excel简单(无需解析复杂的二进制结构),比XML/JSON更省空间,跨平台兼容性极强(几乎所有系统、工具都支持)。销售数据的导入导出是典型场景:比如将ERP系统的销售记录导出为CSV给vb.net教程C#教程python教程SQL教程access 2010教程
财务部门,或从经销商提供的CSV中导入销售数据到自己的系统。
本节不依赖任何第三方库(如CsvHelper),用原生C# IO+正则表达式实现CSV的完整处理,带你掌握CSV的标准格式规则(如特殊字符转义)、导入导出的核心逻辑,以及如何处理实际场景中的异常(如格式错误、编码问题)。
需求分析
-
业务场景
模拟销售数据的双向流转:
导出:将内存中的SalesRecord列表(含订单ID、产品名称、销售数量、单价、销售日期、客户名称)导出为CSV文件,支持处理含逗号、引号的特殊字段;
导入:从CSV文件解析出SalesRecord列表,正确处理带引号的字段、日期/数值类型转换,兼容UTF-8/GBK编码;
异常处理:捕获文件不存在、格式错误(如字段数不匹配、日期格式错误)等问题,返回友好提示。 -
核心规则(遵循RFC 4180标准)
字段间用逗号分隔;
第一行可选表头;
字段含逗号/引号时,需用双引号包裹;
字段内的双引号需转义为两个双引号(如He said "Hello" → "He said ""Hello""");
行结束符为CRLF( )。
代码实现
csharp
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
namespace CsvSalesDemo
{
// ==================== 1. 销售记录模型 ====================
public class SalesRecord
{
public string OrderId { get; set; } = string.Empty;
public string ProductName { get; set; } = string.Empty;
public int Quantity { get; set; }
public decimal UnitPrice { get; set; }
public DateTime SaleDate { get; set; }
public string CustomerName { get; set; } = string.Empty;
}
// ==================== 2. CSV处理工具类 ====================
public static class CsvSalesHandler
{
// CSV字段分割正则(处理带引号的字段)
private static readonly Regex _csvFieldRegex = new Regex(
@"(""([^""]|""""")*""|[^,]*)(?=,|$)",
RegexOptions.Compiled | RegexOptions.ExplicitCapture
);
/// <summary>
/// 导出销售记录到CSV文件
/// </summary>
/// <param name="records">销售记录列表</param>
/// <param name="filePath">文件路径</param>
/// <param name="encoding">编码(默认UTF-8)</param>
public static void ExportToCsv(List<SalesRecord> records, string filePath, Encoding? encoding = null)
{
encoding ??= Encoding.UTF8;
var lines = new List<string>();
// 1. 添加表头
lines.Add("OrderId,ProductName,Quantity,UnitPrice,SaleDate,CustomerName");
// 2. 添加数据行
foreach (var record in records)
{
var fields = new List<string>
{
EscapeCsvField(record.OrderId),
EscapeCsvField(record.ProductName),
record.Quantity.ToString(),
record.UnitPrice.ToString("F2"),
record.SaleDate.ToString("yyyy-MM-dd"),
EscapeCsvField(record.CustomerName)
};
lines.Add(string.Join(",", fields));
}
// 3. 写入文件(用CRLF换行)
File.WriteAllLines(filePath, lines, encoding);
Console.WriteLine($"✅ 导出成功:{filePath}(编码:{encoding.EncodingName})");
}
/// <summary>
/// 从CSV文件导入销售记录
/// </summary>
/// <param name="filePath">文件路径</param>
/// <param name="encoding">编码(默认UTF-8)</param>
/// <returns>销售记录列表</returns>
public static List<SalesRecord> ImportFromCsv(string filePath, Encoding? encoding = null)
{
encoding ??= Encoding.UTF8;
var records = new List<SalesRecord>();
// 1. 检查文件存在
if (!File.Exists(filePath))
{
throw new FileNotFoundException("CSV文件不存在", filePath);
}
// 2. 读取所有行(跳过表头)
var lines = File.ReadAllLines(filePath, encoding).Skip(1).ToList();
if (lines.Count == 0)
{
Console.WriteLine("⚠️ CSV文件无数据行");
return records;
}
// 3. 解析每一行
for (int i = 0; i < lines.Count; i++)
{
var line = lines[i].Trim();
if (string.IsNullOrEmpty(line)) continue;
try
{
// 分割字段(处理带引号的情况)
var fields = ParseCsvLine(line);
if (fields.Count != 6)
{
throw new FormatException($"字段数不匹配(预期6个,实际{fields.Count}个)");
}
// 转换数据类型
records.Add(new SalesRecord
{
OrderId = fields[0],
ProductName = fields[1],
Quantity = int.TryParse(fields[2], out var qty) ? qty : 0,
UnitPrice = decimal.TryParse(fields[3], out var price) ? price : 0m,
SaleDate = DateTime.TryParse(fields[4], out var date) ? date : DateTime.MinValue,
CustomerName = fields[5]
});
}
catch (Exception ex)
{
Console.WriteLine($"❌ 解析第{i+2}行失败:{ex.Message}(内容:{line})");
}
}
Console.WriteLine($"✅ 导入完成:共{records.Count}条有效记录");
return records;
}
/// <summary>
/// 转义CSV字段(处理逗号、引号)
/// </summary>
private static string EscapeCsvField(string field)
{
if (string.IsNullOrEmpty(field)) return string.Empty;
// 若字段含逗号、引号或换行,需用双引号包裹
if (field.Contains(',') || field.Contains('"') || field.Contains('
') || field.Contains('
'))
{
// 引号转义为两个引号
return $""{field.Replace(""", """")}"";
}
return field;
}
/// <summary>
/// 解析CSV行(提取字段,处理带引号的情况)
/// </summary>
private static List<string> ParseCsvLine(string line)
{
var fields = new List<string>();
var matches = _csvFieldRegex.Matches(line);
foreach (Match match in matches)
{
var field = match.Value;
// 去除前后引号,并转义回单个引号
if (field.StartsWith('"') && field.EndsWith('"'))
{
field = field.Substring(1, field.Length - 2).Replace("""", """);
}
fields.Add(field);
}
return fields;
}
}
// ==================== 3. 测试程序 ====================
class Program
{
static void Main(string[] args)
{
// 测试数据(含特殊字符)
var testRecords = new List<SalesRecord>
{
new() { OrderId = "ORD2025001", ProductName = "Laptop, Pro", Quantity = 2, UnitPrice = 9999.00m, SaleDate = new DateTime(2025, 1, 15), CustomerName = "张三"先生" },
new() { OrderId = "ORD2025002", ProductName = "Mouse", Quantity = 5, UnitPrice = 129.00m, SaleDate = new DateTime(2025, 1, 16), CustomerName = "李四" }
};
// 1. 导出测试
string csvPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "sales.csv");
CsvSalesHandler.ExportToCsv(testRecords, csvPath);
// 2. 导入测试
Console.WriteLine("
=== 导入结果 ===");
var importedRecords = CsvSalesHandler.ImportFromCsv(csvPath);
foreach (var record in importedRecords)
{
Console.WriteLine($"订单ID:{record.OrderId} | 产品:{record.ProductName} | 数量:{record.Quantity} | 日期:{record.SaleDate:yyyy-MM-dd}");
}
}
}
}
逐行讲解
-
销售记录模型(SalesRecord)
包含销售场景的核心字段:订单ID、产品名称、数量、单价、销售日期、客户名称;
所有字符串字段默认初始化空字符串,避免空引用异常。 -
CSV处理工具类(CsvSalesHandler)
(1)导出方法(ExportToCsv)
编码处理:默认UTF-8,支持传入自定义编码(如GBK);
表头生成:第一行固定为字段名,与模型属性对应;
字段转义:EscapeCsvField方法处理特殊字符——若字段含逗号、引号或换行,用双引号包裹,引号转义为两个引号(符合RFC 4180标准);
写入文件:用File.WriteAllLines写入,自动处理换行(CRLF)。
(2)导入方法(ImportFromCsv)
文件检查:先判断文件是否存在,避免FileNotFoundException;
跳过表头:用Skip(1)跳过第一行表头;
行解析:ParseCsvLine方法通过正则表达式提取字段,处理带引号的情况(正则匹配规则:要么是带引号的字段,要么是不含逗号的普通字段);
类型转换:用TryParse安全转换数值和日期,避免格式错误导致整个导入失败;
异常捕获:针对每一行单独捕获异常,打印错误信息但不中断整个导入流程。
(3)核心辅助方法
EscapeCsvField:转义特殊字段,确保导出的CSV格式正确;
ParseCsvLine:用正则表达式解决CSV字段分割的痛点(直接Split(',')会错误分割引号内的逗号)。 -
测试程序
特殊字符测试:第一条记录的产品名称含逗号(Laptop, Pro),客户名称含引号(张三"先生),验证导出导入是否正确处理;
流程验证:导出后立即导入,对比原始数据与导入结果是否一致。
基础知识拓展 - CSV标准格式(RFC 4180)
| 规则 | 说明 |
|---|---|
| 分隔符 | 逗号(,) |
| 换行符 | CRLF( ) |
| 特殊字段 | 含逗号、引号或换行的字段必须用双引号包裹 |
| 引号转义 | 字段内的双引号需替换为两个双引号("") |
| 表头 | 可选,若存在则为第一行 |
-
原生IO vs 第三方库(如CsvHelper)
| 方案 | 优点 | 缺点 |
| ---- | ---- | ---- |
| 原生IO | 轻量无依赖、灵活可控 | 需手动处理转义、类型转换、异常;代码量大 |
| CsvHelper | 强大的映射功能(模型与CSV字段自动匹配)、支持验证、分页、大文件处理 | 需要安装NuGet包;学习成本略高 | -
编码问题
UTF-8:跨平台首选,建议带BOM(避免部分工具乱码);
GBK:适合国内传统系统(如老版Excel),需指定Encoding.GetEncoding("GBK");
注意:导入时需与导出编码一致,否则会出现乱码。
扩展思考
大文件处理:若CSV文件超过100MB,用File.ReadAllLines会占用大量内存,可改为StreamReader逐行读取:
csharp
using var reader = new StreamReader(filePath, encoding);
reader.ReadLine(); // 跳过表头
string? line;
while ((line = reader.ReadLine()) != null) { /* 解析行 */ }
自定义分隔符:支持分号(;)或制表符( ),只需修改正则表达式和转义逻辑;
数据验证:导入时添加业务规则(如数量>0、单价>0),不符合的记录标记为无效;
数据库集成:导入后将记录存入SQL Server(用Dapper/EFCore),导出前从数据库查询数据。
总结
本节通过原生C#实现了CSV的导入导出,重点解决了特殊字符处理、格式解析、异常兼容等核心问题,让你理解CSV的底层逻辑。虽然第三方库更便捷,但原生实现能帮你掌握文件操作、正则表达式、编码处理等基础技能。在实际项目中,若需求简单用原生IO即可,复杂场景推荐用CsvHelper等成熟库。
记住:CSV看似简单,但细节(如特殊字符、编码)处理不好会导致数据错乱——遵循标准格式是避免问题的关键!
(实例完)










