ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Python学生成绩管理系统实战:从SQLite增删改查到MySQL迁移

Python学生成绩管理系统实战:从SQLite增删改查到MySQL迁移 写“学生成绩管理系统”大概是每个Python入门者都会碰到的一个练手项目。说直白点核心就是四个字增删改查。往一张表里录入学生成绩、按学号或姓名把成绩查出来、把录错的分数改掉、把重复或退学留下的记录删掉——这套流程跑通你就算是把大多数管理类软件的底子摸了一遍。不过照着网上的demo把代码敲出来能跑和真正把一个系统做到“会用”中间差了不止一点。数据存在哪、量大了会不会变慢、输入垃圾数据能不能拦住、控制台输出看着费不费劲、过半个月还能不能看懂自己写的代码这些才是更值得琢磨的问题。我这次就用Python写一遍学生成绩管理系统从选型到建库从功能实现到踩坑记录完整摊开讲。适合刚学完Python基础想找项目练手的同学也适合急着交课程设计、需要一套能跑能交差的完整代码的人。1. 项目定位与整体思路拆解1.1 为什么选Python做这类管理系统很多人纠结用什么语言写管理系统我自己的想法很直接如果一个项目要在几个小时内从零跑到能用Python的性价比最高。标准库里就带了sqlite3、json、csv这些数据模块不需要额外装数据库、配环境装了Python直接跑代码量也少同样的增删改查逻辑用Java或C#写出来可能动辄几百行Python版本一百行上下足够。成绩管理这种体量强类型、多线程、高并发的需求都不存在Python完全罩得住。当然Python被吐槽最多的一点是运行慢但请你冷静想想一个班级几十人、一门课几十次成绩登记就算一秒处理一条一共也就几千条SQLite写起来嗖嗖的。真正让系统没法用的从来不是慢而是逻辑混乱、数据存了就丢。工作后我见过不少用Python做的小工具能稳定用几年的普遍都有一个共同点结构清楚、数据和代码分离、异常处理到位。这套思维正好可以通过这个成绩管理项目练出来。1.2 先搞清楚“增删改查”到底在操作什么数据动手写代码前我建议先把数据模型想清楚不然写着写着就发现字段对不上。学生成绩系统最核心的数据无非这几个要素学号、姓名、科目、成绩。如果用一张表来装那么一行代表“某个学生某门课的一次成绩”。这里有个细节很多人第一次会忽略一个学生有语文、数学、英语多门课所以光用学号当唯一标识是不行的学号科目才是这条记录的唯一标识。比如学号“2024001”可以同时存在“数学”和“语文”两条记录但同一个学号同一个科目只能有一条。这个约束在建表时就应该定好否则用户重复录入也没人提醒数据乱成一锅粥。那增删改查在我们的项目里分别对应什么操作用SQL的视角列一下增加向表里插入一条成绩记录。查询按学号或姓名查看成绩或列出全部记录。修改找到指定学号、指定科目的记录更新成绩。删除删除指定学号、指定科目的记录。后面的实现全部围绕这个逻辑展开。虽然听起来简单但每个动作都有很多边界情况要考虑学号不存在时怎么提示成绩填了“abc”怎么办重复录入同一门课怎么处理下面我会一步步把这些坑填上。2. 环境准备与存储方案选择2.1 装好Python和编辑器就够了如果你是完全从零开始先从python.org下载安装包安装时记得勾选“Add Python to PATH”避免后面命令行找不到python命令。装完在终端敲python --version能输出版本号就代表环境OK。编辑器方面我个人推荐VSCode装一个Python扩展就行写代码、运行、调试三个动作都能在编辑器里完成对新手友好当然你用PyCharm也完全没问题选顺手的最重要。Windows用户如果跑这个项目时发现控制台打印中文变成一堆乱码通常不是代码的问题而是Windows控制台默认编码和Python输出的UTF-8不一致。有两个办法一是在运行窗口先执行chcp 65001切到UTF-8二是写代码时加一句sys.stdout.reconfigure(encodingutf-8)。我建议直接用第二种省得每次手动切。我最初写这类项目时也被乱码折磨了半天后来发现就是这一行代码的事。2.2 存储方案数据放哪决定系统靠不靠谱数据存哪是这类项目最关键的选型。我见过很多新手直接用“全局列表字典”存数据代码跑起来很顺但程序一关所有数据全没了。练手可以真要演示或交作业必须做持久化。常见的持久化方案有三个纯文本/CSV结构简单Excel能打开但查询、修改、去重都得自己写逻辑数据一多容易出错。JSON文件Python自带json模块读写方便适合保存几百条记录的小数据。缺点是修改时通常要全量写回数据量大了效率不高。SQLite数据库一个单文件数据库Python内置sqlite3模块不需要安装独立服务支持SQL语句查重、聚合、事务都现成。我这次选SQLite。原因很实在它让增删改查的逻辑回归到了SQL而SQL语法在这里学到的东西将来迁移到MySQL、PostgreSQL还能复用同时单文件随时备份拷走就能带走。对于这个项目SQLite就是“性价比天花板”。如果你想直接上MySQL思路在第六章我会单独聊连接层换掉就行业务逻辑基本不用动。2.3 建库建表先定结构再谈功能我用sqlite3模块来建表。先看代码再解释每个细节import sqlite3 DATABASE scores.db def get_conn(): 获取数据库连接项目里所有函数都通过它操作数据库 conn sqlite3.connect(DATABASE) return conn def init_db(): 初始化数据库表结构程序启动时调用一次 conn get_conn() conn.execute( CREATE TABLE IF NOT EXISTS scores ( id INTEGER PRIMARY KEY AUTOINCREMENT, student_id TEXT NOT NULL, name TEXT NOT NULL, subject TEXT NOT NULL, score REAL NOT NULL, created_at TEXT DEFAULT (datetime(now, localtime)) ) ) conn.execute( CREATE UNIQUE INDEX IF NOT EXISTS idx_unique ON scores(student_id, subject) ) conn.commit() conn.close()几点说明id是自增主键用来唯一标识每一条记录。虽然我们说学号科目是业务上的唯一标识但数据库里有自增id删除、修改时用id定位更稳妥。score字段用REAL类型也就是浮点数允许85.5这样的分数整门课如果只允许整数用INTEGER也行但要记得在录入层做校验。created_at记录录入时间默认取本地时间。它不参与业务逻辑但排查问题时很有用——比如发现某条成绩录错了可以看录入时间还原现场。UNIQUE索引对(student_id, subject)做联合唯一约束这能从根本上防止“同一个学生同一门课录了两遍”。有了这层约束就算程序里漏了检查数据库也会兜底报错。如果你的需求更规范一点可以把学生信息和成绩分成两张表学生表存学号、姓名成绩表只存学号、科目、分数通过外键关联。这样做的好处是改学生姓名只改一处不会出现同一个学生在成绩表里有三个名字的尴尬。但对一个入门级成绩系统来说一张表足够我下面所有功能都基于这个单表结构。3. 核心功能实现从“能跑”到“好用”手把手写增删改查3.1 先设计程序骨架菜单循环函数分装我写这类交互式程序习惯先把整体骨架搭出来再逐个填充功能。这样能保证每个功能都是独立函数出问题好定位测试也好测。def main(): init_db() while True: show_menu() choice input(请选择操作).strip() if choice 1: add_score() elif choice 2: query_score() elif choice 3: update_score() elif choice 4: delete_score() elif choice 5: export_csv() elif choice 0: print(已退出系统) break else: print(无效选项请重新输入) def show_menu(): print(\n * 40) print(学生成绩管理系统) print(1.录入成绩 2.查询成绩) print(3.修改成绩 4.删除记录) print(5.导出数据 0.退出系统) print( * 40) if __name__ __main__: main()骨架的重点是“主循环”和“菜单”分离。菜单写在单独函数里以后加功能只需要在菜单里加一行、在主循环里加一个分支即可。别小看这个习惯很多人的程序到后期改不动就是因为所有逻辑全塞在while True里加一个功能要翻半天代码。3.2 录入成绩三个防线挡住八成垃圾数据录入是用户操作最多、也最容易出错的地方。我平时写新增功能一定会在程序入口做输入校验目标是空字段、非法成绩、重复记录全部在进入数据库前拦住。代码里有三层防线def _record_exists(conn, student_id, subject): cur conn.execute( SELECT id FROM scores WHERE student_id ? AND subject ?, (student_id, subject), ) return cur.fetchone() is not None def add_score(): print(\n--- 录入成绩 ---) student_id input(请输入学号).strip() name input(请输入姓名).strip() subject input(请输入科目).strip() score_str input(请输入成绩0-100).strip() if not all([student_id, name, subject]): print(学号、姓名、科目都不能为空) return try: score float(score_str) except ValueError: print(成绩必须是数字) return if not (0 score 100): print(成绩必须在0到100之间) return conn get_conn() try: if _record_exists(conn, student_id, subject): print(该学生这门课的成绩已存在请使用修改功能) return conn.execute( INSERT INTO scores (student_id, name, subject, score) VALUES (?, ?, ?, ?), (student_id, name, subject, score), ) conn.commit() print(录入成功) finally: conn.close()第一道防线是空值检查。我用了input后紧跟的strip()把首尾空格去掉否则用户手滑多打个空格学号就可能查不到。第二道防线是用float转换成绩顺手让“abc”“12分”这类输入直接暴露最后再用0到100的范围约束把平时抄写成绩常见的笔误也拦住。第三道防线就是查重调用_record_exists先查一遍重复就停止录入。我自己写这类函数时还特别注意一点所有输入都先经过处理再入库绝不在数据库里修数据。另外修改数据之后一定要connect.commit()否则数据只在内存里程序一崩就丢了我用finally里的conn.close()做清理也是防止连接泄漏。3.3 查询成绩按学号查、按姓名查、全部列出查询功能是整个系统里用得最频繁的我把它做成一个入口、三种方式按学号精确查询、按姓名模糊查询、查看全部记录。这样既满足“老师点名看成绩”的场景也方便学生自己查。def query_score(): print(\n--- 查询成绩 ---) print(1.按学号查询 2.按姓名查询 3.查看全部) mode input(请选择查询方式).strip() conn get_conn() try: if mode 1: sid input(请输入学号).strip() rows conn.execute( SELECT student_id, name, subject, score FROM scores WHERE student_id ?, (sid,), ).fetchall() elif mode 2: keyword input(请输入姓名关键字).strip() rows conn.execute( SELECT student_id, name, subject, score FROM scores WHERE name LIKE ?, (f%{keyword}%,), ).fetchall() else: rows conn.execute( SELECT student_id, name, subject, score FROM scores ORDER BY student_id, subject ).fetchall() if not rows: print(没有找到匹配的成绩记录) return print(\n学号 姓名 科目 成绩) print(- * 40) for sid_val, name_val, subject_val, score_val in rows: print(f{sid_val:10}{name_val:6}{subject_val:6}{score_val:6.1f}) print(f共查到 {len(rows)} 条记录) finally: conn.close()这里有两个细节值得解释。一是LIKE模糊查询用通配符%拼起来这样输入“张”就能查到“张三”“张四五”等人体验比精确匹配好很多。二是输出时我用f-string的格式控制符比如{name_val:6}表示左对齐并占6个字符宽度。这样在控制台里列会整齐数据一多也不会乱成一片。成绩显示用{score_val:6.1f}固定保留一位小数界面会干净不少。关于按学号查询的值来源我用的是精确匹配。如果担心用户输入学号时不带前导0那就在录入和查询时都统一用字符串不要图省事存成数字。3.4 修改成绩先定位再更新最后确认结果修改功能是最容易“改了却好像没改”的地方。我推荐的做法是先查出来确认存在再做更新最后看更新行数。这样用户不会以为操作成功了实际上条件写错导致0行变化。def update_score(): print(\n--- 修改成绩 ---) student_id input(请输入学号).strip() subject input(请输入科目).strip() conn get_conn() try: cur conn.execute( SELECT student_id, name, subject, score FROM scores WHERE student_id ? AND subject ?, (student_id, subject), ) row cur.fetchone() if not row: print(没有找到这条成绩记录无法修改) return sid, name, subj, old_score row print(f当前记录{sid} {name} {subj} {old_score}) score_str input(请输入新的成绩).strip() try: new_score float(score_str) except ValueError: print(成绩必须是数字) return if not (0 new_score 100): print(成绩必须在0到100之间) return cur conn.execute( UPDATE scores SET score ? WHERE student_id ? AND subject ?, (new_score, student_id, subject), ) conn.commit() if cur.rowcount 0: print(更新失败没有找到对应记录) else: print(成绩修改成功) finally: conn.close()这个函数的逻辑看着简单但几个取舍我要说明白。先查后改和直接UPDATE有什么区别直接UPDATE当然也能改但如果查不到记录数据库不会报错只是更新0行用户看到“修改成功”其实什么都没改。所以先SELECT一次把原成绩打印出来让用户确认目标没错再更新最后用rowcount判断影响行数。这算是一个很朴素的“操作前确认、操作后反馈”的流程放到生产环境里更规范的还会加权限控制、操作日志但思路是通的。另外我只允许改成绩不允许在这修改学号和姓名。理由也很简单学号、姓名是记录的“身份标识”如果允许随意改很容易改出重复记录或外键问题。真要调整学号就删掉旧记录重新录一条语义更清楚。3.5 删除记录先查后删加二次确认删除是最危险的操作误删了就得从备份里找回所以我坚持“先查到记录、打印给用户确认、再执行删除”的流程。def delete_score(): print(\n--- 删除记录 ---) student_id input(请输入学号).strip() subject input(请输入科目).strip() conn get_conn() try: cur conn.execute( SELECT student_id, name, subject, score FROM scores WHERE student_id ? AND subject ?, (student_id, subject), ) row cur.fetchone() if not row: print(没有找到这条成绩记录无需删除) return print(f准备删除{row[0]} {row[1]} {row[2]} {row[3]}) confirm input(确认删除吗输入y确认).strip().lower() if confirm ! y: print(已取消删除) return cur conn.execute( DELETE FROM scores WHERE student_id ? AND subject ?, (student_id, subject), ) conn.commit() print(f已删除 {cur.rowcount} 条记录) finally: conn.close()二次确认在命令行工具里容易被嫌烦但成绩数据不是随便能重建的这个确认步骤能挡掉很多“手滑”。如果你做的是一个正式一点的成绩系统还可以再加一层“软删除”给表加一个status字段默认1代表正常删除时改成0查询默认只查status1。软删除的好处是数据不真丢误删还能恢复坏处是每次查询都要带条件代码稍微复杂些。就这个小项目而言硬删除加备份就够了。3.6 输出排版与导出控制台也要有基本观感很多初学者不重视控制台输出查出来的数据挤成一团学生名都看不清。这其实不是功能问题是体验问题。我习惯在输出类函数里做三件事表头明确、分隔线清晰、统计结果附带说明。上面的查询函数已经用格式化做了列对齐可以说已经达到了“不丢人”的水平。如果你想把数据导出给别人看还可以加一个“导出到CSV文件”的功能。CSV用Excel打开非常方便我给一个极简实现def export_csv(): import csv conn get_conn() try: rows conn.execute(SELECT student_id, name, subject, score FROM scores).fetchall() with open(scores.csv, w, newline, encodingutf-8-sig) as f: writer csv.writer(f) writer.writerow([学号, 姓名, 科目, 成绩]) writer.writerows(rows) print(f已导出 {len(rows)} 条记录到 scores.csv) finally: conn.close()这里用encodingutf-8-sig而不是utf-8是为了让Excel打开CSV时不乱码。这种细节网上的教程通常不写遇到一次你就记住了。4. 常见问题与排查技巧实录4.1 数据写不进去多半是忘了commit我第一次写SQLite程序时踩过最经典的坑增删改查都执行了也没有报错但重启程序后数据没了。原因就一句话——忘记调用conn.commit()。SQLite默认在事务里执行操作不commit的话下次连接可能就回滚了。这个问题的排查思路也很标准检查执行完INSERT/UPDATE/DELETE之后有没有commit没有就先补上。4.2 中文乱码不是代码的锅在Windows命令行里运行这个系统很容易看到输出中文变问号或方块。原因刚才说过是编码不一致。解决方式统一为在程序入口加上reconfigure或者用utf-8-sig写导出文件。另外要注意代码文件本身最好存成UTF-8编码VSCode右下角能看到当前编码如果发现不是UTF-8就另存为转换一下。这能避免一种更隐蔽的乱码——代码里中文正常但输入中文查询时匹配不上。4.3 字段类型检查不严导致成绩排序错乱SQLite对类型的约束很宽松这一点很多人不习惯。比如INSERT时如果传入字符串“95”SQLite默认也会放进REAL字段里。表面看没问题等你想按成绩排序发现“9”排到了“15”后面因为字符串排序是按字典序来的。为避免这类问题我在录入层强制把输入转成float而不是依赖数据库去校验。所以我的建议是不要把校验工作全推给SQLiteSQLite的类型约束只能算最后一道兜底。4.4 数据被覆盖或误删恢复的姿势这问题没有完美的救命药预防最重要。我建议在main循环里加一个“备份数据库”的隐藏选项或者每次启动时自动备份一次。代码非常简单import shutil from datetime import datetime def backup_db(): backup_name fscores_backup_{datetime.now().strftime(%Y%m%d_%H%M%S)}.db shutil.copy(DATABASE, backup_name) print(f备份完成{backup_name})启动时调用一次backup_db()相当于每次操作前都有历史快照。真误删了直接拿备份文件替换回来。这样虽然粗暴但对这种单文件数据库极其有效。4.5 数据库文件被占用或损坏SQLite最常见的问题是写入时卡住或报“database is locked”。多半是连接没关或者程序异常退出导致锁没释放。所以我在每个函数里都坚持用finally关闭连接真遇到锁定重启程序一般能解决。至于损坏一般发生在写了一半磁盘满或程序被强杀不过SQLite有WAL日志机制绝大多数情况能自己恢复。当然备份永远是最好的保险。5. 从“能跑”到“像样”稳定性增强手段5.1 把配置收口别在代码里到处写死DATABASE scores.db这种常量如果散落在一堆函数里以后改名或迁移就会很痛苦。我习惯把所有环境相关的值放到文件顶部比如数据库路径、成绩范围、菜单文案、导出文件名统一管理。等你想把数据库从scores.db改成data/score.db只动一处就够了。5.2 用logging记录操作日志控制台的print只给人看程序真正的行为轨迹应该交给logging模块。我在正式一点的版本里会给每个增删改查操作加日志记录操作时间、类型、学号、科目以及成功与否。一开始你可能觉得没用等某一天有人喊“成绩被改错了”日志能帮你快速定位是谁、什么时间、改了哪里。虽然是命令行小工具但这套思路是通用的。5.3 给核心逻辑写个简单测试这类小项目很多人不愿意写测试觉得麻烦。但增删改查这种逻辑恰恰是最适合写测试的因为它不涉及复杂界面。我通常用Python内置的unittest或pytest对_record_exists这类纯函数写几个用例。比如插入重复记录是否抛错、成绩范围校验是否生效、删除不存在的记录是否返回0行。跑一遍测试比每次用手点一遍菜单快得多也能防止改了一个功能把另一个功能弄坏。5.4 异常要兜底但别随便except Exception新手喜欢在main外面套一个大大的try...except Exception觉得这样“怎么都不会崩”。但真正排查问题的时候这等于把所有错误信息吞了。我推荐的操作是在具体可能出错的位置捕获具体异常比如float转换用ValueError数据库操作用sqlite3.Error最外层可以加一个兜底但一定要打印完整错误信息方便排查至少让你知道是哪一行出的问题。6. 从SQLite换到MySQL一条SQL都没多学6.1 为什么还要提MySQLSQLite虽然好用但它有个天生限制单机、写并发差。如果你把成绩系统部署到服务器上让几十个老师同时录入成绩SQLite的“database is locked”会很快出现。这时候换MySQL或PostgreSQL是更稳的选择。好消息是前面学到的SQL语法在MySQL里基本都能直接用差别主要在连接方式和少量细节上。6.2 连接层替换把get_conn换掉就行为了让迁移更容易我建议一开始就把所有数据库操作封装在get_conn一个函数里。换MySQL时只需要把连接函数改成PyMySQL的写法import pymysql DATABASE_CONFIG { host: localhost, user: root, password: your_password, database: school, charset: utf8mb4, } def get_conn(): return pymysql.connect(**DATABASE_CONFIG)PyMySQL的用法和sqlite3非常像execute、fetchall、commit这些方法几乎同名所以之前的增删改查函数都能复用。需要先安装pymysqlpip install pymysql。先在MySQL里建好库school然后建表时注意两点AUTO_INCREMENT用法相似但字段类型和字符集默认InnoDB要按MySQL的规矩来日期默认值用CURRENT_TIMESTAMP。SQL占位符也有差异sqlite3用的是?MySQL的pymysql用%s。代码从SQLite迁过来时把SQL字符串里的?全部替换成%s就行。6.3 增删改查SQL速查对照表把常用SQL放在一张表里方便换库时对照操作SQLite / MySQL通用SQL占位符说明插入INSERT INTO scores (student_id, name, subject, score) VALUES (?, ?, ?, ?)sqlite用?MySQL用%s按学号查SELECT * FROM scores WHERE student_id ?同上模糊查询SELECT * FROM scores WHERE name LIKE ?参数传 %关键字%更新成绩UPDATE scores SET score ? WHERE student_id ? AND subject ?同上删除记录DELETE FROM scores WHERE student_id ? AND subject ?同上统计平均分SELECT AVG(score) FROM scores WHERE subject ?同上统计最高最低分SELECT MAX(score), MIN(score) FROM scores WHERE subject ?同上观察这张表你会发现核心SQL其实就是那五条增删改查语句再加一两条聚合查询。这些语句才是这个项目真正的“底层资产”Python代码写得好不好反而在其次。6.4 迁移时容易踩的三个坑迁移到MySQL时有几个坑我经常见人踩。一是大小写敏感MySQL的库名、表名在Linux下区分大小写Windows下不区分部署到服务器后容易踩雷。二是编码问题连接MySQL时一定要指定charset推荐utf8mb4否则中文写入可能变成“???”。三是自增主键MySQL的AUTO_INCREMENT无法用SQL在已有表上直接加建表时就要想好迁移时如果原数据有id要确保新表id列也定义成AUTO_INCREMENT插入时带id就没问题不带就自动生成。7. 一份可以直接复制运行的完整代码如果你不想跟着上面一节节拼我也把整个单文件版本整理在下面保存为score_system.py直接运行就能用。这段代码整合了前面所有功能菜单、增删改查、导出逻辑都收敛在一个文件里方便你快速跑起来。import sqlite3 import sys DATABASE scores.db sys.stdout.reconfigure(encodingutf-8) def get_conn(): return sqlite3.connect(DATABASE) def init_db(): conn get_conn() conn.execute( CREATE TABLE IF NOT EXISTS scores ( id INTEGER PRIMARY KEY AUTOINCREMENT, student_id TEXT NOT NULL, name TEXT NOT NULL, subject TEXT NOT NULL, score REAL NOT NULL, created_at TEXT DEFAULT (datetime(now, localtime)) ) ) conn.execute( CREATE UNIQUE INDEX IF NOT EXISTS idx_unique ON scores(student_id, subject) ) conn.commit() conn.close() def add_score(): print(\n--- 录入成绩 ---) student_id input(请输入学号).strip() name input(请输入姓名).strip() subject input(请输入科目).strip() score_str input(请输入成绩0-100).strip() if not all([student_id, name, subject]): print(学号、姓名、科目都不能为空) return try: score float(score_str) except ValueError: print(成绩必须是数字) return if not (0 score 100): print(成绩必须在0到100之间) return conn get_conn() try: cur conn.execute( SELECT id FROM scores WHERE student_id ? AND subject ?, (student_id, subject), ) if cur.fetchone(): print(该学生这门课的成绩已存在请使用修改功能) return conn.execute( INSERT INTO scores (student_id, name, subject, score) VALUES (?, ?, ?, ?), (student_id, name, subject, score), ) conn.commit() print(录入成功) finally: conn.close() def query_score(): print(\n--- 查询成绩 ---) print(1.按学号 2.按姓名 3.全部) mode input(请选择).strip() conn get_conn() try: if mode 1: sid input(请输入学号).strip() rows conn.execute( SELECT student_id, name, subject, score FROM scores WHERE student_id ?, (sid,), ).fetchall() elif mode 2: keyword input(请输入姓名关键字).strip() rows conn.execute( SELECT student_id, name, subject, score FROM scores WHERE name LIKE ?, (f%{keyword}%,), ).fetchall() else: rows conn.execute( SELECT student_id, name, subject, score FROM scores ORDER BY student_id, subject ).fetchall() if not rows: print(没有找到匹配的成绩记录) return print(\n学号 姓名 科目 成绩) print(- * 40) for sid_val, name_val, subject_val, score_val in rows: print(f{sid_val:10}{name_val:6}{subject_val:6}{score_val:6.1f}) print(f共 {len(rows)} 条记录) finally: conn.close() def update_score(): print(\n--- 修改成绩 ---) student_id input(请输入学号).strip() subject input(请输入科目).strip() conn get_conn() try: row conn.execute( SELECT student_id, name, subject, score FROM scores WHERE student_id ? AND subject ?, (student_id, subject), ).fetchone() if not row: print(没有找到这条成绩记录) return print(f当前{row[0]} {row[1]} {row[2]} {row[3]}) score_str input(请输入新的成绩).strip() try: new_score float(score_str) except ValueError: print(成绩必须是数字) return if not (0 new_score 100): print(成绩必须在0到100之间) return cur conn.execute( UPDATE scores SET score ? WHERE student_id ? AND subject ?, (new_score, student_id, subject), ) conn.commit() print(修改成功 if cur.rowcount else 没有记录被修改) finally: conn.close() def delete_score(): print(\n--- 删除记录 ---) student_id input(请输入学号).strip() subject input(请输入科目).strip() conn get_conn() try: row conn.execute( SELECT student_id, name, subject, score FROM scores WHERE student_id ? AND subject ?, (student_id, subject), ).fetchone() if not row: print(没有找到这条成绩记录) return print(f准备删除{row[0]} {row[1]} {row[2]} {row[3]}) if input(确认删除输入y确认).strip().lower() ! y: print(已取消) return cur conn.execute( DELETE FROM scores WHERE student_id ? AND subject ?, (student_id, subject), ) conn.commit() print(f已删除 {cur.rowcount} 条记录) finally: conn.close() def export_csv(): import csv conn get_conn() try: rows conn.execute( SELECT student_id, name, subject, score FROM scores ).fetchall() with open(scores.csv, w, newline, encodingutf-8-sig) as f: writer csv.writer(f) writer.writerow([学号, 姓名, 科目, 成绩]) writer.writerows(rows) print(f已导出 {len(rows)} 条记录到 scores.csv) finally: conn.close() def show_menu(): print(\n * 40) print(学生成绩管理系统) print(1.录入成绩 2.查询成绩) print(3.修改成绩 4.删除记录) print(5.导出数据 0.退出系统) print( * 40) def main(): init_db() while True: show_menu() choice input(请选择操作).strip() if choice 1: add_score() elif choice 2: query_score() elif choice 3: update_score() elif choice 4: delete_score() elif choice 5: export_csv() elif choice 0: print(已退出系统) break else: print(无效选项请重新输入) if __name__ __main__: main()这段代码跑起来就能实现最核心的增删改查我建议你先完整运行一遍再对照前面的章节理解每一部分。如果环境有问题比如python命令不好使先回去处理环境VSCode里直接点右上角三角形运行也可以。8. 一些越早知道越好的实操心得写这个系统写到最后我想分享几个并不在代码里、但对我影响很大的体会。第一先想清楚数据结构再动手。我把表结构定下来之后后面所有功能都顺了而以前我总是一边写一边加字段最后代码里全是补丁。第二参数化查询真的别省。所有用户输入都拼SQL字符串的话一旦输入里带了引号或特殊字符轻则报错重则被注入。用问号占位符或MySQL的%s把参数传进去安全又省事。第三复杂的不是增删改查本身而是那些边界情况。空值、重复数据、不存在的记录、没用过的菜单项把这些都考虑到程序才算真正能用。如果你把这套逻辑跑熟了下一步可以试着加一个tkinter图形界面或者把登录权限加进来甚至可以把它改造成一个Web API。数据结构不变核心增删改查不变变化的外层表现而已。项目的价值往往不在代码有多少行而在于它让你把“数据处理”这件事想明白了。
返回列表