ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Flask+SQLite3手敲Demo:构建Web开发完整闭环

Flask+SQLite3手敲Demo:构建Web开发完整闭环

简介:一份面向Python初学者的Flask Web开发入门实例,源自试验楼动手实践,完整体现了从零搭建一个小型博文系统的过程。项目采用轻量级Flask框架,配合Python自带的SQLite3数据库,实现了用户注册登录、会话跟踪、博文发布与列表展示等核心功能,并覆盖路由与视图函数设计、Jinja2模板渲染、表单提交与验证、基本错误处理等Web开发关键环节。压缩包内共8个文件,包含主程序、SQL建表脚本、SQLite数据库文件、三个HTML页面模板以及CSS样式表等,整体仅5KB,体量虽小但结构完整,便于初学者逐行研读或在此基础上扩展改造。已有3085人学习该资源,适合希望通过最小完整项目快速理解Web开发全流程的入门者;对其中的用户认证、数据库操作、模板继承等写法稍加调整,即可迁移到个人博客、学习笔记等实际场景。

1. Python Flask开发实例:一个手敲的小Demo怎么帮你建立完整的Web开发闭环

很多人学 Python 的最终诉求就是 Web 开发,但看完教程自己从头写 Flask 时,卡在最没技术含量的地方:路由不知道拆几个、sqlite3 连完不关会不会出问题、模板里为什么中文渲染出来是乱码。这个 Flask 开发实例就是一个自己敲过的试验楼小 Demo,用 Flask + sqlite3 做了最基础的增删改查页面,把「请求进路由 → 操作数据库 → 模板渲染回页面」这条链路完整走了一遍。它解决的核心问题不是功能多复杂,而是让新手在抄代码之外,清楚每一个文件放在哪、每一行代码在干什么、改坏之后去哪排查。适合刚学完 Flask 语法还没做过完整项目的人,也适合想快速搭一个本地工具页面的开发。

2. 项目骨架:Flask + SQLite3 的选型理由与目录拆解

一个小 Demo 做到能安全运行、能往上叠加功能,第一步不是写路由,而是先把项目形态摆清楚。Flask 和 SQLite3 这个组合是这个场景下性价比最高的选择,但很多人一开始不是这么想的,这里先说透为什么这么选,再落目录和入口文件。

2.1 选型理由:为什么 Flask + SQLite3 更适合手敲起步

对比 Django,Flask 没有强制性的项目结构和数据模型配置,一个.py文件就能把应用跑起来。Django 的 settings、迁移、admin 后台等自动化的东西在项目大时有优势,但对只想验证「表单提交后数据到底存哪了」的初学者来说,过多的框架约束反而把注意力从逻辑本身挪走了。我一般会建议用 Flask 作为第一个 Web 框架,因为它的路由、请求上下文、模板渲染都是显式写出来的,没有太多框架魔法。

SQLite3 则是 Python 标准库自带的东西,不需要单独装数据库服务。我曾经碰到过一个学 Django 的同学,配置 MySQL 连不上,最后发现是本地根本没启动服务,这类问题在 sqlite3 场景下完全消失:连接的就是一个文件,不需要账号密码,不需要监听端口。对于实验楼这种小 Demo 来说,SQLite3 够用,数据量上到一定量级再迁移 MySQL 也不迟,因为 SQL 语句大多是通用的。

选型还要考虑部署成本。Flask 应用可以用内置服务器直接跑,SQLite3 数据库就是项目目录里的一个.db文件,迁移时拷贝文件就带走了全部数据,这对做试验、做课程设计、做内部工具都是一个极大的便利。当然,SQLite3 不适合高并发写入,这是它的边界,但对这个小 Demo 的规模完全不是问题。

2.2 目录结构:一个可运行的项目应该怎么放文件

先看一份规范的目录,这是许多入门项目最容易被忽视的部分。很多人写 Flask 项目时把所有内容塞进一个.py文件,最后代码上千行,模板字符串写死在 Python 里,改个界面要重启服务,血泪教训。合理的拆分方式如下:

