书读完就忘了:我用 Flask 加 SQLite 做了个读书记录本

举报
柠檬-夏日清爽 发表于 2026/09/24 13:46:36 2026/09/24
【摘要】 一年读了二十几本书,年底想不起来都读了啥、哪本值得重读。与其装个 App,不如自己撸一个——这篇用 Python 的 Flask 加 SQLite 从零搭一个能增删查的读书记录页面:为什么选这两样、表怎么设计、最小可用版本长什么样,以及 sqlite3 那个"with 语句其实没关连接"的坑。

去年年底想给朋友推荐几本书,翻了半天发现自己根本记不全这一年读了啥——有的读完就还了,有的读了半本放弃了,有的读了但完全想不起讲什么。

手机 App 装过两个,功能太多反而懒得记。后来自己用 Python 撸了个网页版,就三个字段:书名、作者、状态。简单到打开就会用,反而坚持下来了。这篇讲从零搭起来的过程,代码都能直接跑。

image.png

一、为什么是 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 渲染页面,都是最基础的用法。

【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0)

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。