Day 6 - 小项目:CSV 清洗
建议用时:180-240 分钟
你将学会什么
- CSV 是什么,为什么它本质上还是文本
- 如何按行读取 CSV 数据
- 如何用
Split拆字段 - 如何校验列数、空值、数字格式和业务范围
- 如何把有效数据放进集合,把错误原因放进错误列表
- 如何用 LINQ 对清洗后的数据统计
- 如何输出清洗结果和错误报告
- 为什么真实复杂 CSV 不能只靠
Split
本页仍然是 C# 基础小项目。先用内存里的几行 CSV 文本练清洗流程,再看文件读取版本,避免一开始卡在文件路径问题。
本页固定顺序
- 先学第一部分:弄懂今天最小、最重要的知识,并运行短例子。
- 再学第二部分:把刚学的知识组合成一个完整例子。
- 然后做第三部分:自己跟着敲,再完成重复训练和每日小测。
- 最后做第四部分:先独立完成作业,再用完整答案检查。
学习衔接
上一页学习的是“异常处理”,今天继续学习“小项目:CSV 清洗”。先使用上一页已经会的写法,再只增加今天这个新知识点;如果前置内容还不能独立敲出,先回上一页复习,不要硬跳。
今天的最低通过线
第一次学习不要求背完整页。完成下面 3 项,就可以继续:
- 能用自己的话说明“小项目:CSV 清洗”解决什么问题。
- 把第一部分的短例子亲手敲完,并确认每个例子都能运行。
- 不看完整答案完成第三部分至少前 3 个例子,再主动改一个值观察结果。
第一部分:先学原理和最小知识
这一部分先解决“这是什么、为什么要这样写”。每看到一个短例子就亲手敲一遍并运行,不要先背完整程序。
今天先抓住什么
CSV 看起来像表格,但程序读进来时通常是一行一行文本:
name,price,count
Keyboard,199,2
Mouse,abc,1清洗流程是:
原始行
-> 跳过表头
-> 拆字段
-> 检查列数
-> 去掉空格
-> 转换类型
-> 检查业务范围
-> 保存有效数据
-> 记录错误原因
-> 统计结果名词先讲清楚
| 名词 | 直接解释 | 例子 |
|---|---|---|
| CSV | 用逗号分隔字段的文本 | Keyboard,199,2 |
| 表头 | 第一行字段名 | name,price,count |
| 行 | CSV 里的一条文本 | 一条商品记录 |
| 字段 | 一行里按逗号拆出来的部分 | name、price、count |
| 解析 | 把文本拆成字段 | Split(',') |
| 清洗 | 把脏数据处理成可用数据 | 去空格、校验、跳过坏行 |
| 有效数据 | 通过全部校验的数据 | 商品名称、价格、数量都正确 |
| 错误报告 | 记录坏数据原因 | 第几行价格错误 |
为什么先用内存数据
文件读取会多一个路径问题。刚开始练清洗时,先把 CSV 行写在数组里:
string[] lines =
{
"name,price,count",
"Keyboard,199,2",
"Mouse,abc,1"
};这样能先专心练清洗逻辑。清洗逻辑稳定后,再把 lines 换成:
string[] lines = File.ReadAllLines("products.csv");Split(',') 怎么工作
string line = "Keyboard,199,2";
string[] parts = line.Split(',');
Console.WriteLine(parts[0]);
Console.WriteLine(parts[1]);
Console.WriteLine(parts[2]);输出:
Keyboard
199
2parts[0] 是商品名,parts[1] 是价格文本,parts[2] 是数量文本。
为什么要检查列数
如果这一行少了字段:
Keyboard,199那 parts[2] 就不存在。直接读取会越界。
所以要先判断:
string line = "Keyboard,199";
string[] parts = line.Split(',');
if (parts.Length != 3)
{
Console.WriteLine("列数不正确");
return;
}为什么每列都要起名字
不要一直写 parts[0]、parts[1]、parts[2]。先转成有意义的变量:
string line = " Keyboard , 199 , 2 ";
string[] parts = line.Split(',');
string name = parts[0].Trim();
string priceText = parts[1].Trim();
string countText = parts[2].Trim();
Console.WriteLine(name);
Console.WriteLine(priceText);
Console.WriteLine(countText);这样后面的校验更清楚。
错误行不要直接丢掉
清洗不是默默跳过。最好记录原因:
var errors = new List<string>();
errors.Add("第 3 行:价格格式错误");这样最后能知道文件哪里有问题。
Split 的限制
本课使用 Split(',') 是为了练基础流程。真实 CSV 可能出现:
"Keyboard, Pro",199,2商品名里本身有逗号,这时简单 Split 会拆错。真实项目要使用成熟 CSV 库。本页先不引入库,先把清洗思路学清楚。
CSV 清洗常用操作速查
| 需求 | 写法 | 作用 |
|---|---|---|
| 读取全部行 | File.ReadAllLines(path) | 文件版 CSV 输入 |
| 写出全部行 | File.WriteAllLines(path, lines) | 导出 CSV 或错误报告 |
| 判断文件存在 | File.Exists(path) | 读取前检查 |
| 拆字段 | line.Split(',') | 把一行拆成列 |
| 去掉空格 | value.Trim() | 清理字段 |
| 判断空字段 | string.IsNullOrWhiteSpace(value) | 必填校验 |
| 转金额 | decimal.TryParse(text, out decimal price) | 价格校验 |
| 转整数 | int.TryParse(text, out int count) | 数量校验 |
| 拼回 CSV 行 | string.Join(",", parts) | 导出时使用 |
| 跳过表头 | for (int i = 1; i < lines.Length; i++) | 第一行不处理 |
| 记录错误 | errors.Add(...) | 不让坏数据直接消失 |
| 统计总额 | products.Sum(...) | 清洗后汇总 |
错误收集模板
var errors = new List<string>();
int lineNumber = 3;
string reason = "价格格式错误";
errors.Add($"第 {lineNumber} 行:{reason}");错误信息要带行号,因为用户回去改 CSV 时,需要知道是哪一行。
导出清洗后 CSV
var products = new List<Product>
{
new("Keyboard", 199m, 2),
new("Cable", 19m, 3)
};
var outputLines = new List<string>();
outputLines.Add("name,price,count,total");
foreach (Product product in products)
{
decimal total = product.Price * product.Count;
outputLines.Add(string.Join(",", product.Name, product.Price.ToString("F2"), product.Count, total.ToString("F2")));
}
File.WriteAllLines("clean-products.csv", outputLines);
record Product(string Name, decimal Price, int Count);这段代码把清洗后的有效商品重新写成 CSV。
今天写 CSV 清洗时按这个顺序
- 准备包含正常行和错误行的数组。
- 从第 2 行开始处理,跳过表头。
- 空行直接记录错误。
Split(',')拆字段。- 检查字段数量。
- 每个字段先
Trim()。 - 数字字段用
TryParse。 - 业务范围不对也记录错误。
- 有效数据加入集合。
- 最后输出有效数据、统计结果、错误报告。
第二部分:把知识组合成完整例子
前面已经学过最小知识。现在把它们组合起来,先读懂执行顺序,再完整敲一遍。今天最终要写一个商品 CSV 清洗程序:读入原始行,跳过坏数据,保留有效数据,最后统计总金额。
最终你要写出这个程序
string[] lines =
{
"name,price,count",
"Keyboard,199,2",
"Mouse,abc,1",
" ,20,1",
"Cable,19,3",
"Monitor,899,0"
};
var products = new List<Product>();
var errors = new List<string>();
for (int i = 1; i < lines.Length; i++)
{
string line = lines[i];
if (string.IsNullOrWhiteSpace(line))
{
errors.Add($"第 {i + 1} 行:空行");
continue;
}
string[] parts = line.Split(',');
if (parts.Length != 3)
{
errors.Add($"第 {i + 1} 行:列数不正确");
continue;
}
string name = parts[0].Trim();
string priceText = parts[1].Trim();
string countText = parts[2].Trim();
if (string.IsNullOrWhiteSpace(name))
{
errors.Add($"第 {i + 1} 行:商品名称为空");
continue;
}
if (!decimal.TryParse(priceText, out decimal price))
{
errors.Add($"第 {i + 1} 行:价格格式错误");
continue;
}
if (!int.TryParse(countText, out int count))
{
errors.Add($"第 {i + 1} 行:数量格式错误");
continue;
}
if (price < 0 || count <= 0)
{
errors.Add($"第 {i + 1} 行:价格或数量范围错误");
continue;
}
products.Add(new Product(name, price, count));
}
decimal total = products.Sum(product => product.Price * product.Count);
Console.WriteLine("=== 有效商品 ===");
foreach (Product product in products)
{
Console.WriteLine($"{product.Name}: {product.Price:F2} x {product.Count}");
}
Console.WriteLine($"有效行数: {products.Count}");
Console.WriteLine($"总金额: {total:F2}");
Console.WriteLine("=== 错误报告 ===");
foreach (string error in errors)
{
Console.WriteLine(error);
}
record Product(string Name, decimal Price, int Count);输出里会看到:
Keyboard: 199.00 x 2
Cable: 19.00 x 3
有效行数: 2
总金额: 455.00同时会看到价格错误、名称为空、数量范围错误的报告。
第三部分:跟着敲代码
从这里开始动手。每个例子都可以直接放进 Program.cs 运行。
例子 1:读取内存里的每一行
string[] lines =
{
"name,price,count",
"Keyboard,199,2",
"Mouse,abc,1"
};
foreach (string line in lines)
{
Console.WriteLine(line);
}先确认每一行都能输出。
例子 2:拆字段并检查列数
string line = "Keyboard,199,2";
string[] parts = line.Split(',');
if (parts.Length != 3)
{
Console.WriteLine("列数不正确");
return;
}
Console.WriteLine(parts[0]);
Console.WriteLine(parts[1]);
Console.WriteLine(parts[2]);预期输出:
Keyboard
199
2例子 3:清洗一行商品
string line = " Keyboard , 199 , 2 ";
string[] parts = line.Split(',');
if (parts.Length != 3)
{
Console.WriteLine("列数不正确");
return;
}
string name = parts[0].Trim();
string priceText = parts[1].Trim();
string countText = parts[2].Trim();
if (!decimal.TryParse(priceText, out decimal price))
{
Console.WriteLine("价格格式错误");
return;
}
if (!int.TryParse(countText, out int count))
{
Console.WriteLine("数量格式错误");
return;
}
Console.WriteLine($"{name}: {(price * count):F2}");预期输出:
Keyboard: 398.00例子 4:处理多行并记录错误
var lines = new List<string>
{
"Keyboard,199,2",
"Mouse,abc,1",
"Cable,19,3"
};
var errors = new List<string>();
foreach (string line in lines)
{
string[] parts = line.Split(',');
if (parts.Length != 3)
{
errors.Add($"列数不对: {line}");
continue;
}
string name = parts[0].Trim();
bool priceOk = decimal.TryParse(parts[1], out decimal price);
bool countOk = int.TryParse(parts[2], out int count);
if (!priceOk || !countOk)
{
errors.Add($"数字格式不对: {line}");
continue;
}
Console.WriteLine($"{name}: {(price * count):F2}");
}
Console.WriteLine("错误数量: " + errors.Count);第二行会被记录为错误。
例子 5:用 LINQ 统计清洗后的数据
var products = new List<Product>
{
new("Keyboard", 199m, 2),
new("Cable", 19m, 3)
};
decimal total = products.Sum(product => product.Price * product.Count);
int totalCount = products.Sum(product => product.Count);
decimal maxPrice = products.Max(product => product.Price);
Console.WriteLine($"商品种类: {products.Count}");
Console.WriteLine($"商品件数: {totalCount}");
Console.WriteLine($"总金额: {total:F2}");
Console.WriteLine($"最高单价: {maxPrice:F2}");
record Product(string Name, decimal Price, int Count);例子 6:文件读取版本
string fileName = "products.csv";
if (!File.Exists(fileName))
{
Console.WriteLine("文件不存在,请先创建 products.csv");
return;
}
string[] lines = File.ReadAllLines(fileName);
Console.WriteLine($"读取到 {lines.Length} 行");文件版本只比内存版本多了文件读取,后面的清洗流程一样。
常见错误和修法
| 错误 | 为什么错 | 修法 |
|---|---|---|
| 直接按逗号切复杂 CSV | 引号内逗号会被误切 | 本阶段先处理简单 CSV,真实项目用 CSV 库 |
| 不检查列数 | 缺列会导致下标越界 | parts.Length 不够时记录错误 |
价格库存直接 Parse | 一行错误会中断全部清洗 | 用 TryParse,失败行写入错误列表 |
| 清洗后不写错误报告 | 不知道哪些数据有问题 | 输出成功文件和错误文件 |
忘记 Trim() | 前后空格影响校验 | 每个字段解析前先裁剪 |
小白重复敲写训练
CSV 清洗不要直接上完整文件,先拿一行文本反复练。
训练 1:拆开一行 CSV
string line = "P001, Keyboard ,199.9";
string[] parts = line.Split(',');
foreach (string part in parts)
{
Console.WriteLine(part.Trim());
}改动任务:加入库存字段 , 5,确认数组长度变成 4。
训练 2:校验列数
string line = "P001,Keyboard";
string[] parts = line.Split(',');
if (parts.Length != 3)
{
Console.WriteLine("这一行不是三列");
}改动任务:补上第三列,让错误提示不再出现。
训练 3:清洗并转换价格
string line = "P001, Keyboard , 199.9 ";
string[] parts = line.Split(',');
string code = parts[0].Trim();
string name = parts[1].Trim();
if (decimal.TryParse(parts[2].Trim(), out decimal price))
{
Console.WriteLine($"{code} | {name} | {price:F2}");
}第三遍把价格改成 abc,补一个 else 输出错误信息。
每日小测
做完本页后,用这 5 题检查是否真的掌握。
1. 判断题
本页的目标不是只把代码运行起来,还要能说清楚“为什么这样写”。
答案:对。能运行只是第一步,能解释原理、常用操作和常见错误,才说明本页内容进入了可复用能力。
2. 填空题
本页主题是:小项目:CSV 清洗。今天至少要掌握的 3 个点是:
1. CSV 是什么,为什么它本质上还是文本
2. 如何按行读取 CSV 数据
3. 如何用 `Split` 拆字段答案:以上 3 点必须能用自己的代码跑通,不能只停留在阅读。
3. 流程题
遇到本页相关功能时,先按什么顺序处理?
答案:先看完整例子,确认最终效果;再读原理和名词;然后跟着第三部分从空项目敲代码;最后对照作业答案检查。
4. 找错误题
如果本页代码运行失败,第一步应该做什么?
答案:先看终端或 IDE 里的第一条错误,找到文件名和行号;不要同时改很多地方。再回到本页的“常见错误和修法”表格,对照错误类型逐项排查。
5. 改需求题
在本页完整例子跑通后,至少改一个小需求。
可选改法:
- 改一个字段名称。
- 多加一个校验条件。
- 多输出一行结果。
- 把固定数据改成用户输入。
- 把一次处理改成多条数据处理。
答案标准:修改后能重新运行,并能说明这次修改影响了哪一段逻辑。重点检查:CSV 是什么,为什么它本质上还是文本。
上位机专项练习
CSV 常用于导入设备点位和导出采集记录。今天把文本清洗改成设备编号、时间和采集值。
下面 3 个例子都要亲手敲。先运行原代码,再完成每个例子后面的改动任务。
专项例子 1:拆分一行采集记录
string line = "PLC-01,2026-07-13 10:00:00,25.6";
string[] parts = line.Split(',');
Console.WriteLine($"设备: {parts[0]}");
Console.WriteLine($"数值: {parts[2]}");运行结果或界面效果:
设备: PLC-01
数值: 25.6改动任务: 增加第四列单位 ℃ 并读取。
专项例子 2:跳过格式错误的行
string[] lines = { "PLC-01,25.6", "错误行", "PLC-02,30.1" };
foreach (string line in lines)
{
string[] parts = line.Split(',');
if (parts.Length != 2)
{
Console.WriteLine($"跳过: {line}");
continue;
}
Console.WriteLine($"{parts[0]} = {parts[1]}");
}运行结果或界面效果:
PLC-01 = 25.6
跳过: 错误行
PLC-02 = 30.1改动任务: 加入一个空字符串,观察是否被跳过。
专项例子 3:验证采集值是不是数字
string line = "PLC-01,abc";
string[] parts = line.Split(',');
if (parts.Length == 2 && double.TryParse(parts[1], out double value))
{
Console.WriteLine($"有效值: {value}");
}
else
{
Console.WriteLine("采集值格式错误");
}运行结果或界面效果:
采集值格式错误改动任务: 把 abc 改成 26.8。
第四部分:作业完整答案
这一部分给出当天作业的完整答案。先按第三部分做一遍,再用这里检查。
作业要求
写一个商品 CSV 清洗程序,要求:
- 使用内存数组模拟 CSV。
- 第一行是表头,要跳过。
- 每行必须有 3 列:商品名、价格、数量。
- 商品名不能为空。
- 价格必须是数字,且不能小于 0。
- 数量必须是整数,且必须大于 0。
- 有效数据放进
List<Product>。 - 错误原因放进
List<string>。 - 输出有效商品、总金额、错误报告。
作业拆解
按这个顺序写:
- 准备
lines。 - 创建
products和errors。 - 从下标
1开始循环,跳过表头。 - 先判断空行。
- 用
Split(',')拆字段。 - 检查列数。
- 清理字段空格。
- 转换价格和数量。
- 判断业务范围。
- 添加有效商品或错误信息。
完整代码
string[] lines =
{
"name,price,count",
"Keyboard,199,2",
"Mouse,abc,1",
" ,20,1",
"Cable,19,3",
"Monitor,899,0",
"Desk,300,1"
};
var products = new List<Product>();
var errors = new List<string>();
for (int i = 1; i < lines.Length; i++)
{
string line = lines[i];
int lineNumber = i + 1;
if (string.IsNullOrWhiteSpace(line))
{
errors.Add($"第 {lineNumber} 行:空行");
continue;
}
string[] parts = line.Split(',');
if (parts.Length != 3)
{
errors.Add($"第 {lineNumber} 行:列数不正确");
continue;
}
string name = parts[0].Trim();
string priceText = parts[1].Trim();
string countText = parts[2].Trim();
if (string.IsNullOrWhiteSpace(name))
{
errors.Add($"第 {lineNumber} 行:商品名称为空");
continue;
}
if (!decimal.TryParse(priceText, out decimal price))
{
errors.Add($"第 {lineNumber} 行:价格格式错误");
continue;
}
if (!int.TryParse(countText, out int count))
{
errors.Add($"第 {lineNumber} 行:数量格式错误");
continue;
}
if (price < 0)
{
errors.Add($"第 {lineNumber} 行:价格不能小于 0");
continue;
}
if (count <= 0)
{
errors.Add($"第 {lineNumber} 行:数量必须大于 0");
continue;
}
products.Add(new Product(name, price, count));
}
Console.WriteLine("=== 有效商品 ===");
foreach (Product product in products)
{
Console.WriteLine($"{product.Name}: {product.Price:F2} x {product.Count}");
}
decimal total = products.Sum(product => product.Price * product.Count);
int totalCount = products.Sum(product => product.Count);
Console.WriteLine($"有效行数: {products.Count}");
Console.WriteLine($"商品件数: {totalCount}");
Console.WriteLine($"总金额: {total:F2}");
Console.WriteLine("=== 错误报告 ===");
foreach (string error in errors)
{
Console.WriteLine(error);
}
record Product(string Name, decimal Price, int Count);正确运行结果
你应该能看到有效商品:
Keyboard: 199.00 x 2
Cable: 19.00 x 3
Desk: 300.00 x 1
有效行数: 3
商品件数: 6
总金额: 755.00也应该能看到错误报告:
第 3 行:价格格式错误
第 4 行:商品名称为空
第 6 行:数量必须大于 0你要能说清楚这 7 件事
- CSV 读进程序后先是一行行字符串。
Split(',')可以拆基础 CSV 字段。- 拆字段后先检查列数。
- 每个字段要先
Trim()。 - 数字字段用
TryParse。 - 有效数据和错误报告要分开保存。
- 真实复杂 CSV 应该使用专门 CSV 库。