demo_app/ ├── app.py # Flask 应用入口与路由 ├── schema.sql # 建表语句独立维护 ├── demo.db # SQLite3 数据库文件(首次运行后生成) ├── static/ │ └── style.css # 静态资源 └── templates/ ├── index.html # 列表页模板 └── form.html # 新增/编辑表单模板

schema.sql 独立出来是为了让建表语句可追踪、可重复执行。数据库文件 demo.db 不手动创建,而是由初始化脚本生成,这样每次 clone 项目后只需要执行一次刷新就能重建环境。

入口文件 app.py 是最早应该写清楚的。下面是一个最小但完整的应用入口:

from flask import Flask, render_template, request, redirect, url_for import sqlite3 DB_PATH = 'demo.db' app = Flask(__name__) @app.route('/') def index(): conn = sqlite3.connect(DB_PATH) conn.row_factory = sqlite3.Row rows = conn.execute( 'SELECT * FROM items ORDER BY created_at DESC' ).fetchall() conn.close() return render_template('index.html', items=rows) if __name__ == '__main__': app.run(debug=True)

这段代码里,app = Flask(__name__)创建应用实例,__name__让 Flask 知道去哪里找 templates 和 static 目录。conn.row_factory = sqlite3.Row是关键配置,它让查询结果可以用row['字段名']的方式取值,而不是靠数字索引。conn.close()是新手最容易漏的——SQLite 不像 MySQL 那样连接断掉会自动回收,文件句柄一直不关后面会报database is locked。

写完入口文件,可以在命令行手动验证一下:

python -c "from app import app; print(app.url_map)"

这一步会用 URL Map 打印出所有注册路由。它能验证两件事:一是 Flask 应用能正常导入,没有语法错误;二是能看到每条路由对应的函数名和 HTTP 方法。注意:这里打印的是url_map而不是直接运行app.run(),因为直接运行会启动服务阻塞终端,不方便做快速检查。

3. SQLite3 数据层:建表、连接与初始化脚本怎么设计

路由层决定了页面长什么样,数据层决定了数据存得住、查得对。这个小 Demo 的核心表只有一张,但建表语句、连接管理、初始化流程这三个环节每一个都有讲究。次序上先建表、再连接、最后初始化,每一步都能独立验证。

3.1 数据表设计与字段类型选择

以一个待办清单管理为例,数据字段不能只想到「标题一个字段」。我见过不少人一张表只有一个 title 字段,后面加状态、加时间的时候全部返工。建议从第一版就把这几个字段定下来:

字段名类型约束/默认值说明
idINTEGERPRIMARY KEY AUTOINCREMENT主键,自增
titleTEXTNOT NULL待办标题
contentTEXTDEFAULT ''详细描述,允许为空
statusINTEGERDEFAULT 00 未完成,1 已完成
created_atTEXTDEFAULT CURRENT_TIMESTAMP创建时间,UTC 格式

status 用 INTEGER 而不是 TEXT,是为了让状态切换的 SQL 更轻量:UPDATE items SET status = 1 - status WHERE id = ?,一次请求完成切换。created_at 用 TEXT 是因为 SQLite 没有独立的 datetime 类型,默认字符串 'YYYY-MM-DD HH:MM:SS' 在排序时依然能按字典序正确排列。

schema.sql 文件里写了标准的建表语句:

DROP TABLE IF EXISTS items; CREATE TABLE IF NOT EXISTS items ( id INTEGER PRIMARY KEY AUTOINCREMENT, title TEXT NOT NULL, content TEXT DEFAULT '', status INTEGER DEFAULT 0, created_at TEXT DEFAULT CURRENT_TIMESTAMP );

第一行DROP TABLE IF EXISTS items;是初始化脚本的惯例写法。它让每一次重新初始化都从干净状态开始,避免残留数据影响调试。生产环境里当然不能随手 DROP,但试验场景下这是最高效的「后悔药」。

