ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Python文件处理实战:学生成绩读取、平均分计算与排序写入

Python文件处理实战:学生成绩读取、平均分计算与排序写入

这道题看着简单——读文件、算平均分、排序、写新文件,四步走完好像就结束了。但真正动手做的时候你会发现,凡是能把这一步做得干净利落的人,前五道题的基础一定很扎实。作为一个带过不少入门者、也改过大量作业的人,我很清楚这类题目真正的价值不在"能跑出结果",而在于你有没有把一个数据处理的完整链路打通。下面我把整道题的拆解思路、实现方式、以及那些不写进答案里的坑,一次性讲清楚。

1. 题目拆解:这道题真正想考察的三个能力点

1.1 表面上是在排序,底层其实在考文件流

仔细读题目:"将第5题stud文件中的学生数据,按平均分进行排序处理,将已排序的学生数据存入一个新文件stu_sort中。"

这句话里有几个关键动作:读取、计算、排序、写入。很多初学者第一反应是"排序好难",但实际上排序在Python里就是一行sorted()的事。真正容易出问题的,反而是最不起眼的"读取"和"写入"。

文件操作最典型的问题就是:你怎么知道数据读进来是对的?你怎么保证写出去之后别人能正常打开、不乱码、不错位?这些问题不自己踩一遍,背再多语法都没用。所以这道题表面上是一道排序题,但只要你稍微较真一点,它就会把你逼着去面对编码、数据类型、文件路径、异常处理这些"脏活累活"。这正是它作为阶段性练习的用意所在。

1.2 数据格式约定:没有格式,一切排序都是空谈

题目里只说"stud文件",没有明确给出文件内容长什么样。在实际练习中,最常见的约定格式是每行一条学生记录,字段之间用空格或制表符分隔,形如:

2023001 张明 85 90 78 2023002 李华 72 88 65 2023003 王芳 91 83 79

从左到右依次是:学号、姓名、语文成绩、数学成绩、英语成绩。不同的教材或作业平台可能用逗号(CSV格式)或者用竖线分隔,但核心逻辑是一样的。

这里我想强调一个习惯:不管题目有没有明确格式,动手写代码之前,先打开文件看一眼。哪怕它只有三行数据,你也得知道每个字段在第几个位置、成绩是整数还是带小数、有没有表头、有没有空行。因为你的解析代码是完全依赖这些细节的,格式假设错了,后面写得再漂亮也是错的。

1.3 为什么"平均分"要现算而不是直接读

有人可能会想:如果文件里已经有平均分字段,是不是就不用算了?道理上确实可以,但绝大多数这类题目给的文件故意不包含平均分,目的就是让你自己算一遍。

算平均分这个操作,本身又牵扯出两个问题:一是成绩字段有几个、是不是固定数量;二是要不要保留小数、保留几位。这两个问题都直接影响到排序结果和最终文件内容。所以别把"算平均分"当作一个理所当然的中间步骤,它在整个题目里承担着"把原始数据加工成可比较指标"的角色,是很关键的一环。

2. 读取stud文件:数据解析的几种常见姿势与健壮性设计

2.1 按行读取与字段拆分的基本方法

Python里读取文件最推荐的方式是用with语句,它会在代码块结束后自动关闭文件,避免文件句柄泄漏。

with open('stud.txt', 'r', encoding='utf-8') as f: for line in f: line = line.rstrip('\n') # 后续处理...

这里注意一个细节:for line in f拿到的每一行是包含换行符的,直接split()其实无所谓,因为它会按空白字符切分,自动把末尾的换行符忽略掉。但如果你用的是line.split(',')这种按逗号切分的方式,换行符就会粘在最后一个字段后面,处理起来就麻烦。所以我个人习惯先rstrip('\n'),把换行符处理掉再做后续操作,逻辑更干净。

拆字段的方式取决于分隔符:

# 空格/制表符分隔 parts = line.split() # 逗号分隔(CSV) parts = line.split(',')

split()不带参数的时候,会把连续多个空格或制表符当做一个分隔符处理,这在实际数据里非常实用——因为手敲的数据经常有"多个空格对齐"的情况。按逗号切分则更严格,字段里但凡多一个逗号就会错位。

2.2 成绩字符串转数值:一个隐藏的类型陷阱

从文件里读出来的所有内容都是字符串,就算文件里写的是85,读进来也是'85'。如果你直接拿去做算术运算,Python会报TypeError,或者更隐蔽地,字符串之间做+运算变成拼接。

所以必须显式转换:

scores = list(map(float, parts[2:]))

