Python背单词App实战:从SQLite存储到SM-2复习调度算法详解

发布时间:2026/9/16 19:13:47
Python背单词App实战:从SQLite存储到SM-2复习调度算法详解 简介这是一份基于PythonSqliteKivyVirtualenv实现的“51斩百词”背单词软件APP项目完整源码适合有一定Python基础、希望入门移动端开发或学习Kivy框架的学习者。项目围绕背单词应用的真实业务场景完整覆盖了界面搭建、数据存储、词汇管理、音频播放等功能模块可直接运行调试也能作为毕业设计或课程项目的参考范例。资源包共118个文件压缩包大小约86.15MB其中包含61个Python脚本、20个Kivy界面文件、多张图片与音频素材以及打包好的APK安装包、SQLite数据库和使用说明文档结构清晰便于对照学习和二次开发。目前该资源已有2556人学习浏览对于想通过实际项目掌握Python开发APP全流程的读者来说是一份难得的实战参考资料。1. 背单词软件不是界面问题是复习调度问题拿到一个“Python开发背单词软件APP”的题目大部分人会先去调界面、想按钮放在哪但真正拉开差距的是另一层东西单词该在哪天出现复习了几次还不熟要不要降级新词什么时候混进队列。界面只是把调度结果渲染出来核心价值在数据结构和状态转移上。这篇文章按一条能落地的链路来讲先定项目边界和APP框架选型再做SQLite词库与记忆状态表用简化版SM-2算法驱动复习节奏最后用Flet实现翻卡交互并解决打包成独立APP时最常见的几个问题。适合想从零跑通完整项目、也想知道Anki类工具内部调度逻辑的开发者对Python版本要求不高3.10以上即可。2. 定项目边界与Python选型数据层、调度层、界面层一次拆清2.1 最小可用背单词系统的四层组成做背单词APP先不要想着一次做全。一个能持续用、能迭代的最小系统按职责拆成四层词库层负责单词、释义、例句的存储和导入状态层记录每个单词的学习阶段、记忆因子、下次复习时间调度层决定下一次弹哪个词展示层负责渲染和收集用户反馈。四层里最容易写歪的是状态层。不少初版实现把“复习间隔”算出来后直接写死在界面上下次打开程序所有单词又从第一天开始这就是没区分“数据”和“状态”。间隔、记忆系数、复习次数属于状态必须持久化单词本身属于静态数据可以常驻。状态层单独设计还有一个好处后面想换UI框架、从命令行改成App调度逻辑完全不动。2.2 Python写APP的框架选择Flet、Kivy、Toga、PySide6Python做APP的框架不少但每个的边界差很远。做背单词这类工具型应用我一般优先考虑Flet其次才是KivyPySide6适合桌面工具不太适合往移动端走。下面这张表列出最常见的四种选择之后按“哪个坑最少”来拍板。框架渲染方式跨平台能力打包难度适合场景FletFlutter前端渲染Windows、macOS、Linux、Android、Web桌面用PyInstaller移动端有flet build命令工具型App、数据驱动界面Python代码统一写Kivy自绘Canvas桌面、Android、iOSAndroid需配SDK和构建链iOS基本得靠云服务复杂自定义界面、游戏化交互Toga系统原生控件桌面为主移动端受限依赖系统控件跨端一致性问题多原生控件爱好者不推荐新手选PySide6Qt原生控件桌面三平台PyInstaller成熟专业桌面软件不考虑手机端结合标题里的“APP项目实战”选Flet的收益最大。它最大的特点是Python端写逻辑界面组件由Flutter渲染键盘输入、列表滚动、按钮点击这些都不需要自己画。对背单词这种列表加翻卡的界面Flet内置的Text、ElevatedButton、Card足够用而且打包成Android APK时走的也是官方构建流程不会像Kivy那样依赖一堆C库。安装上没什么特殊之处在python安装完成后执行下面命令即可pip install flet装完后跑一个最小入口脚本能看到Flet启动的是一个窗口程序而不是控制台说明渲染后端正常工作。注意Flet有桌面模式和Web模式之分默认走桌面模式对本地SQLite文件读写没有限制。2.3 数据落地的选型JSON做导入SQLite做存储现在很多开源背单词项目喜欢把单词表放在一个大JSON里直接读这个做法用来demo可以但一旦加入复习状态就会很别扭。JSON要改一个单词的间隔得把整个文件读进内存、改完再写回两个窗口同时打开时还会互相覆盖。比较合理的设计是JSON做词库的交换格式内部状态全部放SQLite。SQLite在Python里是标准库的一部分不需要额外安装单文件数据库对桌面App再合适不过。背单词APP的数据量撑死几万条SQLite完全扛得住索引建好后查询毫秒级返回。更重要的是状态更新可以用事务保证原子性按“认识”按钮的那一瞬间间隔、记忆因子、复习次数要么全部更新要么全部不更新不会出现半截状态。2.4 目录结构与初始化项目结构建议按职责分文件而不是把所有代码堆在main.py里。我一般会拆成下面这个样子wordapp/ ├── main.py # Flet入口只管界面 ├── db.py # 数据库连接、建表、初始化 ├── scheduler.py # 复习调度算法纯Python不依赖UI ├── words.csv # 词库导入文件 └── data/ └── words.db # SQLite数据文件运行时生成db.py放数据层scheduler.py放调度逻辑main.py只负责把调度结果渲染出来。这样拆的好处是调度算法可以单独写单元测试不启动界面就能验证复习间隔算得对不对。界面上一个按钮的事故回溯起来不会牵扯到数据层。3. 词库与记忆状态库SQLite建表、CSV导入、重复词与类型转换3.1 words 与 reviews 两张核心表背单词系统不需要复杂的ER图两张表就够。words表存静态词条reviews表存每个单词的学习状态两个表通过word_id关联。CREATE TABLE IF NOT EXISTS words ( id INTEGER PRIMARY KEY AUTOINCREMENT, word TEXT NOT NULL UNIQUE, phonetic TEXT DEFAULT , definition TEXT NOT NULL, example TEXT DEFAULT , created_at TEXT DEFAULT CURRENT_TIMESTAMP ); CREATE TABLE IF NOT EXISTS reviews ( word_id INTEGER PRIMARY KEY, stage INTEGER DEFAULT 0, ease REAL DEFAULT 2.5, interval_days INTEGER DEFAULT 0, due_date TEXT NOT NULL, last_review TEXT, review_count INTEGER DEFAULT 0, lapses INTEGER DEFAULT 0 );建表的细节跟普通业务表不太一样。words.word必须加UNIQUE约束不然CSV导入时脚本容易产生重复词条。reviews表用了word_id做主键因为一个单词只保留一份学习状态不需要历史版本。ease是记忆因子也叫难度系数默认2.5在SM-2算法里控制间隔增长的倍率。due_date是下次到期时间调度层所有查询都围着这个字段转后面要建索引。3.2 启动即初始化的连接模块Python连接SQLite的代码很固定但背单词APP有个特殊点界面回调里要频繁读写数据库最好保证每次拿到的连接都是独立且开启WAL模式的。import sqlite3 from pathlib import Path BASE_DIR Path(__file__).parent DB_PATH BASE_DIR / data / words.db def get_conn(): conn sqlite3.connect(DB_PATH) conn.row_factory sqlite3.Row conn.execute(PRAGMA journal_modeWAL;) conn.execute(PRAGMA foreign_keysON;) return conn def init_db(): conn get_conn() conn.executescript( CREATE TABLE IF NOT EXISTS words (...); CREATE TABLE IF NOT EXISTS reviews (...); CREATE INDEX IF NOT EXISTS idx_reviews_due ON reviews(due_date); ) conn.commit() conn.close()代码里PRAGMA journal_modeWAL这行很关键。WAL模式让读操作和写操作不互相阻塞Flet界面在刷新队列时另一个回调刚好在写复习结果不会抛database is locked。init_db在main.py启动时调用一次如果data目录不存在记得先mkdir。3.3 从CSV导入词库并清洗CSV是词库交换最常见的形式很多开源词库都提供word,phonetic,definition,example四列。导入脚本要做三件事跳过空行、去重、把字符串转成规范类型。import csv from db import get_conn def import_words(csv_path): conn get_conn() cur conn.cursor() inserted 0 skipped 0 with open(csv_path, encodingutf-8) as f: reader csv.DictReader(f) for row in reader: word (row.get(word) or ).strip() definition (row.get(definition) or ).strip() if not word or not definition: continue cur.execute( INSERT OR IGNORE INTO words(word, phonetic, definition, example) VALUES(?, ?, ?, ?), (word, row.get(phonetic, ).strip(), definition, row.get(example, ).strip()) ) if cur.rowcount 1: inserted 1 else: skipped 1 conn.commit() conn.close() print(finserted{inserted}, skipped_duplicated{skipped})这里有几个容易踩的类型转换细节。row.get()从CSV拿到的一定是字符串但CSV里如果definition列被引号包着Python csv模块会自动去引号不需要手动处理。 phonetic字段在很多词库里是空字符串直接strip避免存一堆空格。skipped统计的是INSERT OR IGNORE触发去重的数量能直观看到词库有没有重复项。3.4 学习记录导出备份SQLite文件本身可以直接拷贝备份但用户拿到手不一定能打开。常见的做法是提供CSV导出把复习状态导成人类可读的表格。import csv from db import get_conn def export_reviews(csv_path): conn get_conn() rows conn.execute( SELECT w.word, r.stage, r.ease, r.interval_days, r.review_count, r.lapses, r.due_date FROM reviews r JOIN words w ON w.id r.word_id ORDER BY r.due_date ).fetchall() with open(csv_path, w, newline, encodingutf-8) as f: writer csv.writer(f) writer.writerow([word, stage, ease, interval_days, review_count, lapses, due_date]) writer.writerows(rows)export_reviews用JOIN把单词文本和复习状态拼在一起如果用户想迁移到别的背单词软件这份CSV就是通行格式。review_count和lapses是两个容易被忽略的指标review_count表示总共复习了几次lapses表示忘记过几次lapses偏高说明这个词需要降低间隔或者加入生词本。4. 用简化版SM-2算法驱动背单词复习调度4.1 为什么背单词APP要把调度逻辑单独抽出来很多背单词软件“背了就忘”的根本原因是调度逻辑写在了按钮回调里每点一次按钮只更新一个字段从不计算下次何时出现。真正能落地的调度算法要回答三个问题新词什么时候开始出现、复习成功的词间隔怎么拉长、忘记的词怎么降级。SM-2算法是目前Anki类软件最常用的方案它的优势是把这三件事压缩成一组可计算的参数而且离线可用、不需要联网。把调度逻辑单独抽到scheduler.py里还有一个直接好处不启动界面就能用脚本批量生成复习计划。写一个遍历words表、按SM-2规则生成due_date的脚本跑完直接查数据库就能看出未来两周的复习负载。4.2 SM-2在App里的参数与状态变化简化版SM-2不需要完整的五个阶段只需要记三样东西stage表示学习阶段ease表示记忆因子interval_days表示当前间隔天数。用户每次点击按钮给出一个0到5的打分分数不同状态转移不同。用户操作grade值状态变化interval_days变化完全忘记0stage归0lapses加1变为1天想起来但卡顿2stage归0lapses加1变为1天犹豫后答对3stage加1第1次变1天第2次变6天之后乘ease轻松答对5stage加1同上且ease增加ease的调整公式在标准SM-2里是ease ease (0.1 - (5 - grade) * (0.08 (5 - grade) * 0.02))grade为5时ease增加0.1grade为3时不增不减低于3时不调整或直接重置。最小ease设为1.3防止间隔越拉越短。4.3 复习函数与“下一词”查询的Python实现调度函数入参只需要word_id和grade两样东西内部完成读状态、算新间隔、写回三步最后返回新的间隔天数。from datetime import datetime, timedelta from db import get_conn def review_word(word_id, grade, nowNone): now now or datetime.now() conn get_conn() row conn.execute( SELECT stage, ease, interval_days, review_count, lapses FROM reviews WHERE word_id ?, (word_id,) ).fetchone() if row is None: stage, ease, interval_days 0, 2.5, 0 review_count, lapses 0, 0 else: stage, ease, interval_days, review_count, lapses row if grade 3: if interval_days 0: new_interval 1 elif interval_days 1: new_interval 6 else: new_interval round(interval_days * ease) stage 1 review_count 1 else: new_interval 1 stage 0 lapses 1 review_count 1 ease max(1.3, ease (0.1 - (5 - grade) * (0.08 (5 - grade) * 0.02))) due (now timedelta(daysnew_interval)).isoformat() conn.execute( INSERT INTO reviews(word_id, stage, ease, interval_days, due_date, last_review, review_count, lapses) VALUES(?, ?, ?, ?, ?, ?, ?, ?) ON CONFLICT(word_id) DO UPDATE SET stageexcluded.stage, easeexcluded.ease, interval_daysexcluded.interval_days, due_dateexcluded.due_date, last_reviewexcluded.last_review, review_countexcluded.review_count, lapsesexcluded.lapses, (word_id, stage, ease, new_interval, due, now.isoformat(), review_count, lapses) ) conn.commit() conn.close() return stage, new_intervalgrade的判断逻辑里3分是分水岭3分及以上算记住间隔正常推进3分以下算忘记重置到1天。这样设计比二值判断更符合真实记忆状态用户记单词时“犹豫一下但答对”和“秒答”应该产生不一样的间隔增长。ON CONFLICT语句在SQLite 3.24以上版本可用Python 3.10自带的SQLite通常都满足。“下一词”的查询同样写在scheduler.py里核心是取due_date早于当前时间、或者还没有任何复习记录的单词。def next_word(nowNone): now now or datetime.now() conn get_conn() row conn.execute( SELECT w.id, w.word, w.definition FROM words w LEFT JOIN reviews r ON w.id r.word_id WHERE r.due_date IS NULL OR r.due_date ? ORDER BY CASE WHEN r.due_date IS NULL THEN 1 ELSE 0 END, r.due_date ASC LIMIT 1 , (now.isoformat(),)).fetchone() conn.close() return row注意ORDER BY里的CASE表达式先把已有到期记录排前面新词排最后。这样复习为主、新词穿插避免用户一上来就被几十个生词砸晕。如果你偏好新词优先把CASE里1和0对调即可。4.4 三个容易踩的边界新词、跨天、重复复习第一个边界是新词没有reviews记录LEFT JOIN查出来due_date是NULL如果不加r.due_date IS NULL条件新词永远不会进入学习队列。第二个边界是跨天电脑休眠一晚上隔天打开APP时due_date早于当前时间查询正常命中但如果休眠期间时间跳变interval_days还是用旧值算不会出问题只是当天复习队列会偏多。第三个边界是用户连续点两次按钮第二次点下去会把第一次的复习结果覆盖掉所以按钮回调里要禁用重复点击或者用review_count校验。5. Flet界面层与学习队列翻卡交互、按钮回调、调试断点5.1 Flet最小组件骨架Flet的界面代码风格很像Flutter页面是一个纵向布局的Column组件挂在page.add里。背单词APP的界面不需要复杂路由一个页面就够核心是三个组件显示单词的Text、显示释义的Text、两个操作按钮。import flet as ft from scheduler import next_word, review_word def main(page: ft.Page): page.title 背单词App page.window.width 480 page.window.height 760 page.horizontal_alignment ft.CrossAxisAlignment.CENTER word_text ft.Text(, size30, weightft.FontWeight.BOLD) meaning_text ft.Text(, size18, visibleFalse, selectableTrue) def show_next(_None): row next_word() if row is None: word_text.value 今天的单词已复习完 meaning_text.value else: word_text.value row[word] meaning_text.value row[definition] meaning_text.visible False page.update() def on_grade(grade): if word_text.value and meaning_text.visible: current next_word.__self__ # 实际项目中用session保存当前词id review_word(current[id], grade) show_next() reveal_btn ft.ElevatedButton( 显示释义, on_clicklambda e: (setattr(meaning_text, visible, True), page.update()) ) again_btn ft.ElevatedButton(忘记, on_clicklambda e: on_grade(0)) good_btn ft.FilledButton(认识, on_clicklambda e: on_grade(4)) page.add( ft.Column( [ word_text, meaning_text, reveal_btn, ft.Row([again_btn, good_btn]), ], alignmentft.MainAxisAlignment.CENTER, spacing20, ) ) show_next() ft.app(targetmain)上述代码有一个必须补上的缺陷show_next里没有保存当前word_idon_grade调用review_word时拿不到词条。实际上要在类或闭包里加一个current_id变量show_next时赋值on_grade里读取。补齐后的逻辑是显示释义前点“忘记”“认识”无效因为meaning_text还是隐藏状态这正好防止误触。5.2 学习队列的生成策略到期优先新词交错队列不是在界面层生成的而是在scheduler里用一个查询函数拉取。每点一次按钮就调用一次next_word只取一条天然保证“一次只学一个词”的节奏。ORDER BY CASE WHEN r.due_date IS NULL THEN 1 ELSE 0 END, r.due_date ASC这个排序的目的是让最紧迫的词先出现。如果你想控制每天新词数量可以在应用层加一个计数器今天已经学过的新词数来自reviews表里面stage为0且今天刚插入的count超过阈值就把新词过滤掉。5.3 事件回调与页面刷新Flet的按钮回调接收一个e参数但用不上时可以命名为_。lambda表达式里用setattr修改visible属性然后立刻page.update()注意Flet不会自动感知属性变化每次更新界面必须显式调用update。这是新手最容易漏的一步漏掉的表现为数据变了界面没变。5.4 VSCode调试背单词Applaunch.json与“当前不会命中断点”调试Flet界面逻辑比调试命令行脚本有讲究。直接按F5启动main.py通常能断住普通函数但如果断点设在回调函数里第一次点击按钮才触发。如果提示“当前不会命中断点”先检查launch.json里python解释器是不是当前虚拟环境再确认program指向的是main.py而不是某个flet-cli入口。{ version: 0.2.0, configurations: [ { name: Python: flet, type: debugpy, request: launch, program: ${workspaceFolder}/main.py, console: integratedTerminal, justMyCode: true } ] }调试器配置好后在review_word第一行打断点界面点“认识”按钮就能看到调度函数的完整调用栈包括grade传入值和数据库写回结果。这也是验证上面SM-2算法逻辑最直接的方式比看print日志高效得多。6. 打包独立App与二次验证PyInstaller、安卓注意事项、复习报表6.1 用PyInstaller打包成桌面exePython桌面App分发最成熟的方式还是PyInstaller。Flet应用打包时入口脚本是main.py执行命令如下pip install pyinstaller pyinstaller --onefile --windowed --name WordApp main.py--onefile把Python解释器和依赖打成一个exe--windowed表示启动时不弹控制台窗口。打包产物在dist目录下双击就能运行。有一个隐含坑数据文件放data/words.db是在源码目录下打包后exe解压到临时目录直接访问相对路径会找不到数据库。常见做法是把数据库路径改成Path(sys.executable).parent / dataexe运行时以它自己所在目录为基准。6.2 打包Android时的路径与数据目录问题如果要出APKFlet官方有打包命令大致流程需要安装Android SDKAndroid打包同样要处理数据目录和权限。生成APK后SQLite数据库通常放在应用私有目录下这时要注意不要把词库打包进APK的assets里后直接写库APK里的文件是只读的要先复制到app.getExternalFilesDir。python -m flet build apk这个命令会自动完成构建脚本和资源打包。如果报SDK路径错误先确认ANDROID_HOME环境变量指向你的SDK安装目录再检查platform-tools有没有装。数据迁移到Android后每次启动检查数据库是否存在不存在就从assets里复制一个初始模板逻辑和桌面端几乎一致。6.3 启动自检与复习报表验证打包成果最直接的方式是启动后命令行执行一条sqlite查询看数据是否完整。sqlite3 data/words.db SELECT COUNT(*) FROM words; SELECT COUNT(*) FROM reviews;这段能返回words表词条总数和reviews表状态记录数。更高阶的验证是把复习调度导出成可视化报表从reviews表按due_date分组统计数量用matplotlib画成柱状图能直观看到未来一周的复习负载曲线。import sqlite3 import matplotlib.pyplot as plt conn sqlite3.connect(data/words.db) rows conn.execute( SELECT date(due_date) d, COUNT(*) FROM reviews GROUP BY d ORDER BY d ).fetchall() days [r[0] for r in rows] counts [r[1] for r in rows] plt.bar(days, counts) plt.xticks(rotation45) plt.tight_layout() plt.savefig(复习分布.png, dpi150)把这段放到启动逻辑里每隔一段时间生成一次就能验证SM-2调度是不是真的把复习压力摊平了第一天有高峰第二天回落这才是算法生效的表现。如果每天都是几十个词堆积在同一天说明ease调整或者grade映射还需要回看第4章的公式。本文还有配套的精品资源点击获取

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询