3.2 连接管理与初始化执行方式

上一章入口文件里直接连接的方式有个隐患:每次请求都要手动 connect、close。路由少的时候能忍,路由一多就会出现某个分支忘记 close 的情况。推荐用 Flask 的g对象结合teardown_appcontext把连接管理和应用生命周期绑定:

from flask import g def get_db(): if 'db' not in g: g.db = sqlite3.connect(DB_PATH) g.db.row_factory = sqlite3.Row return g.db @app.teardown_appcontext def close_db(exception): db = g.pop('db', None) if db is not None: db.close()

这样每个请求进来后调用get_db()拿到连接,请求结束后 Flask 自动调用close_db关闭。逻辑说明:g是 Flask 的请求级全局变量,每个请求独立存储,'db' not in g保证了同一个请求内复用同一个连接,不开第二个。

初始化数据库的流程不能放在路由里,不然每次有人访问首页都会执行一遍建表。常见做法是写一个独立的执行入口:

python -c "import sqlite3; exec(open('schema.sql').read()); print('DB initialized')"

这条命令的语义是:打开 schema.sql 文件,把其中的 SQL 语句全部读出并交给 sqlite3 模块执行。需要说明的是exec执行的是 Python 代码,本质是open().read()把 SQL 文件当成字符串取出,exec执行该字符串后,sqlite3 的Connection.execute会解析并执行其中的 SQL 语句。执行成功后会出现一个 demo.db 文件。如果这一步报错,通常是 schema.sql 路径不对——用相对路径时必须确保命令行当前目录在 demo_app 下。

从这之后的所有路由,都用get_db()拿连接,不需要再关心关闭问题。这也让后续的路由函数变得非常干净:查询、返回结果、交给模板渲染。

4. 路由与页面流:从表单提交到数据落库的完整链路

数据层就绪后,接下来是整条业务链路:用户在浏览器填写表单,点击提交,Flask 路由接收到 POST 请求,校验并写入 sqlite3,最后重定向回列表页看到新数据。每一个环节都可以单独验证,后端跑起来了再对前端模板做微调。

4.1 路由设计与请求方法划分

这个小 Demo 的主要路由不超过五个,按动作拆分:

路由方法作用
/GET列表页,显示所有待办
/addPOST新增待办
/check/<int:item_id>GET切换完成状态
/delete/<int:item_id>POST删除待办

有人会把新增和编辑合在一起共用一个路由,通过隐藏字段区分。这个策略在复杂项目里能减少代码量,但在小 Demo 里会引入分支判断的复杂度,反而不容易看懂。这里刻意拆开,每个路由只做一件事。

新增路由的核心代码如下:

@app.route('/add', methods=['POST']) def add(): title = request.form.get('title', '').strip() answer = request.form.get('answer', '').strip() if not title: return '标题不能为空', 400 db = get_db() db.execute( 'INSERT INTO items (title, content) VALUES (?, ?)', (title, content) ) db.commit() return redirect(url_for('index'))

逻辑说明:request.form.get拿到的是表单里的字符串,.strip()去掉首尾空格。not title是空值校验,直接返回 400 状态码比写入脏数据再修复简单得多。参数说明:SQL 中的?是占位符,第二个参数是元组,顺序必须与?一一对应。任何时候都禁止用 f-string 拼接 SQL,这是 SQL 注入的入口。

4.2 模板渲染与表单页实现

新增页面的表单是 HTML 写的,但要在 Jinja2 模板里嵌入,才能保证后续的 url 生成不乱。这里演示 templates/form.html 的完整结构:

<!doctype html> <html> <head><title>新增待办</title></head> <body> <form method="post" action="{{ url_for('add') }}"> <input type="text" name="title" placeholder="标题" required> <textarea name="content" placeholder="备注内容"></textarea> <button type="submit">提交</button> </form> </body> </html>

