前面这一整个阶段,我们其实一直在为一件事做准备。
学文件,是为了让程序真正和外部数据打交道。 学异常,是为了让程序在真实环境里不要一碰就碎。 学调试,是为了程序出问题时,你能自己把它查出来。
如果这些知识点一直分开看,很多人会觉得自己都懂一点,但还没形成真正的整体感。所以这一章,我们不再拆着讲,而是做一个完整的小项目,把前面学过的内容真正串起来。
这一次,我们来做一个很适合入门的小型数据处理器。
它要做的事情并不复杂,但非常像真实工作里的小工具:
读取一个本地成绩文件 检查每一行数据是否合法 把合法数据整理出来 跳过错误数据 计算总分和平均分 最后把处理结果写入新的文件
你会发现,这个小项目麻雀虽小,但几乎把这一阶段最核心的东西都用上了。
一、先别急着写代码,先看项目需求
很多初学者做综合案例时,最容易犯的错,就是一上来就开始敲代码。结果写着写着就乱了,因为脑子里根本没有整体流程。
所以真正动手之前,先把需求拆出来。
假设我们现在有一个 scores.csv 文件,内容如下:
姓名,成绩张三,90李四,85王五,abc赵六,78小明,小红,92
注意,这份数据里并不是每一行都干净。
王五,abc 这一行里,成绩不是数字。小明, 这一行里,成绩是空的。
也就是说,这不是一份完美数据,而是一份很真实的脏数据。
我们的程序目标是:
先读取这份 CSV 文件 逐行检查每条数据 如果成绩能转成整数,就当成有效数据 如果成绩有问题,就跳过并记录 最后计算有效成绩的总分、人数、平均分 再把结果写到一个 report.txt 文件里
你看,这就是一个非常典型的数据处理任务。
二、为什么这个小项目特别适合你现在做
因为它刚好把前面几个核心知识点全串起来了。
读取文件,要用到文件操作。 处理表格数据,要用到 CSV。 脏数据转整数时可能报错,要用到异常处理。 文件可能不存在,也要处理异常。 程序写完后要把结果导出,又要用到文件写入。 如果过程中出错,还得学会看报错、调试问题。
也就是说,这不是为了凑一个案例,而是真正让前面学的内容落地。
你从这一章开始,要慢慢体会到一件事:
编程不是一个知识点一个知识点背下来,而是把多个能力拼起来解决一个完整问题。
三、先把程序流程画清楚
写代码前,先把处理流程想清楚。
第一步,尝试打开 scores.csv第二步,读取表头,跳过第一行 第三步,一行一行读取学生姓名和成绩 第四步,尝试把成绩转成整数 第五步,如果成功,就加入有效成绩列表 第六步,如果失败,就记录这是一条错误数据 第七步,循环结束后,计算总分和平均分 第八步,把结果写入 report.txt
你会发现,一旦流程清晰,代码就没那么可怕了。
很多人写程序写乱,不是因为不会语法,而是因为没有先把流程想明白。
四、先写出第一版完整代码
下面我们直接来看第一版完整实现。
import csvvalid_scores = []error_lines = []try:with open('scores.csv', 'r', encoding='utf-8') as f: reader = csv.reader(f) next(reader)for row in reader:try: name = row[0] score = int(row[1]) valid_scores.append((name, score))except: error_lines.append(row) total = 0for item in valid_scores: total += item[1] count = len(valid_scores) average = total / countwith open('report.txt', 'w', encoding='utf-8') as f: f.write('成绩处理结果\n') f.write(f'有效人数:{count}\n') f.write(f'总分:{total}\n') f.write(f'平均分:{average:.2f}\n') f.write('\n') f.write('有效数据如下:\n')for name, score in valid_scores: f.write(f'{name}:{score}\n') f.write('\n') f.write('错误数据如下:\n')for row in error_lines: f.write(f'{row}\n') print('处理完成,结果已写入 report.txt')except FileNotFoundError: print('没有找到 scores.csv 文件,请检查文件是否存在')
这段代码看起来比前面单个例子长了一些,但不要怕。我们接下来一段一段拆开看,你会发现它并没有想象中难。
五、第一层 try,在保护什么
先看最外层这个结构:
try:with open('scores.csv', 'r', encoding='utf-8') as f: ...except FileNotFoundError: print('没有找到 scores.csv 文件,请检查文件是否存在')
这一层的作用很明确:
程序先尝试打开 scores.csv。 如果这个文件根本不存在,就不要让程序直接崩掉,而是给出一条清楚的提示。
这就是异常处理最典型的实际价值。
如果没有这一层,一旦文件不见了,程序就会直接抛出一大段报错。 而加上它以后,程序就显得更稳,也更像一个能给人用的小工具。
六、第二层 try,在保护什么
再看循环内部这段:
for row in reader:try: name = row[0] score = int(row[1]) valid_scores.append((name, score))except: error_lines.append(row)
这一层是在处理每一行数据时兜底。
为什么要这么写?
因为我们已经知道,真实数据不一定干净。 有些行可能成绩不是数字。 有些行可能干脆是空的。 有些行甚至列数都不完整。
所以程序每读到一行,都要抱着一种现实心态:
这行数据也许是正常的,也许有问题。
如果能顺利拿到姓名、把成绩转成整数,那就说明这一行是有效数据。 如果这里出错了,就把它记进 error_lines,等后面统一处理。
这就是一个很重要的思维升级:
不要幻想所有数据都完美,而要让程序具备处理脏数据的能力。
七、为什么这里用两个列表
我们定义了两个列表:
valid_scores = []error_lines = []
它们的作用完全不同。
valid_scores 用来保存处理成功的数据。error_lines 用来保存处理失败的数据。
为什么要分开?
因为后面我们要做两件不同的事。
对有效数据,要计算总分和平均分。 对错误数据,要单独记录下来,方便排查。
如果你把所有东西混在一个列表里,后面处理会非常乱。 而分开保存以后,整个程序逻辑一下就清楚了。
这其实也在提醒你一件事:
写程序时,数据结构一旦设计清楚,后面的代码会轻松很多。
八、为什么有效数据保存成元组
看这句:
valid_scores.append((name, score))
这里我们保存的不是单独一个名字,也不是单独一个成绩,而是一个二元组。
比如某一条数据可能会被存成:
('张三', 90)
这样做的好处是,姓名和成绩天然绑在一起。 后面你遍历有效数据时,就能一边拿到姓名,一边拿到成绩。
比如写报告时:
for name, score in valid_scores: f.write(f'{name}:{score}\n')
这就非常自然。
你现在应该慢慢有这种意识:
程序不是只在写代码,更是在组织数据。
九、为什么成绩计算要放在数据清洗之后
看这里:
total = 0for item in valid_scores: total += item[1]count = len(valid_scores)average = total / count
注意这个顺序很重要。
我们不是一边读文件一边急着算平均分,而是先把有效数据筛出来,再统一计算。 这样逻辑会更清楚,也更稳。
因为只有当你确定哪些是合法成绩以后,总分和平均分才有意义。
如果你把坏数据也混进去算,结果一定会乱。 所以这个过程其实分成了两步:
先清洗 再统计
这就是很多数据处理任务的基本套路。
十、写报告文件,是在把程序结果落地
很多初学者写完程序以后,习惯只在屏幕上 print() 一下就结束。 这当然也可以,但很多真实场景里,我们更需要把结果保存下来。
所以这里我们把处理结果写入 report.txt:
with open('report.txt', 'w', encoding='utf-8') as f: ...
这样有什么好处?
第一,结果不会随着程序结束而消失。 第二,后面可以反复打开看。 第三,更适合分享、归档、留痕。
你会慢慢发现,程序真正有用的时候,往往不是输出一闪而过,而是能把结果留下来。
十一、如果一个有效成绩都没有,会发生什么
这个问题非常值得想。
看这句:
average = total / count
如果 count 等于 0,会怎么样?
没错,又会触发除零错误。
这说明哪怕我们已经处理了文件不存在、脏数据这些问题,程序依然可能在别的地方出状况。 这就是现实开发的样子。
所以第一版代码虽然已经能用,但还不够稳。 接下来我们要继续改进它。
十二、把程序再写稳一点
下面是改进版代码,我们专门处理“没有有效成绩”这种情况。
import csvvalid_scores = []error_lines = []try:with open('scores.csv', 'r', encoding='utf-8') as f: reader = csv.reader(f) next(reader)for row in reader:try: name = row[0] score = int(row[1]) valid_scores.append((name, score))except: error_lines.append(row) total = 0for name, score in valid_scores: total += score count = len(valid_scores)if count > 0: average = total / countelse: average = 0with open('report.txt', 'w', encoding='utf-8') as f: f.write('成绩处理结果\n') f.write(f'有效人数:{count}\n') f.write(f'总分:{total}\n') f.write(f'平均分:{average:.2f}\n') f.write('\n') f.write('有效数据如下:\n')for name, score in valid_scores: f.write(f'{name}:{score}\n') f.write('\n') f.write('错误数据如下:\n')for row in error_lines: f.write(f'{row}\n') print('处理完成,结果已写入 report.txt')except FileNotFoundError: print('没有找到 scores.csv 文件,请检查文件是否存在')
改动不大,但程序明显更稳了。
如果一个有效成绩都没有,平均分就先记成 0。 虽然这只是一个简单处理方式,但它体现的是很重要的意识:
程序不只是写通,还要尽量考虑边界情况。
十三、这段代码里,其实已经有了小项目的味道
很多初学者会误以为,只有写网页、写系统、写几百行代码,才叫项目。其实不是。
只要一个程序具备下面这些特征,它就已经有项目雏形了:
有明确输入 有处理流程 有异常情况 有结果输出 有一定的健壮性
而我们现在这个小型数据处理器,已经基本具备这些特点了。
输入是 scores.csv处理是数据清洗和统计 异常情况有文件不存在、脏数据、空数据 输出是 report.txt
所以不要小看这种练习。 真正的能力,就是从这样一个个小项目里长出来的。
十四、再把代码优化一下,让它更清楚
前面的版本已经能用了,但我们还可以写得更像“结构清晰的程序”。
比如把“读取并清洗数据”和“生成报告”拆成两个函数。
import csvdefread_scores(filename): valid_scores = [] error_lines = []with open(filename, 'r', encoding='utf-8') as f: reader = csv.reader(f) next(reader)for row in reader:try: name = row[0] score = int(row[1]) valid_scores.append((name, score))except: error_lines.append(row)return valid_scores, error_linesdefwrite_report(filename, valid_scores, error_lines): total = 0for name, score in valid_scores: total += score count = len(valid_scores)if count > 0: average = total / countelse: average = 0with open(filename, 'w', encoding='utf-8') as f: f.write('成绩处理结果\n') f.write(f'有效人数:{count}\n') f.write(f'总分:{total}\n') f.write(f'平均分:{average:.2f}\n') f.write('\n') f.write('有效数据如下:\n')for name, score in valid_scores: f.write(f'{name}:{score}\n') f.write('\n') f.write('错误数据如下:\n')for row in error_lines: f.write(f'{row}\n')try: valid_scores, error_lines = read_scores('scores.csv') write_report('report.txt', valid_scores, error_lines) print('处理完成,结果已写入 report.txt')except FileNotFoundError: print('没有找到 scores.csv 文件,请检查文件是否存在')
你会发现,一拆成函数以后,结构立刻更清晰了。
读取归读取。 写报告归写报告。 主流程也更像一句完整的话:
先读数据 再写报告 如果文件不存在,就提示用户
这就是前面函数阶段知识的真正价值。 不是为了写函数而写函数,而是为了把程序组织得更清楚。
十五、这里为什么暂时还用裸 except
你可能已经注意到了,这里内层还写的是:
except: error_lines.append(row)
严格来说,这不是最规范的写法。 因为它会把所有异常都接住。
更稳一点的写法,可以写得更明确一些,比如:
except (ValueError, IndexError): error_lines.append(row)
为什么?
ValueError 对应的是成绩转整数失败。IndexError 对应的是某一行列数不够,比如只有姓名没有成绩。
这两种异常,才是我们真正预期到的脏数据问题。 如果写成这种形式,代码会更准确。
所以你可以把内层改成这样:
for row in reader:try: name = row[0] score = int(row[1]) valid_scores.append((name, score))except (ValueError, IndexError): error_lines.append(row)
这也刚好对应了我们前面学过的一条原则:
异常处理要尽量明确,不要一股脑把所有错误都吞掉。
十六、如果程序跑出来结果不对,该怎么调试
这正是前一章要接上的地方。
比如你发现 report.txt 里的有效人数不对,或者错误数据少了、多了,这时候怎么办?
第一步,不要慌。 第二步,不要一上来就大改代码。 第三步,加打印,看程序到底处理了什么。
例如你可以在循环里先打印每一行:
for row in reader: print('当前读取到的行是:', row)try: name = row[0] score = int(row[1]) valid_scores.append((name, score))except (ValueError, IndexError): error_lines.append(row)
还可以进一步打印处理结果:
print('有效数据:', valid_scores)print('错误数据:', error_lines)
很多问题一打印就清楚了。 因为你终于不再靠猜,而是看到了程序真实处理的数据。
这就是调试能力真正开始发挥作用的地方。
十七、这个小项目最值得你学到的,不只是代码本身
说到底,这一章最重要的不是把这几十行代码背下来,而是学会这种处理问题的思路。
先明确输入是什么 再明确输出是什么 然后把流程拆开 知道哪一步可能出错 提前给这些风险做处理 最后把结果落地保存
这就是一个很完整的开发思维雏形。
如果你以后做通讯录、记账本、成绩管理、日志分析、文件整理,其实思路都是类似的。 换的只是业务内容,不变的是解决问题的方法。
十八、你可以自己继续升级这个小工具
学到这里,这个小项目其实还能继续往前改。
比如你可以加上:
统计最高分和最低分 把错误数据单独写进 error.txt让用户自己输入要处理的文件名 支持处理更多列,比如语文、数学、英语三科 对空白姓名也做检查 把平均分保留两位小数 处理完成后把总结打印到屏幕上
你会发现,真正的项目进步,不是突然重写一个完全不同的东西,而是在现有基础上一点点扩展。
这也很像真实开发。 大多数项目都不是从零到一夜之间变复杂,而是一版一版长出来的。
十九、本章小练习
你可以自己动手做一个变体版练习。
准备一个 books.csv 文件,内容像这样:
书名,价格Python基础,59数据分析,abc自动化办公,88爬虫实战,Flask入门,72
然后写一个小程序,完成下面这些事:
读取这份文件 把价格合法的书籍筛出来 把价格有问题的行单独记录 计算所有有效书籍的总价和平均价 把结果写入 book_report.txt
这个练习和我们本章的成绩处理器几乎是同一种结构。 你只要能自己独立改出来,说明这一阶段的内容已经真的进脑子了。
二十、本章总结
这一章,我们把文件、异常、调试这些看起来分散的知识点,真正串成了一个完整的小项目。
你学会了如何读取 CSV 文件。 学会了在处理每一行数据时用异常处理兜底。 学会了把合法数据和错误数据分开保存。 学会了在统计之前先清洗数据。 学会了把最终结果写入新的本地文件。 也进一步体会到,程序真正的价值,不只是跑通,而是能在真实数据和真实问题面前保持稳定。
到这里,第七阶段就算正式收束了。 你已经不只是会写一些零散的小语法,而是开始具备把文件、异常和调试能力组合起来解决实际问题的基础。
下一章,我们就进入全新的第八阶段:071|从现实世界理解类与对象:OOP 并不抽象。