书读完就忘了:我用 Flask 加 SQLite 做了个读书记录本
去年年底想给朋友推荐几本书,翻了半天发现自己根本记不全这一年读了啥——有的读完就还了,有的读了半本放弃了,有的读了但完全想不起讲什么。
手机 App 装过两个,功能太多反而懒得记。后来自己用 Python 撸了个网页版,就三个字段:书名、作者、状态。简单到打开就会用,反而坚持下来了。这篇讲从零搭起来的过程,代码都能直接跑。

一、为什么是 Flask 加 SQLite
两个理由,都是图省事:
- Flask 轻。装一个包就能起服务,不用像 Django 那样先生成一堆目录文件。我就想要几个路由,够用就行。
- SQLite 不用装服务。它就是一个
.db文件,Python 标准库自带sqlite3模块,连驱动都不用 pip。数据要备份直接复制文件。
pip install flask
就这一步。数据库那边什么都不用装。
几个选择的理由摆一起看:
| 选择 | 理由 |
|---|---|
| Flask 而不是 Django | 我只想要几个路由,Django 得先生成一堆目录文件 |
| SQLite 而不是 MySQL | 一个 .db 文件,标准库自带模块,不用装数据库服务 |
| 网页而不是手机 App | 打开就能用,不用下载安装,换设备也能访问 |
二、先把表设计出来
我想记的东西:书名、作者、在读还是已读、评分、几句笔记、读完的日期。
CREATE TABLE IF NOT EXISTS books (
id INTEGER PRIMARY KEY AUTOINCREMENT,
title TEXT NOT NULL,
author TEXT,
status TEXT NOT NULL DEFAULT 'reading', -- reading / done / abandoned
rating INTEGER, -- 1~5 星,没评分就是 NULL
notes TEXT,
finished_at TEXT
);
status 我只给了三个值:在读、读完、弃了。一开始想搞得更细,后来发现状态一多,我自己都懒得选——字段越少越容易坚持,这是做完之后最大的体会。
三、最小可用版本
整个应用就一个文件:
# app.py
import sqlite3
from flask import Flask, render_template, request, redirect
app = Flask(__name__)
DB = 'books.db'
def get_conn():
conn = sqlite3.connect(DB)
conn.row_factory = sqlite3.Row # 加了这行才能用 b['title'] 取值
return conn
def init_db():
conn = get_conn()
conn.execute('''
CREATE TABLE IF NOT EXISTS books (
id INTEGER PRIMARY KEY AUTOINCREMENT,
title TEXT NOT NULL,
author TEXT,
status TEXT NOT NULL DEFAULT 'reading',
rating INTEGER,
notes TEXT,
finished_at TEXT
)
''')
conn.commit()
conn.close()
@app.route('/')
def index():
conn = get_conn()
books = conn.execute('SELECT * FROM books ORDER BY id DESC').fetchall()
conn.close()
return render_template('index.html', books=books)
@app.route('/add', methods=['POST'])
def add():
title = request.form['title'].strip()
author = request.form.get('author', '').strip()
if not title:
return redirect('/')
conn = get_conn()
conn.execute('INSERT INTO books (title, author) VALUES (?, ?)', (title, author))
conn.commit()
conn.close()
return redirect('/')
if __name__ == '__main__':
init_db()
app.run(debug=True)
跑起来:
python app.py
打开 http://127.0.0.1:5000 就能看到了。
四、页面长什么样
templates/index.html:
<form method="post" action="/add">
<input name="title" placeholder="书名" required>
<input name="author" placeholder="作者">
<button type="submit">添加</button>
</form>
<table>
<tr><th>书名</th><th>作者</th><th>状态</th></tr>
{% for b in books %}
<tr>
<td>{{ b['title'] }}</td>
<td>{{ b['author'] or '-' }}</td>
<td>{{ '读完' if b['status'] == 'done' else '在读' }}</td>
</tr>
{% endfor %}
</table>
丑是丑了点,但能用。能用比好看重要——真要坚持用下去,第一版能跑起来最关键,样式后面慢慢加。
五、标记读完
再加一个路由,点一下就把状态改成已读、顺便记上日期:
from datetime import date
@app.route('/finish/<int:book_id>')
def finish(book_id):
conn = get_conn()
conn.execute(
'UPDATE books SET status = ?, finished_at = ? WHERE id = ?',
('done', date.today().isoformat(), book_id)
)
conn.commit()
conn.close()
return redirect('/')
日期我用 2026-09-15 这种字符串存。SQLite 没有专门的日期类型,存成 ISO 格式的字符串既能排序又能直接看,比时间戳省事。
要注意的几个地方
-
with语句不会帮你关连接。这个坑了我一次。写with sqlite3.connect(DB) as conn:看起来很优雅,但它只负责提交或回滚事务,连接本身不会关闭。跑久了连接越攒越多。老老实实conn.close(),或者在函数里用try/finally。 -
占位符是
?不是%s。MySQL 那边的习惯带过来就会报错。SQLite 统一用?:cur.execute('SELECT * FROM books WHERE title = ?', (title,))另外
(title,)那个逗号别漏——单个元素的元组不加逗号就退化成字符串了。 -
查询出来默认取不到列名。不加
conn.row_factory = sqlite3.Row的话,只能row[0]、row[1]按位置取,改一下表结构代码就全乱。加那一行能按列名取值,值得。 -
debug=True只能本地开。调试模式会在报错时把代码上下文显示出来,还能执行任意代码。放到服务器上必须关掉。 -
并发写入会锁库。SQLite 同一时刻只允许一个写操作。个人使用完全没问题,真要多人同时写就得换数据库了。
-
数据库文件别放项目外的路径。用相对路径
'books.db'的话,你在哪个目录执行脚本,文件就建在哪,很容易出现"明明加过书但打开是空的"——因为进了另一个目录,其实连的是另一个库。建议写死绝对路径。
小结
这个小东西总共一百来行,但它解决的是真问题:记东西的工具必须足够简单,否则坚持不下来。技术上没什么难点,Flask 提供路由、SQLite 存数据、Jinja 渲染页面,都是最基础的用法。
- 点赞
- 收藏
- 关注作者
评论(0)