逻辑说明:action="{{ url_for('add') }}"是 Flask 推荐写法,由后端根据视图函数名生成 URL,而不是手写/add。这样如果有一天路由路径从/add改成/create,前端模板不需要动。required属性是浏览器端的非空校验,与后端校验组成双保险。

列表页模板的核心循环:

{% for item in items %} <li> <span>{{ item.title }}</span> <span>{{ '已完成' if item.status == 1 else '未完成' }}</span> <a href="{{ url_for('check', item_id=item['id']) }}"> 切换状态 </a> <form method="post" action="{{ url_for('delete', item_id=item['id']) }}"> <button type="submit">删除</button> </form> </li> {% endfor %}

这里的url_for('check', item_id=item['id'])展示了带参路由的写法:第一个参数是视图函数名,后面的关键字参数对应路由中的<int:item_id>。由于数据库连接设置了row_factory = sqlite3.Row,item['id']和item.status两种取法都能用。

4.3 改写与删除操作的边界处理

切状态和删除是两个看起来「只需要一行 SQL」的操作,但坑都在边界上。

@app.route('/check/<int:item_id>') def check(item_id): db = get_db() db.execute( 'UPDATE items SET status = 1 - status WHERE id = ?', (item_id,) ) db.commit() return redirect(url_for('index'))

逻辑说明:1 - status把 0 变 1、1 变 0,完成一次切换。这里不需要先 SELECT 再判断,可以少一次查询。参数说明:<int:item_id>里的int:转换器会让 Flask 自动把 URL 片段转成 Python 整数,如果传了非数字值,会直接返回 404,不会进到视图函数里。删除路由类似,只是把 UPDATE 换成 DELETE。

边界处理有两处必须看守:一是 ID 不存在。UPDATE和DELETE对不存在的 ID 不会报错,只是影响行数为 0,所以「假装成功但实际什么都没发生」的现象很容易出现。二是数据库写入后必须commit()。sqlite3默认开启事务但不会自动提交,漏了 commit 的话数据只在连接内可见,刷新页面后消失,这是最容易翻车的点。

再补充一个批量操作的做法:如果待办列表多了,逐个点击很痛苦,可以加一个「清空已完成」的批处理路由。核心 SQL 是DELETE FROM items WHERE status = 1,删除前用SELECT COUNT(*)确认影响行数,再决定是否重定向。

5. Flask 排错避坑:小 Demo 里最容易翻车的五个地方

一个人写完整个项目后,遇到的问题往往不在语法上,而在环境、路径、缓存这些「看不见的地方」。这里的每一条都是我为这个 Demo 调试时实际撞见过的,写下来供后面的人少走弯路。

5.1 端口被占用导致启动失败

现象:执行python app.py后终端提示Address already in use,Flask 起不来。

原因:上一次运行没有正常终止,系统还保留了 5000 端口;或者机器上别的程序占了默认端口。Flask 默认监听 5000 端口,它不是所有机器的真空地带。

解决:换端口启动,或者找出占用进程。临时方案是把app.run()改成app.run(port=5001)。找占用进程的命令各行其道:Windows 下netstat -ano | findstr 5000拿到 PID 后在任务管理器结束进程。

5.2 数据库文件被多个连接写锁

现象:运行正常,但在某个操作后报sqlite3.OperationalError: database is locked。

原因:SQLite 在写操作时会锁定整个数据库文件。发生锁的问题通常有两个来源:一是上一个连接没关闭,事务没结束;二是用sqlite3.connect()直接连时,Python 持有的连接在异常时没有释放。

解决:统一走get_db()+teardown_appcontext这套流程,保证每个请求结束都关闭连接。同时确保每次写操作后都要commit(),否则写锁一直持续到连接被强行回收。这里再给一个排查命令:

python -c "import sqlite3; conn = sqlite3.connect('demo.db'); print(conn.execute('SELECT count(*) FROM items').fetchone())"

能查到行数说明文件本身没坏,锁是运行时的问题,不是数据损坏。