这里用float而不是int,是因为成绩可能带小数,比如89.5。如果你确定题目里的成绩都是整数,用int也没问题,但用float的兼容性更好,而且后面算平均分时不容易出现整数除法的困惑。Python 3里/本身就是浮点除法,所以用int其实也不会出错,但从数据健壮性考虑,float更稳妥。

提示:转换的时候如果某一行数据格式有问题,float()会抛出ValueError。初学阶段你当然可以选择"假设数据永远正确",但稍微加一点异常处理,可以让你在程序崩溃的时候立刻定位到是文件的第几行出了问题。

2.3 文件里可能出现哪些"坏数据"

我见过不少同学在读取环节翻车,不是语法不会,而是没预料到数据本身的意外情况。最常见的四类:

  1. 空行:文件末尾或者中间不小心多了一个空行,直接split()会得到空列表,取parts[0]就崩溃。
  2. 字段缺失:某一行的成绩少写了一个,parts[2:]得到的成绩数量不一样,后面算平均分时分子分母不匹配。
  3. 多余空格:行首行尾有空格,如果不做strip(),第一个字段可能会带前导空格。
  4. 编码问题:文件是用GBK保存的,你用UTF-8打开就会乱码甚至报错。

针对前两类,简单的防御性写法是:

if not line.strip(): continue parts = line.split() if len(parts) < 5: print(f"警告:跳过格式异常的行 - {line.strip()}") continue

别看这几行代码简单,它体现了"我的程序是给真实数据用的,而不只是给标准答案用的"这一层意识。这也是区分"会写作业"和"会写代码"的分水岭。

3. 平均分计算:从"会算"到"算得对"

3.1 核心计算逻辑与字段数量的灵活性

假设一条记录有1个学号、1个姓名、3门成绩,平均分就是三门成绩之和除以3。但如果有的行只有2门成绩呢?你的代码不应该写死sum(scores) / 3,而应该写成:

avg = sum(scores) / len(scores)

用len(scores)动态获取成绩数量,这样以后扩展成4门、5门课都不用改逻辑。这个细节很多人不注意,等到题目变一下——"把3科改成5科"——就又重新写一遍代码。写代码的人应该有点"偷懒"的意识:能用数据长度推导的,就不要写死常量。

3.2 精度问题:平均分到底保留几位

算出来的平均分可能是无限小数,比如85.3333333333。你会发现,如果直接把整个avg存进列表,打印出来会很长;但如果四舍五入到2位再排序,可能出现两个学生原始平均分不同、保留两位后却相同的现象。

这里我的建议是:排序用原始值,展示用格式化值。也就是说,计算avg的时候保持浮点数的完整精度,排序时直接用这个完整精度的值;只有当写入stu_sort文件时,再把平均分格式化成两位小数。这样既保证了排序的准确性,又保证了输出文件的整洁。

格式化的方式:

f"{avg:.2f}"

这是最简洁的写法。老一点的代码里会看到'{:.2f}'.format(avg)或者round(avg, 2),但round是真正的数值舍入,存进文件没问题,可如果后面还要参与计算,round之后反而可能引入误差,不如在输出的最后一刻再格式化。

3.3 缺考的科目:平均分该除以几

这是一个容易被忽略但实际中很常见的问题:如果某个学生有一门课缺考,文件里这一门成绩存的是0或者干脆没有这个字段,平均分应该怎么算?

不同题目有不同的约定。有的题目约定缺考记0分,那很简单,照常除以科目总数。有的约定缺考字段直接不写,那就要看你的代码策略了:

  • 策略A:只对存在的成绩求平均,即sum(scores)/len(scores),这反映的是"已考科目的平均表现"。
  • 策略B:按固定科目数求平均,缺考计0,即sum(scores)/3。

哪种更合理取决于业务场景。考试场景里通常用策略B,因为总排名要统一标准;课程作业场景里可能用策略A。做题的时候一定要看题目有没有明确约定。如果题目没说,我倾向于写清楚自己的假设,比如在代码注释里写明"缺考科目按0分计入"。

4. 排序处理:三种实现思路与它们的适用边界

4.1 排序的本质:把"比较规则"讲清楚

排序本身并不复杂,复杂的是"按什么排"。题目说的是"按平均分排序",那排序的键就是平均分。Python里sorted函数和列表的sort方法都支持一个key参数,用来指定从每个元素中提取出用于比较的值。

例如,如果你把每个学生存成字典:

