恒美微站
首页
关于我们
建站服务
主题模板
案例展示
资讯中心
联系我们
Python文件处理实战:学生成绩读取、平均分计算与排序写入
首页
资讯中心
/
Python文件处理实战:学生成绩读取、平均分计算与排序写入
Python文件处理实战:学生成绩读取、平均分计算与排序写入
发布时间:2026/10/16 17:43:35
这道题看着简单——读文件、算平均分、排序、写新文件四步走完好像就结束了。但真正动手做的时候你会发现凡是能把这一步做得干净利落的人前五道题的基础一定很扎实。作为一个带过不少入门者、也改过大量作业的人我很清楚这类题目真正的价值不在能跑出结果而在于你有没有把一个数据处理的完整链路打通。下面我把整道题的拆解思路、实现方式、以及那些不写进答案里的坑一次性讲清楚。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, encodingutf-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 文件里可能出现哪些坏数据我见过不少同学在读取环节翻车不是语法不会而是没预料到数据本身的意外情况。最常见的四类空行文件末尾或者中间不小心多了一个空行直接split()会得到空列表取parts[0]就崩溃。字段缺失某一行的成绩少写了一个parts[2:]得到的成绩数量不一样后面算平均分时分子分母不匹配。多余空格行首行尾有空格如果不做strip()第一个字段可能会带前导空格。编码问题文件是用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(keylambda s: s[avg])按平均分降序从高到低这个通常更符合排名的习惯students.sort(keylambda s: s[avg], reverseTrue)如果你用的是列表的列表结构比如[学号, 姓名, 成绩列表]那就需要从嵌套结构里取索引students.sort(keylambda s: s[2])4.2sort()和sorted()到底用哪个这是入门阶段最容易被问到的区别list.sort()原地排序直接修改原列表返回None更省内存。sorted(list)返回一个新的排序后列表原列表不变。在我们这个场景里数据读取完成后后续所有操作都用同一个列表没有什么需要保留原顺序的理由所以用sort()就够了。但如果你是从只读数据源获取数据后面还想保留原始顺序做别的分析那就用sorted()。我见过不少同学在这道题里纠结半天最后发现两个都对只是使用场景不同。这种纠结本身其实是好事说明你在思考工具的适用边界。4.3 平均分相同怎么办——排序稳定性在起作用如果两个学生的平均分恰好一样最终谁排在前面这里就涉及到排序算法的稳定性。Python的sort是稳定排序意思是当两个元素的排序键相等时它们之间的相对顺序保持原文件中的顺序。这意味着如果你先用学号排序再按平均分排序那么平均分相同的学生会按照学号有序排列。如果你希望平均分相同时按姓名排、或者按语文成绩排可以一次指定多个键students.sort(keylambda s: (s[avg], s[scores][0]), reverseTrue)注意这里reverseTrue会同时反转两个键的升序方向也就是说语文成绩也是从高到低。如果你想平均分降序语文成绩升序那就不能直接传reverse得用负值技巧或者多次排序# 平均分降序语文成绩升序 students.sort(keylambda s: s[scores][0]) # 先按语文升序 students.sort(keylambda s: s[avg], reverseTrue) # 再按平均分降序这个技巧在职场上很常用叫多次排序法利用稳定排序的特性后排序的键优先级更高。5. 写入stu_sort文件格式设计与可读性兼顾5.1 输出格式别让排序结果变成一坨难读的字符串写文件的代码很简短但格式设计却值得想想。你需要决定每条记录还是不是学号 姓名 成绩1 成绩2 成绩3 平均分平均分放到最后还是紧跟姓名字段之间用空格还是制表符要不要写入表头我推荐的方案和原文件保持相同的字段分隔方式平均分追加在末尾。这样做的最大好处是后面如果还有下一题比如读取stu_sort再统计你的解析代码可以复用不用为两种格式分别写逻辑。5.2 写入文件的基本写法with open(stu_sort.txt, w, encodingutf-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下写出的文件用记事本打开是乱码这是几乎所有初学者都会遇到的经典问题。原因很简单你用encodingutf-8写文件而Windows记事本默认用GBK解码。解决办法有两个一是换成GBK编码写入with open(stu_sort.txt, w, encodinggbk) as f:二是保持UTF-8写入但在文件开头写入BOM字节序标记让记事本自动识别编码。Python里可以用encodingutf-8-sig它会自动加上BOM头。with open(stu_sort.txt, w, encodingutf-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, encodingutf-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, encodingutf-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(keylambda s: s[avg], reverseTrue) 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。处理方式是先用记事本或编辑器确认文件编码然后把打开参数改成对应编码比如encodinggbk。第三个是ValueError: could not convert string to float某一行成绩字段里混入了非数字内容。这时候程序会直接中断你需要加上异常处理或者先打印出那一行内容排查。第四个是KeyError如果你给字典取了别的键名比如average后面排序又用了avg那就会报错。这类问题最好的预防办法是字典的键名保持一致并且避免拼写太接近的多个变体。第五个是排序结果和预期不一致。很多人以为降序是从小到大但reverseTrue是从大到小。这类逻辑错误不会报错但结果就是错的。遇到这种情况先把排序结果打印出来人工核对一遍再写文件。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(平均分, ascendingFalse)就能完成排序。但我不建议在初学阶段直接用pandas来完成这道题——因为文件操作和基础数据结构的练习价值会被掩盖掉。写在最后的一点个人体会这种读文件、算数据、排序、写文件的小练习我前前后后看过太多人做了。有的十分钟交差有的折腾两小时还卡在编码问题上。说实话这道题的重点从来不是排序函数怎么调而是你有没有建立起一种意识程序处理的数据是有来源、有格式、有异常可能的现实数据代码要对这些情况负责。在我自己写程序的习惯里处理这类任务时总是会先花一点时间确认输入文件的真实结构再动手写代码。这个过程看起来很笨但恰恰是它帮我避开了大量看起来对、实际错的尴尬情况。如果你正在学编程我强烈建议你也养成这个习惯拿到任何数据文件的第一件事不是写代码而是先把它打开看一眼。真正的高手不是记的语法多而是踩过的坑多并且知道怎么绕开。这道题做完之后你可以顺手把输入文件改成几千行数据试试看看程序跑起来是否依然稳定——那才是这道题真正留给你的检验标准。