5.3 模板修改后浏览器没变化

现象:改了 index.html 的标题,刷新浏览器还是旧内容。

原因:有两个层面。浏览器可能缓存了静态页面,但 Flask 模板文件没有缓存一说;真正的原因是模板文件路径被写死或加载了错误副本。比如 templates 目录不在 app.py 同级,Flask 会去当前工作目录找模板,找不到时报TemplateNotFound,而找到的和修改的不是同一个文件。

解决:在模板里加一个版本号参数来强制刷新,比如<script src="{{ url_for('static', filename='app.js') }}?v=2"></script>,这是静态资源的通用做法。对模板文件本身,先确认app.root_path和实际目录一致:

python -c "from app import app; print(app.root_path)"

把输出的路径和实际项目目录对比,不一致时用Flask(__name__, template_folder='路径')显式指定。

5.4 表单提交后页面 400 或 405

现象:点击提交按钮后,页面显示400 Bad Request或405 Method Not Allowed。

原因:405 是请求方法对不上路由声明的方法,典型原因是表单里写了method="get",但后端路由只声明了POST;400 则通常是缺少了路由中定义的关键参数或表单字段名不匹配。

解决:先在后端起调试模式跑,请求进来时看终端输出的 method 和 path。检查request.form.get()里写的字段名与 HTML 中name属性是否一致。前后端字段名的错位是手工写模板最常见的失误,没有之一。

5.5 中文乱码

现象:模板里写的中文在浏览器里变成Ã¥之类乱码。

原因:绝大多数情况下是 HTML 文件没有声明 UTF-8 编码,Flask 渲染时无法正确解码。也有一部分情况是浏览器本身编码猜测错误。

解决:所有 HTML 文件的<head>里加一行<meta charset="utf-8">,并确保编辑器保存时用 UTF-8 编码。如果还乱,在 Flask 应用实例化后用以下代码显式设置:

app = Flask(__name__) app.config['JSON_AS_ASCII'] = False

这个配置影响 JSON 接口,但对模板页面的中文渲染没有直接作用。模板页面乱码的第一解决方案永远是 HTML 文件头声明,其次才是浏览器手动切换编码。

6. 进阶验证:会话控制、静态资源与部署前的收尾检查

小 Demo 的功能链路跑通之后,真正的学习才刚刚开始。下面三个进阶点,能让这个项目从「能跑」变成「像一个正经应用」,而且都是可以在本地立即验证的。

第一,加会话控制。Flask 的session可以用来记录当前操作者是谁,哪怕是本机用也很值。用法上只需要在实例化后设置一个密钥:

app.secret_key = 'dev-key-please-change'

然后任意路由里就能用session['username'] = 'demo'写入、session.get('username')读取。注意:生产部署时这个密钥必须换成不可猜测的随机串,否则有会话伪造风险。

第二,静态资源的正确引用方式。直接把 CSS 或 JS 文件放进 static 目录还不够,在模板里要用url_for引用,而不是相对路径。<link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}">。这样静态文件也会经过 Flask 的统一处理,目录结构再调整也不怕断链。

第三,部署前的收尾检查。关闭debug=True是必须的一步——很多框架内置调试器会在运行时对请求做代码级别的检查,如果暴露到对外环境,任何访问者都能拿到代码执行入口。使用app.run(host='127.0.0.1', port=5000)让应用只监听本机回环地址,不暴露到局域网。

验证完整性的一个方式是命令行检查所有路由的状态:

python -c "from app import app; [print(rule) for rule in app.url_map.iter_rules()]"

每一条路由的输出都确认存在,再手动跑一遍猜的 URL 流程,基本就收尾完成了。从那以后我每次写完一个小 Demo,都会强制走一遍加会话、检查静态资源引用、关闭 debug 这三件事,宁可多花十分钟也不愿意在演示的时候被一个 404 卡住。希望帮到你。

本文还有配套的精品资源,点击获取

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进