students = [ {'id': '2023001', 'name': '张明', 'scores': [85, 90, 78], 'avg': 84.33}, {'id': '2023002', 'name': '李华', 'scores': [72, 88, 65], 'avg': 75.00}, ]

按平均分升序:

students.sort(key=lambda s: s['avg'])

按平均分降序(从高到低,这个通常更符合"排名"的习惯):

students.sort(key=lambda s: s['avg'], reverse=True)

如果你用的是列表的列表结构,比如[学号, 姓名, 成绩列表],那就需要从嵌套结构里取索引:

students.sort(key=lambda s: s[2])

4.2sort()和sorted()到底用哪个

这是入门阶段最容易被问到的区别:

  • list.sort():原地排序,直接修改原列表,返回None,更省内存。
  • sorted(list):返回一个新的排序后列表,原列表不变。

在我们这个场景里,数据读取完成后,后续所有操作都用同一个列表,没有什么"需要保留原顺序"的理由,所以用sort()就够了。但如果你是从只读数据源获取数据,后面还想保留原始顺序做别的分析,那就用sorted()。

我见过不少同学在这道题里纠结半天,最后发现两个都对,只是使用场景不同。这种纠结本身其实是好事,说明你在思考工具的适用边界。

4.3 平均分相同怎么办——排序稳定性在起作用

如果两个学生的平均分恰好一样,最终谁排在前面?这里就涉及到排序算法的稳定性。Python的sort是稳定排序,意思是:当两个元素的排序键相等时,它们之间的相对顺序保持原文件中的顺序。

这意味着,如果你先用学号排序,再按平均分排序,那么平均分相同的学生会按照学号有序排列。如果你希望平均分相同时按姓名排、或者按语文成绩排,可以一次指定多个键:

students.sort(key=lambda s: (s['avg'], s['scores'][0]), reverse=True)

注意这里reverse=True会同时反转两个键的升序方向,也就是说语文成绩也是从高到低。如果你想"平均分降序,语文成绩升序",那就不能直接传reverse,得用负值技巧或者多次排序:

# 平均分降序,语文成绩升序 students.sort(key=lambda s: s['scores'][0]) # 先按语文升序 students.sort(key=lambda s: s['avg'], reverse=True) # 再按平均分降序

这个技巧在职场上很常用,叫"多次排序法",利用稳定排序的特性,后排序的键优先级更高。

5. 写入stu_sort文件:格式设计与可读性兼顾

5.1 输出格式:别让排序结果变成一坨难读的字符串

写文件的代码很简短,但格式设计却值得想想。你需要决定:

  • 每条记录还是不是"学号 姓名 成绩1 成绩2 成绩3 平均分"?
  • 平均分放到最后还是紧跟姓名?
  • 字段之间用空格还是制表符?
  • 要不要写入表头?

我推荐的方案:和原文件保持相同的字段分隔方式,平均分追加在末尾。这样做的最大好处是,后面如果还有下一题(比如"读取stu_sort再统计"),你的解析代码可以复用,不用为两种格式分别写逻辑。

5.2 写入文件的基本写法

with open('stu_sort.txt', 'w', encoding='utf-8') as f: for stu in students: scores_str = ' '.join(str(s) for s in stu['scores']) f.write(f"{stu['id']} {stu['name']} {scores_str} {stu['avg']:.2f}\n")

用with同样是负责自动关闭文件。写入模式'w'会清空文件原有内容再写入,所以要注意:如果你不小心把文件名写成了stud.txt,那你的源数据就被清空了。建议在写之前先确认文件名是stu_sort而不是原文件。

5.3 编码问题:为什么Windows下写出的文件用记事本打开是乱码

这是几乎所有初学者都会遇到的经典问题。原因很简单:你用encoding='utf-8'写文件,而Windows记事本默认用GBK解码。解决办法有两个:

一是换成GBK编码写入:

with open('stu_sort.txt', 'w', encoding='gbk') as f:

二是保持UTF-8写入,但在文件开头写入BOM(字节序标记),让记事本自动识别编码。Python里可以用encoding='utf-8-sig',它会自动加上BOM头。

with open('stu_sort.txt', 'w', encoding='utf-8-sig') as f:

哪种更好?如果这个文件只是自己程序读,用什么编码都行,只要读写一致。如果要给别人用Excel打开,UTF-8加BOM更友好。如果题目没有特别说明,我建议用utf-8保持跨平台兼容性,同时了解utf-8-sig这个逃生舱的存在。

6. 完整实现与一次跑通的参考代码

6.1 组合起来:从读取到排序到写入的完整链路

