简介:这份资源是一套面向Python Web开发初学者的Flask入门实践Demo,围绕一个简易博文系统展开,涵盖用户登录登出、博文发布与展示等核心功能,适合刚接触Web框架、希望从零跑通完整项目流程的开发者练手。压缩包共8个文件,约5KB,以html模板、py主程序、sql建表脚本、db数据库文件及css样式为主,另含pyc编译文件,体量轻巧却覆盖了路由视图、Jinja2模板渲染、SQLite3数据存储与session身份验证等关键环节,便于对照理解各模块如何协作。目前已有3088人学习下载,说明其作为入门范例具有一定参考价值。读者可通过这份实例观察Flask应用的最小目录组织方式,理清表单提交、数据库表结构设计与页面渲染之间的衔接关系,并借助错误处理与日志相关思路排查运行问题,为后续独立搭建Web项目打下基础。
1. 从一份“试验楼”Demo说起:Flask + SQLite3 能跑通什么
很多人学 Flask 卡在一个尴尬位置:官方教程能看懂,但真让自己从零搭一个带数据库、带增删改查、带页面跳转的小系统,就不知道从哪下手。这份“试验楼小 Demo”正好填这个缝——它不是脚手架生成器吐出来的空壳,而是一个人一行行敲出来的可运行实例,技术栈就是 Python + Flask + sqlite3,没有 ORM 中间层,没有前端框架,页面用 Jinja2 模板渲染,数据落在本地.db文件里。对刚接触 Python web 开发的人来说,这种“裸写”反而比上来就上 SQLAlchemy 更容易看清请求是怎么进、SQL 是怎么出、模板是怎么渲染的。它适合三类人:想搞懂 Flask 最小闭环的初学者、需要快速验证某个想法的从业者、以及想拿一份干净底稿改造成课程设计或内部工具的人。下面我按“先跑起来、再拆结构、最后填坑”的顺序,把这份 Demo 的用法和边界讲透。
2. 把 Demo 跑起来:环境、依赖与首次启动
2.1 先确认 Python 版本和目录结构
拿到源码包第一件事不是急着pip install,而是先看目录。这类手敲 Demo 通常长这样:入口是app.py或run.py,模板放在templates/,静态资源在static/,数据库文件可能是data.db也可能在首次运行时自动建。先执行python --version,Flask 2.x 要求 Python 3.7 以上,Flask 3.x 建议 3.8 以上;如果你机器上是 3.6,很多新语法会直接报错,别浪费时间排查代码。
# 查看 Python 版本,确认 >= 3.8 python --version # 进入项目目录后,先看结构再动手 ls -la # 典型输出里应能看到 app.py、templates/、static/、requirements.txt目录里如果有requirements.txt,说明作者留了依赖清单,这是最省事的路径;如果没有,就按 Flask + 标准库 sqlite3 来装,sqlite3 是 Python 自带的,不需要额外安装。这里有个细节:不要用系统全局环境直接装,建个虚拟环境,否则后面版本冲突会很难受。
2.2 虚拟环境与依赖安装
# 创建并激活虚拟环境(Windows 用 venv\Scripts\activate) python -m venv venv source venv/bin/activate # 有 requirements.txt 就按清单装 pip install -r requirements.txt # 没有清单时,最小依赖就一个 Flask pip install flask装完用pip list确认 Flask 版本。这里常见的翻车点是:作者在 Flask 2.x 下写的代码,你装了 3.x,某些写法(比如@app.before_first_request)已经被移除,启动就报AttributeError。遇到这种情况,优先按报错信息降级 Flask 版本,而不是去改业务代码,因为 Demo 的逻辑往往依赖特定版本的钩子行为。
2.3 首次启动与数据库初始化
# 启动开发服务器,debug 模式方便看报错 flask --app app run --debug # 或者直接 python app.py(取决于入口写法)启动后浏览器访问http://127.0.0.1:5000。如果页面能出来但一点查询就报“no such table”,说明数据库没初始化。手敲 Demo 一般有两种处理方式:一种是在app.py里用with app.app_context(): db.create_all()之类逻辑自动建表;另一种是单独一个init_db.py脚本。先翻代码找CREATE TABLE关键字,找到后执行对应脚本:
# init_db.py 常见写法:建表 + 插入几条种子数据 import sqlite3 conn = sqlite3.connect('data.db') cur = conn.cursor() cur.execute(''' CREATE TABLE IF NOT EXISTS items ( id INTEGER PRIMARY KEY AUTOINCREMENT, title TEXT NOT NULL, content TEXT, created_at TEXT DEFAULT CURRENT_TIMESTAMP ) ''') cur.execute("INSERT INTO items (title, content) VALUES (?, ?)", ("第一条测试数据", "用来验证列表页能否正常渲染")) conn.commit() conn.close() print("数据库初始化完成")这段脚本的关键点有三个:IF NOT EXISTS保证重复执行不报错;?占位符是参数化查询的写法,后面讲注入时会再提;CURRENT_TIMESTAMP让时间字段自动填充,省得业务代码里手动拼时间字符串。执行python init_db.py后刷新页面,列表能出数据,说明最小闭环通了。
3. 拆开看结构:路由、模板与 sqlite3 的三层配合
3.1 路由层:一个请求从 URL 到函数的映射
Flask 的核心就一句话:URL 规则绑定到视图函数。这份 Demo 里通常能看到列表页、新增页、编辑页、删除动作四类路由。理解它们的关键不是背语法,而是看清methods参数和返回值的关系——GET 返回渲染后的模板,POST 处理表单后重定向。
from flask import Flask, render_template, request, redirect, url_for import sqlite3 app = Flask(__name__) def get_db(): # 每次请求新建连接,简单直接;生产环境应换成连接池或 g conn = sqlite3.connect('data.db') conn.row_factory = sqlite3.Row # 让查询结果支持按列名取值 return conn @app.route('/') def index(): conn = get_db() rows = conn.execute('SELECT * FROM items ORDER BY id DESC').fetchall() conn.close() return render_template('index.html', items=rows) @app.route('/add', methods=['GET', 'POST']) def add(): if request.method == 'POST': title = request.form['title'] content = request.form.get('content', '') conn = get_db() conn.execute('INSERT INTO items (title, content) VALUES (?, ?)', (title, content)) conn.commit() conn.close() return redirect(url_for('index')) # POST 后重定向,避免刷新重复提交 return render_template('add.html')row_factory = sqlite3.Row这行值得单独说:不设它,查询结果只能按索引row[0]取值,模板里写起来很别扭;设了之后可以row['title'],可读性直接上一个台阶。redirect(url_for('index'))是 POST 处理的标准收尾,少了它,用户刷新页面会触发重复插入,这是新手最常踩的坑之一。
3.2 模板层:Jinja2 的变量、循环与继承
模板不是 HTML 加几个花括号那么简单,它决定了数据怎么呈现、页面怎么复用。这份 Demo 一般会用base.html做骨架,子模板通过{% extends %}继承,导航和页脚只写一次。
<!-- templates/base.html:公共骨架 --> <!DOCTYPE html> <html> <head><title>{% block title %}试验楼{% endblock %}</title></head> <body> <nav><a href="{{ url_for('index') }}">首页</a> | <a href="{{ url_for('add') }}">新增</a></nav> <main>{% block content %}{% endblock %}</main> </body> </html> <!-- templates/index.html:列表页 --> {% extends "base.html" %} {% block content %} <table> <tr><th>ID</th><th>标题</th><th>时间</th><th>操作</th></tr> {% for item in items %} <tr> <td>{{ item['id'] }}</td> <td>{{ item['title'] }}</td> <td>{{ item['created_at'] }}</td> <td><a href="{{ url_for('edit', item_id=item['id']) }}">编辑</a></td> </tr> {% else %} <tr><td colspan="4">暂无数据</td></tr> {% endfor %} </table> {% endblock %}{% for %}配{% else %}是 Jinja2 的一个实用特性:循环体一次都没执行时走 else 分支,正好用来做空状态提示,比在视图里判断len(items) == 0再传标志位干净。url_for传参时用函数名加参数名,改路由规则时模板不用跟着改,这是它比硬编码 URL 强的地方。
3.3 数据层:sqlite3 直连的取舍
为什么这份 Demo 不用 ORM?因为 sqlite3 是标准库,零依赖,单文件存储,拷走就能跑,特别适合教学和原型。代价是 SQL 要自己写、事务要自己管、迁移要自己想办法。对初学者来说,这个代价换来的是对“一条 SQL 到底怎么执行”的直观认知,值得。
| 维度 | sqlite3 直连 | SQLAlchemy ORM |
|---|---|---|
| 依赖 | 标准库,零安装 | 需额外安装 |
| 学习成本 | 会 SQL 即可 | 需理解模型映射 |
| 迁移 | 手写脚本 | 配合迁移工具 |
| 适用场景 | 教学、原型、单机小工具 | 中大型项目、多表关联 |
选型上没有绝对对错,关键看阶段。这份 Demo 的定位就是“看清底层”,所以直连是合理选择;等你把增删改查跑顺了,再换 ORM 迁移成本也不高。
4. 避坑与排查:五个真实会卡住你的问题
4.1 现象:刷新页面重复插入数据 → 原因:POST 后直接返回模板 → 解决:重定向
表单提交后如果视图直接return render_template(...),浏览器地址栏还停在 POST 请求上,用户一刷新就会再提交一次。正确做法是处理完写操作后redirect到 GET 页面,这就是所谓的 PRG 模式(Post-Redirect-Get)。检查你的新增和编辑视图,凡是methods含 POST 的,成功分支都应该是重定向。
4.2 现象:模板里中文乱码 → 原因:响应头编码或文件编码不一致 → 解决:统一 UTF-8
Flask 默认按 UTF-8 输出,但如果你用某些编辑器把模板存成了 GBK,渲染出来就是乱码。排查顺序:先确认.html文件本身是 UTF-8 无 BOM;再确认app.py顶部没有奇怪的编码声明;最后看浏览器开发者工具里响应头的Content-Type是否带charset=utf-8。三处统一,乱码基本消失。
4.3 现象:查询报“database is locked” → 原因:连接没关或并发写冲突 → 解决:确保 close 并缩短事务
sqlite3 是文件级锁,写操作会锁库。如果某个视图开了连接却忘了conn.close(),或者在一个长事务里做了耗时操作,其他请求就会撞上锁。血泪经验是:每个请求内用完即关,写操作commit后立刻close,不要在视图里跨请求持有连接。并发量稍大时,这个限制会很明显,这也是 sqlite3 不适合高并发写入的根本原因。
4.4 现象:sqlite3.OperationalError: no such table→ 原因:工作目录不对或没初始化 → 解决:用绝对路径定位数据库
sqlite3.connect('data.db')是相对当前工作目录的。你在项目根目录启动没问题,换个目录启动就会新建一个空库,自然没有表。稳妥做法是用os.path拼出基于__file__的绝对路径:
import os BASE_DIR = os.path.dirname(os.path.abspath(__file__)) DB_PATH = os.path.join(BASE_DIR, 'data.db') conn = sqlite3.connect(DB_PATH)这样无论从哪个目录启动,连的都是同一个库文件。
4.5 现象:表单字段拿不到值报 KeyError → 原因:字段名拼错或没传 → 解决:用 get 兜底并核对 name 属性
request.form['title']在字段缺失时直接抛 KeyError,页面就是 500。两个检查点:HTML 里<input name="title">的 name 和代码里取的键是否完全一致(大小写、下划线都算);非必填字段用request.form.get('content', '')给默认值。养成这个习惯,能省掉大量低级报错。
5. 从 Demo 到能用:参数化查询、分页与部署前检查
5.1 把 SQL 注入堵死在参数化上
手敲 Demo 最容易留下的隐患是字符串拼接 SQL。看到"SELECT * FROM items WHERE id = " + str(id)这种写法,必须改成占位符。sqlite3 的?占位符会把参数当数据而非 SQL 片段处理,这是最省事也最有效的防线。
# 错误示范:拼接字符串,id 可控时直接注入 # cur.execute("SELECT * FROM items WHERE id = " + item_id) # 正确写法:参数化,类型和内容都由驱动处理 cur.execute("SELECT * FROM items WHERE id = ?", (item_id,)) row = cur.fetchone()注意占位符传参必须是元组,单参数也要写成(item_id,),少了逗号会被当成普通括号,报参数数量不匹配。这个细节我见过太多人栽。
5.2 给列表页加分页,别一次全查
数据一多,SELECT *全量渲染会越来越慢。sqlite3 支持LIMIT和OFFSET,加两个查询参数就能做最简分页:
@app.route('/list') def list_items(): page = request.args.get('page', 1, type=int) # 默认第 1 页 per_page = 10 offset = (page - 1) * per_page conn = get_db() rows = conn.execute( 'SELECT * FROM items ORDER BY id DESC LIMIT ? OFFSET ?', (per_page, offset) ).fetchall() total = conn.execute('SELECT COUNT(*) FROM items').fetchone()[0] conn.close() return render_template('list.html', items=rows, page=page, total_pages=(total + per_page - 1) // per_page)type=int让 Flask 自动把查询串转成整数,非法值回落到默认 1,省去手动校验。total_pages用向上取整算,模板里据此渲染上一页/下一页链接。分页参数别写死在代码里,抽成常量或配置,后面调每页条数时不用满文件找。
5.3 上线前必须走一遍的检查清单
开发服务器flask run只适合本地,别直接对外。部署前至少确认:debug=False,否则报错页会暴露源码和变量;数据库文件路径用绝对路径且做好备份;静态文件交给前置服务处理;表单加基本校验(非空、长度上限)。这份 Demo 的价值在于帮你把闭环跑通、把坑踩明白,真要上生产,连接管理、并发写入、备份策略都得重新设计。我自己的习惯是:每次改完路由或 SQL,先把增删改查四条路径手动走一遍,再去看日志里有没有 500,这个笨办法救过我很多次。希望这份拆解能帮你少走几步弯路,把 Demo 真正变成自己能改、能扩的底稿。
本文还有配套的精品资源,点击获取