Skip to Content
Week 02Day 6 - 小项目:CSV 清洗

Day 6 - 小项目:CSV 清洗

建议用时:180-240 分钟

你将学会什么

  • CSV 是什么,为什么它本质上还是文本
  • 如何按行读取 CSV 数据
  • 如何用 Split 拆字段
  • 如何校验列数、空值、数字格式和业务范围
  • 如何把有效数据放进集合,把错误原因放进错误列表
  • 如何用 LINQ 对清洗后的数据统计
  • 如何输出清洗结果和错误报告
  • 为什么真实复杂 CSV 不能只靠 Split

本页仍然是 C# 基础小项目。先用内存里的几行 CSV 文本练清洗流程,再看文件读取版本,避免一开始卡在文件路径问题。

本页固定顺序

  1. 先学第一部分:弄懂今天最小、最重要的知识,并运行短例子。
  2. 再学第二部分:把刚学的知识组合成一个完整例子。
  3. 然后做第三部分:自己跟着敲,再完成重复训练和每日小测。
  4. 最后做第四部分:先独立完成作业,再用完整答案检查。

学习衔接

上一页学习的是“异常处理”,今天继续学习“小项目: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 2

parts[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 清洗时按这个顺序

  1. 准备包含正常行和错误行的数组。
  2. 从第 2 行开始处理,跳过表头。
  3. 空行直接记录错误。
  4. Split(',') 拆字段。
  5. 检查字段数量。
  6. 每个字段先 Trim()
  7. 数字字段用 TryParse
  8. 业务范围不对也记录错误。
  9. 有效数据加入集合。
  10. 最后输出有效数据、统计结果、错误报告。

第二部分:把知识组合成完整例子

前面已经学过最小知识。现在把它们组合起来,先读懂执行顺序,再完整敲一遍。今天最终要写一个商品 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 清洗程序,要求:

  1. 使用内存数组模拟 CSV。
  2. 第一行是表头,要跳过。
  3. 每行必须有 3 列:商品名、价格、数量。
  4. 商品名不能为空。
  5. 价格必须是数字,且不能小于 0。
  6. 数量必须是整数,且必须大于 0。
  7. 有效数据放进 List<Product>
  8. 错误原因放进 List<string>
  9. 输出有效商品、总金额、错误报告。

作业拆解

按这个顺序写:

  1. 准备 lines
  2. 创建 productserrors
  3. 从下标 1 开始循环,跳过表头。
  4. 先判断空行。
  5. Split(',') 拆字段。
  6. 检查列数。
  7. 清理字段空格。
  8. 转换价格和数量。
  9. 判断业务范围。
  10. 添加有效商品或错误信息。

完整代码

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 库。