下面是一个完整的参考实现,包含了前面讨论的各种细节处理,并加上了必要的注释。这个版本对异常数据做了一定的防御处理,同时保持逻辑清晰:

def read_students(file_path): students = [] with open(file_path, 'r', encoding='utf-8') as f: for line_num, line in enumerate(f, 1): line = line.strip() if not line: continue parts = line.split() if len(parts) < 5: print(f"警告:第{line_num}行字段不足,已跳过: {line}") continue stu_id, name = parts[0], parts[1] try: scores = list(map(float, parts[2:])) except ValueError: print(f"警告:第{line_num}行成绩数据异常,已跳过: {line}") continue avg = sum(scores) / len(scores) students.append({ 'id': stu_id, 'name': name, 'scores': scores, 'avg': avg }) return students def write_students(students, file_path): with open(file_path, 'w', encoding='utf-8') as f: for stu in students: scores_str = ' '.join(str(int(s)) if s.is_integer() else str(s) for s in stu['scores']) f.write(f"{stu['id']} {stu['name']} {scores_str} {stu['avg']:.2f}\n") def main(): students = read_students('stud.txt') students.sort(key=lambda s: s['avg'], reverse=True) write_students(students, 'stu_sort.txt') print(f"处理完成,共排序 {len(students)} 条学生记录。") if __name__ == '__main__': main()

这段代码里有个小细节:写出成绩时,如果成绩是整数,就转成int来写,这样文件里看到的是85而不是85.0。成绩如果是85.5,则保留85.5。这是为了让输出文件更像原始文件,避免无意义的.0后缀干扰阅读。

6.2 我见过程序里出现频率最高的五个报错

第一个是FileNotFoundError,文件路径写错或者文件不在当前目录。解决的办法是打印当前工作目录import os; print(os.getcwd()),或者用绝对路径。

第二个是UnicodeDecodeError,文件实际编码不是UTF-8。处理方式是先用记事本或编辑器确认文件编码,然后把打开参数改成对应编码,比如encoding='gbk'。

第三个是ValueError: could not convert string to float,某一行成绩字段里混入了非数字内容。这时候程序会直接中断,你需要加上异常处理或者先打印出那一行内容排查。

第四个是KeyError,如果你给字典取了别的键名,比如average,后面排序又用了avg,那就会报错。这类问题最好的预防办法是:字典的键名保持一致,并且避免拼写太接近的多个变体。

第五个是"排序结果和预期不一致"。很多人以为降序是从小到大,但reverse=True是从大到小。这类逻辑错误不会报错,但结果就是错的。遇到这种情况,先把排序结果打印出来人工核对一遍再写文件。

6.3 从"做完题"到"做好题"的几个进阶思考

如果你已经能把上面代码跑通,我建议你再做三件事。

第一,把代码改成命令行参数版本,让文件路径从外部传入:

import sys input_file = sys.argv[1] if len(sys.argv) > 1 else 'stud.txt' output_file = sys.argv[2] if len(sys.argv) > 2 else 'stu_sort.txt'

这样程序就不再是"只能处理固定文件"的一次性脚本了。

第二,把排序方式做成可配置的,比如通过参数决定升序还是降序。虽然题目只需要降序,但多一个选择,程序的适用面就更宽。

第三,考虑用csv模块或者pandas来实现同样的功能。前者是标准库,处理CSV格式更规范;后者是数据分析的利器,一行df.sort_values('平均分', ascending=False)就能完成排序。但我不建议在初学阶段直接用pandas来完成这道题——因为文件操作和基础数据结构的练习价值会被掩盖掉。

写在最后的一点个人体会

这种"读文件、算数据、排序、写文件"的小练习,我前前后后看过太多人做了。有的十分钟交差,有的折腾两小时还卡在编码问题上。说实话,这道题的重点从来不是排序函数怎么调,而是你有没有建立起一种意识:程序处理的数据是有来源、有格式、有异常可能的现实数据,代码要对这些情况负责。

在我自己写程序的习惯里,处理这类任务时总是会先花一点时间确认输入文件的真实结构,再动手写代码。这个过程看起来很笨,但恰恰是它帮我避开了大量"看起来对、实际错"的尴尬情况。如果你正在学编程,我强烈建议你也养成这个习惯:拿到任何数据文件的第一件事,不是写代码,而是先把它打开看一眼。真正的高手不是记的语法多,而是踩过的坑多,并且知道怎么绕开。这道题做完之后,你可以顺手把输入文件改成几千行数据试试,看看程序跑起来是否依然稳定——那才是这道题真正留给你的检验标准。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进