首页 / 资讯中心 / 文章详情

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

Flask+SQLite3手敲Demo:构建Web开发完整闭环 ★ FEATURED ARTICLE
简介一份面向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 更适合手敲起步对比 DjangoFlask 没有强制性的项目结构和数据模型配置一个.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, itemsrows) if __name__ __main__: app.run(debugTrue)这段代码里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_idGET切换完成状态/delete/int:item_idPOST删除待办有人会把新增和编辑合在一起共用一个路由通过隐藏字段区分。这个策略在复杂项目里能减少代码量但在小 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 headtitle新增待办/title/head body form methodpost action{{ url_for(add) }} input typetext nametitle placeholder标题 required textarea namecontent placeholder备注内容/textarea button typesubmit提交/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_iditem[id]) }} 切换状态 /a form methodpost action{{ url_for(delete, item_iditem[id]) }} button typesubmit删除/button /form /li {% endfor %}这里的url_for(check, item_iditem[id])展示了带参路由的写法第一个参数是视图函数名后面的关键字参数对应路由中的int:item_id。由于数据库连接设置了row_factory sqlite3.Rowitem[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 useFlask 起不来。原因上一次运行没有正常终止系统还保留了 5000 端口或者机器上别的程序占了默认端口。Flask 默认监听 5000 端口它不是所有机器的真空地带。解决换端口启动或者找出占用进程。临时方案是把app.run()改成app.run(port5001)。找占用进程的命令各行其道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, filenameapp.js) }}?v2/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 是请求方法对不上路由声明的方法典型原因是表单里写了methodget但后端路由只声明了POST400 则通常是缺少了路由中定义的关键参数或表单字段名不匹配。解决先在后端起调试模式跑请求进来时看终端输出的 method 和 path。检查request.form.get()里写的字段名与 HTML 中name属性是否一致。前后端字段名的错位是手工写模板最常见的失误没有之一。5.5 中文乱码现象模板里写的中文在浏览器里变成Ã¥之类乱码。原因绝大多数情况下是 HTML 文件没有声明 UTF-8 编码Flask 渲染时无法正确解码。也有一部分情况是浏览器本身编码猜测错误。解决所有 HTML 文件的head里加一行meta charsetutf-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 relstylesheet href{{ url_for(static, filenamestyle.css) }}。这样静态文件也会经过 Flask 的统一处理目录结构再调整也不怕断链。第三部署前的收尾检查。关闭debugTrue是必须的一步——很多框架内置调试器会在运行时对请求做代码级别的检查如果暴露到对外环境任何访问者都能拿到代码执行入口。使用app.run(host127.0.0.1, port5000)让应用只监听本机回环地址不暴露到局域网。验证完整性的一个方式是命令行检查所有路由的状态python -c from app import app; [print(rule) for rule in app.url_map.iter_rules()]每一条路由的输出都确认存在再手动跑一遍猜的 URL 流程基本就收尾完成了。从那以后我每次写完一个小 Demo都会强制走一遍加会话、检查静态资源引用、关闭 debug 这三件事宁可多花十分钟也不愿意在演示的时候被一个 404 卡住。希望帮到你。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?
咨询建站