英语学习类的毕设年年都有,但真正能把“智能”两个字落到实处的并不多。今天要拆解的这套“基于Python的智能英语学习系统”(源码包编号65438),属于那种看着不花哨、但功能完整、答辩稳的项目:用户注册登录、词库管理、记忆曲线复习调度、四种题型测试、错题收集、学习进度可视化一应俱全。项目用Python + Tkinter + SQLite实现,对刚接触Python的毕设党非常友好,对想做一个完整桌面项目的开发者来说,也是一套可以快速二次开发的模板。
这套系统最值得学的不是某一个炫技功能,而是“如何把一个需求完整地拆成模块、定好数据表、跑通闭环”。很多同学写毕设最大的问题不是代码不会写,而是不知道从哪里下手、表怎么建、模块怎么划分、复习逻辑怎么落地。这篇文章就按我当时开发这套系统的完整思路,从设计、建模、编码到踩坑复盘,全部过一遍。
1. 项目概述与整体设计思路
1.1 这个系统要解决什么问题
先说痛点。大学英语学习最常见的状态是:手机里装了三四个背单词App,真正坚持下来的没几个;单词背了又忘,忘了又背,完全靠临时突击;学习效果没有任何数据反馈,不知道自己这周进步了还是原地踏步。
这套系统把这些问题转成了几个具体功能点:
- 每日学习任务:不仅加入新单词,还能自动安排到期复习词,不用自己记着哪天该复习哪一批。
- 掌握度跟踪:每个单词都有自己的熟练度阶段,从“刚认识”到“长期记住”,状态全部入库。
- 测试闭环:背完立刻测,四种题型混合出题,答错的自动进错题本,后面可以单独重测。
- 可视化反馈:学习趋势折线图、正确率饼图、各模块完成度,一眼看到自己的学习情况。
也就是说,“智能”主要体现在三个地方:懂遗忘规律、懂用户薄弱点、懂学习数据。这个定位在做需求分析时非常好写,也方便答辩时讲清楚创新点。
1.2 技术选型:为什么是Python + Tkinter + SQLite
毕设技术选型有一条铁律:不要选你hold不住的技术栈。当时对比过三套方案:
| 方案 | 界面 | 数据库 | 部署复杂度 | 答辩风险 |
|---|---|---|---|---|
| Python + Tkinter + SQLite | 桌面GUI | 自带文件型数据库 | 极低,装Python即可跑 | 低,逻辑透明 |
| Python + Flask + MySQL | Web界面 | 需要装数据库服务 | 中,要启动服务 | 中,前端工作量翻倍 |
| Java Swing + MySQL | 桌面GUI | 需要装数据库服务 | 中,JDBC配置麻烦 | 中,环境易出问题 |
最终选了第一个组合,理由很实际:
Tkinter是Python标准库自带的GUI工具,不需要额外安装,打包时体型也可控。PyQt虽然界面更精致,但信号槽机制、资源编译对新手不友好,代码一复杂很容易把自己绕晕。毕设要的是“稳定跑起来”,不是“界面赛过商业软件”。
SQLite更是省心。它就是一个单文件数据库,不需要单独安装MySQL、不需要配置账号密码。代码里写好建表语句,第一次运行自动生成数据库文件。两台电脑演示也不用搬数据库,数据库文件拷过去就能用。
至于为什么不用Web方案,核心原因是答辩现场的不可控因素太多:端口被占用、浏览器缓存、前端样式错乱,任何一个问题都可能让演示卡壳。桌面版一个main.py点开就完事,风险最小。
1.3 功能结构总览
系统的功能模块按“学习闭环”来拆,一共六个:
| 模块 | 核心功能 | 对应数据表 |
|---|---|---|
| 用户模块 | 注册、登录、密码加密存储 | users |
| 单词模块 | 词库管理、浏览、按难度筛选 | words |
| 学习模块 | 今日新词+到期复习词调度 | user_word_status |
| 测试模块 | 四种题型抽题、判分、错题收集 | test_records, wrong_records |
| 生词本模块 | 错词重测、自动收集 | wrong_records |
| 统计模块 | 学习趋势、正确率、词量统计 | study_log |
架构上严格分了三层:界面层只负责展示和接收操作,业务层处理学习调度、抽题逻辑、统计计算,数据层只做SQL读写。这样拆的好处是出问题好排查,而且文档里“系统架构设计”那一章写起来非常漂亮。
2. 核心功能拆解与数据库建模
2.1 数据表设计与关系
数据库设计是这套系统最值得讲的部分。表建得好,后面所有逻辑都顺畅;表建得随意,学到一半就发现缺字段、要返工。
我当时建了6张表,核心结构如下:
CREATE TABLE users ( id INTEGER PRIMARY KEY AUTOINCREMENT, username TEXT UNIQUE NOT NULL, password_hash TEXT NOT NULL, created_at TEXT DEFAULT (datetime('now', 'localtime')) ); CREATE TABLE words ( id INTEGER PRIMARY KEY AUTOINCREMENT, word TEXT UNIQUE NOT NULL, phonetic TEXT, definition TEXT NOT NULL, example_sentence TEXT, difficulty INTEGER DEFAULT 3 ); CREATE TABLE user_word_status ( id INTEGER PRIMARY KEY AUTOINCREMENT, user_id INTEGER NOT NULL, word_id INTEGER NOT NULL, stage INTEGER DEFAULT 0, -- 记忆阶段 0~5 review_count INTEGER DEFAULT 0, last_review_date TEXT, next_review_date TEXT, is_favorite INTEGER DEFAULT 0, FOREIGN KEY (user_id) REFERENCES users(id), FOREIGN KEY (word_id) REFERENCES words(id) ); CREATE TABLE test_records ( id INTEGER PRIMARY KEY AUTOINCREMENT, user_id INTEGER NOT NULL, word_id INTEGER NOT NULL, question_type INTEGER NOT NULL, -- 1选择 2拼写 3英译汉 4汉译英 is_correct INTEGER NOT NULL, test_time TEXT DEFAULT (datetime('now', 'localtime')) ); CREATE TABLE wrong_records ( id INTEGER PRIMARY KEY AUTOINCREMENT, user_id INTEGER NOT NULL, word_id INTEGER NOT NULL, wrong_count INTEGER DEFAULT 1, last_wrong_time TEXT, UNIQUE(user_id, word_id) ); CREATE TABLE study_log ( id INTEGER PRIMARY KEY AUTOINCREMENT, user_id INTEGER NOT NULL, study_date TEXT NOT NULL, new_words INTEGER DEFAULT 0, review_words INTEGER DEFAULT 0, total_time INTEGER DEFAULT 0 );几个容易忽略的设计细节:
user_word_status是用户和单词的关联表,这是学习进度的核心。每个用户对每个单词都有自己的状态,不能直接改words表,否则就是所有用户共用一套进度了。stage字段记录了当前记忆阶段,从0到5,配合复习间隔算法使用。wrong_records用了UNIQUE(user_id, word_id)约束,同一个单词只保留一条错题记录,错次数累加。这样做统计的时候非常方便。- 所有时间字段统一用文本格式YYYY-MM-DD,虽然不如时间戳精确,但阅读和调试都方便,毕设完全够用。
2.2 记忆复习调度原理
这是整套系统“智能”的核心,也是答辩时最有说头的地方。原理来自艾宾浩斯遗忘曲线:人脑遗忘不是匀速的,学完20分钟后会忘掉约42%,一小时后忘掉约56%,之后遗忘速度逐渐变慢。所以复习安排应该“先密后疏”。
落地到代码里,就变成一组递增的复习间隔:
REVIEW_INTERVALS = [1, 2, 4, 7, 15, 30] def calc_next_review_date(stage): """根据当前记忆阶段,计算下一次复习日期""" if stage >= len(REVIEW_INTERVALS): return None # 已到最终阶段,进入长期记忆 return date.today() + timedelta(days=REVIEW_INTERVALS[stage])每次复习单词时:
- 答对了,
stage加1,下一次复习间隔拉长。 - 答错了,
stage减1(最低不减到负数),间隔缩短,增加复习频率。 - 一个单词走完0到5六个阶段,大约跨越59天,基本能刻进长时记忆。
这套调度逻辑实现起来不到50行,但价值和解释力远超标。它让你能在答辩时说清楚:“我不是简单做了一个背单词工具,而是引入了认知心理学经典的遗忘曲线模型,并用阶段式调度把它工程化了。”
2.3 智能测试的生成策略
测试模块不能简单地随机出几个单词。我做了四类题型:
- 选择题:给英文单词,四个中文释义里选正确项。
- 拼写题:显示中文释义,用户输入英文拼写。
- 英译汉:给英文句子,选正确中文含义。
- 汉译英:给中文语义,选对应的英文单词。
干扰项生成是里面最有技巧的:如果纯随机抽三个词做干扰项,会出现“正确选项很简单、干扰项离谱”的情况,测试变得没有区分度。我当时的做法是:
import random def generate_choices(word_id, words_pool, count=4): """生成选择题选项,确保干扰项与正确项难度接近""" correct_word = [w for w in words_pool if w['id'] == word_id][0] # 优先从同难度级别的词里选干扰项,避免一眼排除 same_level = [w for w in words_pool if w['id'] != word_id and abs(w['difficulty'] - correct_word['difficulty']) <= 1] if len(same_level) >= count - 1: distractors = random.sample(same_level, count - 1) else: distractors = random.sample([w for w in words_pool if w['id'] != word_id], count - 1) choices = distractors + [correct_word] random.shuffle(choices) return choices同时测试完立刻判分、立刻写库。答错的词自动进错题本,下一次学习界面会优先把错题排在前面。这样做的好处是“学-测-补”形成一个闭环,系统会持续关注用户最薄弱的单词。
3. 从零实现:关键代码与实操步骤
3.1 环境准备与项目目录规划(新手照抄版)
本地开发环境我只用了最朴素的组合:Python 3.9以上版本、Visual Studio Code或者PyCharm Community版。
安装第三方库只需要一行命令,我建议放到虚拟环境里:
python -m venv venv venv\Scripts\activate # Windows系统激活虚拟环境 pip install matplotlibmatplotlib是唯一必须装的第三方库,用于画统计图。Tkinter是Python自带,sqlite3模块也是标准库,都不需要额外安装。
项目目录我习惯按分层结构组织:
english_learning_system/ ├── main.py # 程序入口 ├── core/ │ ├── __init__.py │ ├── database.py # 数据库连接和建表 │ ├── auth_service.py # 登录注册 │ ├── study_service.py # 学习调度 │ ├── test_service.py # 测试生成与判分 │ └── stats_service.py # 统计计算 ├── ui/ │ ├── __init__.py │ ├── login_window.py │ ├── main_window.py │ ├── study_page.py │ ├── test_page.py │ └── stats_page.py ├── data/ │ ├── words.db # SQLite数据库(首次运行自动生成) │ └── wordbook.csv # 词库原始数据 └── requirements.txt这个结构的好处是:数据层代码永远不会混进界面代码。后期如果要换Web界面,core部分基本不用动,改ui层就行,这套减法在论文里也很好写。
3.2 登录注册模块:密码不能存明文
登录注册是每个系统都有的功能,但很多毕设直接明文存密码,答辩时一问“密码安全怎么考虑”就卡壳了。其实加密存密码非常简单,用hashlib就能完成:
import sqlite3 import hashlib import os class AuthService: def __init__(self, db_path): self.db_path = db_path def _get_conn(self): conn = sqlite3.connect(self.db_path) conn.row_factory = sqlite3.Row return conn def _hash_password(self, password, salt=None): """加盐哈希:即使两个用户密码相同,哈希值也不一样""" if salt is None: salt = os.urandom(16).hex() digest = hashlib.sha256((salt + password).encode()).hexdigest() return salt, digest def register(self, username, password): conn = self._get_conn() try: salt, digest = self._hash_password(password) conn.execute( "INSERT INTO users (username, password_hash) VALUES (?, ?)", (username, f"{salt}${digest}") ) conn.commit() return True except sqlite3.IntegrityError: return False # 用户名重复 finally: conn.close() def login(self, username, password): conn = self._get_conn() row = conn.execute( "SELECT password_hash FROM users WHERE username = ?", (username,) ).fetchone() conn.close() if row is None: return False salt, digest = row["password_hash"].split("$") _, new_digest = self._hash_password(password, salt) return digest == new_digest有几个细节值得注意:
- 密码哈希值即使泄露也无法还原出原始密码。
- 加盐的目的是防止“彩虹表攻击”,也就是攻击者预计算出常用密码的哈希表直接比对。
- 数据库连接用短连接,用完即关,避免桌面程序长时间占用连接导致数据库被锁。
答辩时如果被问“数据库安全性怎么保证”,你就可以讲哈希、加盐、防暴力破解的思路,这已经超出很多同水平毕设的准备了。
3.3 单词学习与测试模块实现
学习页面的核心逻辑是获取“今日待学词列表”。这个词表由两部分组成:新词和到期复习词。
class StudyService: def __init__(self, db_path, user_id): self.db_path = db_path self.user_id = user_id def get_today_words(self, new_word_count=8): """获取今日学习列表:先排错题,再排到期复习词,最后补新词""" conn = sqlite3.connect(self.db_path) conn.row_factory = sqlite3.Row today = str(date.today()) # 1. 错题优先,最多5个 wrong_words = conn.execute(""" SELECT w.* FROM wrong_records wr JOIN words w ON w.id = wr.word_id WHERE wr.user_id = ? ORDER BY wr.wrong_count DESC LIMIT 5 """, (self.user_id,)).fetchall() # 2. 到期复习词 due_words = conn.execute(""" SELECT w.* FROM user_word_status uws JOIN words w ON w.id = uws.word_id WHERE uws.user_id = ? AND uws.next_review_date IS NOT NULL AND uws.next_review_date <= ? ORDER BY uws.next_review_date ASC LIMIT ? """, (self.user_id, today, new_word_count)).fetchall() # 3. 新词补足 known_ids = [r["id"] for r in wrong_words] + [r["id"] for r in due_words] placeholders = ",".join("?" * len(known_ids)) new_words = conn.execute(f""" SELECT * FROM words WHERE id NOT IN ({placeholders}) ORDER BY difficulty ASC LIMIT ? """, known_ids + [new_word_count]).fetchall() conn.close() return wrong_words, due_words, new_words这段代码体现了“错题优先 -> 复习到期 -> 新词补充”的调度优先级,这也是“智能”体验的直接体现。用户在界面上点“认识”“模糊”“不认识”,对应的处理逻辑分别是:stage加1、stage不变、stage减1,同时更新复习日期。
测试模块的核心是拼写题判分逻辑,需要处理大小写和前后空格:
def check_spelling(user_answer, correct_word): """拼写判分:忽略大小写和首尾空格""" return user_answer.strip().lower() == correct_word.strip().lower()其余三种题型用上面说到的选项生成函数,最后提交时统一判分并写test_records表。
3.4 学习统计可视化:中文乱码是必经之路
统计页面用matplotlib绘图,然后嵌入Tkinter窗口。嵌入方式是用matplotlib自带的Tkinter后端:
import matplotlib.pyplot as plt from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg def draw_study_trend(parent_frame, user_id): """在Tkinter中绘制近30天学习趋势折线图""" # 解决中文显示问题 plt.rcParams["font.sans-serif"] = ["SimHei"] plt.rcParams["axes.unicode_minus"] = False dates, new_counts, review_counts = stats_service.get_daily_data(user_id) fig, ax = plt.subplots(figsize=(6, 4)) ax.plot(dates, new_counts, marker="o", label="新学单词") ax.plot(dates, review_counts, marker="s", label="复习单词") ax.set_xlabel("日期") ax.set_ylabel("单词数量") ax.set_title("近30天学习趋势") ax.legend() ax.xaxis.set_major_locator(plt.MaxNLocator(8)) # x轴标签太多会重叠 canvas = FigureCanvasTkAgg(fig, master=parent_frame) canvas.draw() canvas.get_tk_widget().pack(fill="both", expand=True)踩坑点提前说一下:
- matplotlib在Windows下默认字体是英文,不设置
font.sans-serif的话图里的中文全是方块。 axes.unicode_minus必须设成False,否则负号会显示成方块。- x轴日期太密集时用
MaxNLocator限制刻度数量,否则整个图糊成一团。
同理还可以绘制单词类型占比饼图、每轮测试正确率柱状图。统计页是答辩演示的高光环节,数据图一出一看就上档次。
4. 实操复盘:典型问题与排查技巧
4.1 数据库并发与路径问题
开发过程中遇到最典型的两个问题,第一个是SQLite报“database is locked”。原因是桌面程序频繁打开多个窗口操作数据库,如果一个写操作卡住了,另一个连接就会抢锁失败。解决办法是我把所有写库操作改成短连接,并且用事务提交:
def record_test_result(self, user_id, word_id, question_type, is_correct): conn = sqlite3.connect(self.db_path) with conn: # 用with块自动管理事务 conn.execute( "INSERT INTO test_records (user_id, word_id, question_type, is_correct) \ VALUES (?, ?, ?, ?)", (user_id, word_id, question_type, is_correct) ) conn.close()第二个问题是相对路径bug。如果代码用了open("words.db")这种相对路径,双击源码运行时工作目录不同,数据库文件会被创建到意想不到的位置。我的处理方式是用Path(__file__)定位到项目根目录,再拼出数据库绝对路径:
from pathlib import Path BASE_DIR = Path(__file__).resolve().parent.parent DB_PATH = BASE_DIR / "data" / "words.db"这样不管在哪个目录启动程序,数据库都稳稳定定在项目data文件夹里。
4.2 Tkinter界面卡顿与布局问题
Tkinter的派克布局(pack/grid)有个规矩:同一个父容器里不能混用两种布局方式。我有一次在主窗口里pack了侧边栏,又用grid放统计图,结果按钮歪得看不出是按钮。这是一个新手必踩坑,答案也很简单:一个容器只选一种布局,如果确实需要表格网格,再用Frame拆一层。
另一个问题是事件回调的变量引用陷阱。在循环里创建多个按钮,直接写command=self.delete_word(word_id),括号一加,函数在创建时就被调用了,点击按钮反而没反应。正确写法是用lambda参数绑定:
btn = tk.Button(..., command=lambda wid=word_id: self.delete_word(wid))初始化阶段这个bug排查了快一个小时,就是因为没注意Python闭包的延后绑定特性。
界面卡顿则几乎都与循环里做了耗时操作有关。比如给Treeview控件插入几百行数据时,用循环逐行insert会很慢,正确做法是拼成数组一次性insert_all_item。如果还需要响应式刷新,记得用root.after()而不是time.sleep(),后者会把整个界面冻结。
4.3 PyInstaller打包与运行环境
答辩通常要求在别人电脑上也能运行,这就绕不开打包。我用的PyInstaller:
pip install pyinstaller pyinstaller -F -w -i assets/icon.ico main.py参数含义:
-F打成单文件exe-w不显示控制台黑窗口-i指定图标
打包后最容易踩的坑是运行时报“No module named matplotlib.backends.backend_tkagg”。明明本地运行好好的,打包完就缺模块。这是因为PyInstaller静态扫描时漏掉了matplotlib动态导入的后端。解决办法有两种:
- 在代码里显式
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg,让扫描器能检测到。 - 不行就在打包命令里加:
pyinstaller -F -w --hidden-import matplotlib.backends.backend_tkagg main.py还有一个小经验:打包出来的exe体积一般在30到50MB,主要是matplotlib拖大的。如果实在介意体积,可以把统计图的matplotlib换成用FigureCanvasTkAgg之外的方式,或者改用PIL画简单柱状图,但我个人觉得为几十MB去折腾不值,稳定优先。
4.4 答辩与演示实用建议
源码包编号65438对应的这套系统,答辩时长一般控制在8到10分钟。我的演示顺序建议是:
- 注册新账号(30秒,展示UI完整)。
- 进入学习页,完成3到5个单词学习(展示调度逻辑入口)。
- 快速做一轮测试,故意答错一题(展示判分、错题收集)。
- 切到生词本,展示刚刚的错题已经自动出现(这里一步就能说明系统的数据联动)。
- 打开统计页,展示趋势图和正确率图(重点停留,图最能撑场面)。
演示用的账号建议提前跑一周数据,比如每天学习8个新词、复习20个词。答辩当天统计页会有一整页漂亮的曲线,而不是今天的孤零零几个点。
关于答辩提问,最常被问到的几个我提前备了答案:
- 为什么用SQLite不用MySQL?答:面向单用户、免部署、数据文件可迁移,原型阶段压缩环境成本。
- 记忆曲线怎么实现的?答:阶段递增复习间隔,0到5共6个阶段,答错回退。
- 系统能不能扩展?答:core层与ui层分离,更换Web界面无需改业务逻辑,词库可批量导入。
整理下来,这套系统代码量不大,核心逻辑加起来也就一千多行,但是模块清晰、闭环完整、有算法支撑,作为本科毕设属于性价比很高的一档。
源码包编号65438里已含词库、数据库初始化脚本和完整项目代码。我个人建议拿到源码后不要直接提交,至少改一个小地方,比如把词库换成四六级核心词表,或者加个“每日打卡提醒”的小按钮。一个小小的个性化改动,就能让答辩老师在源码检查时第一眼看到你的工作痕迹,而不只是从网上下载的模板。