ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

Flask博客开发实战:从数据模型到gunicorn部署

Flask博客开发实战:从数据模型到gunicorn部署 简介Python Flask 个人博客网站毕业设计源码包是一个注重内容创作的轻博客系统面向计算机相关专业学生的毕设、课设及 Flask 全栈学习也可作为课程设计演示和 Web 入门进阶的参考项目。项目采用 Flask 框架与 Bootstrap4 模板集成 flask-wtf、flask-sqlalchemy、markdown、mysql 等技术覆盖文章发布与管理、分类标签、推荐文章、内置图床、网站设置、会员注册与邀请码等完整功能支持 Markdown 写作与图片管理后台结构清晰。压缩包共 494 个文件2.66MB包含 15 个 Python 后端源码、140 个 HTML 页面、197 个 JS 与 62 个 CSS 前端资源、SQL 数据库文件及 Markdown/TXT 说明文档目录结构清晰便于按模块阅读、调试和二次开发。管理员与用户账号已内置admin/admin、dashen/123456代码经过测试运行成功答辩评价较高目前已有 349 人学习下载适合快速启动项目。对希望快速搭建个人博客或掌握 Flask 项目完整开发流程的读者这份源码提供了可直接运行的项目原型和参考实现也可根据需求扩展模块降低从零起步的实现成本。1. 这个 Python Flask 博客项目拆开来看是什么“基于 Flask 框架的个人博客网站 源代码 文档说明 数据库”这种打包标题在课程设计和毕业设计里出现频率很高但它真正的价值不在“博客”两个字而在于它是 Web 开发里最标准的练手骨架Python 做后端、Flask 做路由和视图、SQLite 或 MySQL 做持久化前端用 Jinja2 模板直接渲染。它的核心问题永远是这四件套能不能对上一套源代码放哪、数据库表怎么建、文档里说的启动方式是否有效、预置数据能不能直接跑起来。这篇文章顺着这四个文件讲清楚 Flask 博客从模型设计到部署验证的完整路径适合刚学完 Flask 基础、准备动手写课程设计的人也适合想快速搭内部知识库或作品集的在职开发。2. Flask 博客项目骨架与数据模型先把目录定成能长个子的形状拿到代码包后别先运行先看目录。这类项目最常见的组织方式是 app 工厂模式加蓝图拆模块。Flask 自身不限制目录结构但一个博客要同时承载前台文章列表、后台管理、登录认证、评论回复如果所有路由堆在一个 app.py 里三个月后你自己都改不动。我一般会按“入口、配置、应用包、迁移脚本、静态文件”五块来摆放。2.1 用 app 工厂模式组织项目启动脚本只保留入口把项目的可扩展性放在第一位第一件事是让 run.py 只负责创建实例和启动服务blog/ ├── run.py # 启动入口 ├── config.py # 所有配置项集中管理 ├── requirements.txt # 依赖清单 ├── README.md # 文档说明 ├── project.db # SQLite 数据库文件 ├── app/ │ ├── __init__.py # create_app 工厂 │ ├── models.py # 数据模型 │ ├── views/ │ │ ├── __init__.py │ │ ├── auth.py # 登录登出蓝图 │ │ └── blog.py # 文章、分类、后台蓝图 │ ├── templates/ │ └── static/ └── migrations/ # Flask-Migrate 生成的迁移目录核心工厂函数如下注意注册蓝图时才导入视图原因是要等 app 初始化完再绑定路由否则会出现循环导入的报错。from flask import Flask from flask_sqlalchemy import SQLAlchemy from flask_login import LoginManager db SQLAlchemy() login_manager LoginManager() login_manager.login_view auth.login def create_app(config_classNone): app Flask(__name__) if config_class: app.config.from_object(config_class) else: app.config[SECRET_KEY] dev-change-me app.config[SQLALCHEMY_DATABASE_URI] sqlite:///project.db app.config[SQLALCHEMY_TRACK_MODIFICATIONS] False db.init_app(app) login_manager.init_app(app) from .views.auth import auth_bp from .views.blog import blog_bp app.register_blueprint(auth_bp, url_prefix/auth) app.register_blueprint(blog_bp) return app这里有几个参数值得说明SECRET_KEY直接决定了 session 的安全性代码包里往往写着明文上生产前必须换成环境变量注入SQLALCHEMY_DATABASE_URI决定数据库落盘位置相对路径写法容易受工作目录影响通常建议用os.path.join(BASE_DIR, project.db)拼出绝对路径login_view告诉 Flask-Login 未登录用户该跳转到哪个端点。调试时如果发现登录后跳转地址不对优先检查这个配置项是否与蓝图的url_prefix对齐。2.2 用 SQLAlchemy 定义文章、分类、标签和用户四张表博客模型的核心是一对多和多对多关系。文章属于一个分类分类有一对多文章文章和标签之间是多对多需要用关联表。用户表可以简化为只存管理员账号但字段设计要为后续加评论留余地。from datetime import datetime from flask_login import UserMixin from werkzeug.security import generate_password_hash, check_password_hash from . import db, login_manager post_tags db.Table( post_tags, db.Column(post_id, db.Integer, db.ForeignKey(post.id), primary_keyTrue), db.Column(tag_id, db.Integer, db.ForeignKey(tag.id), primary_keyTrue) ) class User(UserMixin, db.Model): id db.Column(db.Integer, primary_keyTrue) username db.Column(db.String(64), uniqueTrue, nullableFalse) password_hash db.Column(db.String(256), nullableFalse) def set_password(self, password): self.password_hash generate_password_hash(password) def check_password(self, password): return check_password_hash(self.password_hash, password) class Category(db.Model): id db.Column(db.Integer, primary_keyTrue) name db.Column(db.String(64), uniqueTrue, nullableFalse) posts db.relationship(Post, backrefcategory, lazydynamic) class Tag(db.Model): id db.Column(db.Integer, primary_keyTrue) name db.Column(db.String(64), uniqueTrue, nullableFalse) class Post(db.Model): id db.Column(db.Integer, primary_keyTrue) title db.Column(db.String(128), nullableFalse) slug db.Column(db.String(128), uniqueTrue, indexTrue) content db.Column(db.Text, nullableFalse) created_at db.Column(db.DateTime, defaultdatetime.utcnow, indexTrue) updated_at db.Column(db.DateTime, defaultdatetime.utcnow, onupdatedatetime.utcnow) is_published db.Column(db.Boolean, defaultTrue) views db.Column(db.Integer, default0) category_id db.Column(db.Integer, db.ForeignKey(category.id), nullableFalse) user_id db.Column(db.Integer, db.ForeignKey(user.id), nullableFalse) tags db.relationship(Tag, secondarypost_tags, backrefdb.backref(posts, lazydynamic))category_id和user_id是外键删除分类时文章会被约束卡住所以管理后台删除分类前要先处理该分类下的文章归属。lazydynamic表示category.posts返回查询对象而不是列表后续可以继续调用.filter_by().order_by()。slug用于文章详情页的 URL比用数字主键更利于 SEO 和分享生成逻辑可以依赖uuid或拼音库不建议把中文标题直接拼进 URL。2.3 数据库初始化与预置数据一个脚本解决建表和导入主键自增、索引、外键这些在 SQLAlchemy 模型里声明完后初始化数据库只需要两行命令。注意数据库文件不要提交进 git 仓库但课程设计打包时通常会把带数据的project.db一起发所以文档里要写明初始账号密码。flask db init flask db migrate -m create post tables flask db upgrade如果项目没接 Flask-Migrate也可以直接在flask shell里执行flask shellfrom app import db from app.models import User, Category, Post db.create_all() admin User(usernameadmin) admin.set_password(admin123) db.session.add_all([ admin, Category(namePython), Category(nameFlask), ]) db.session.commit()把这段逻辑放进独立的init_db.py比写在视图里干净。给分类预置两条数据是为了让文章发布页的下拉框不为空set_password用的是 werkzeug 的generate_password_hash底层是 pbkdf2默认带盐比把明文密码存在数据库里安全一个数量级。数据库字段设计直接决定了后面能不能加评论、加草稿箱、加标签云所以第一步别偷懒关联关系和索引位都要预先留好。3. 把博客的增删改查跑通蓝图、登录和分页查询数据表建好后进入业务代码阶段这部分要做到“能跑、能写、能算”。博客前台至少有文章列表、文章详情、分类归档三个页面后台至少有发布文章、编辑文章、删除文章三个动作。用蓝图把前台和后台分开URL 语义才清晰权限控制也有地方挂。3.1 用蓝图区分前台和后台URL 前缀避免路由混乱前台和后台是两个权限域不能用同一个视图函数里if 判断当前用户是否管理员来区分会越写越乱。正确做法是拆成两个蓝图后台挂在/admin前缀下。# app/views/__init__.py from flask import Blueprint blog_bp Blueprint(blog, __name__) auth_bp Blueprint(auth, __name__)# app/views/auth.py from flask import render_template, redirect, url_for, flash, request from flask_login import login_user, logout_user, login_required from app import db from app.models import User from app.views import auth_bp auth_bp.route(/login, methods[GET, POST]) def login(): if request.method POST: username request.form.get(username) password request.form.get(password) user User.query.filter_by(usernameusername).first() if user and user.check_password(password): login_user(user, rememberrequest.form.get(remember) on) return redirect(request.args.get(next) or url_for(blog.index)) flash(用户名或密码错误) return render_template(login.html)request.args.get(next)是登录后回跳的参数Flask-Login 在拦截未登录请求时会自动带上?next原地址这里如果直接信任next会存在开放重定向风险生产环境要校验它是以/开头的站内路径。remember参数控制长效 cookie过期时间可以在 Config 里用REMEMBER_COOKIE_DURATION调整默认 365 天偏长个人博客一般改到 7 天。3.2 文章列表的分页查询与模板渲染参数博客首页是整站的流量入口查询要控制每页数量不能一页加载全部文章。Flask-SQLAlchemy 的paginate方法接收 page 和 per_page 两个参数error_outFalse保证越界页码返回空列表而不是直接抛 404。from flask import render_template, request from sqlalchemy import or_ from app.models import Post from app.views import blog_bp blog_bp.route(/) def index(): page request.args.get(page, 1, typeint) keyword request.args.get(kw, ).strip() query Post.query.filter_by(is_publishedTrue) if keyword: like f%{keyword}% query query.filter( or_(Post.title.like(like), Post.content.like(like)) ) pagination query.order_by(Post.created_at.desc()).paginate( pagepage, per_page10, error_outFalse ) return render_template(index.html, postspagination.items, paginationpagination)这里要注意两点request.args.get(page, 1, typeint)第三参数直接做类型转换用户手滑传pageabc时会安全回落成 1比手动int()包一层 try 更稳妥or_条件做标题和内容的模糊搜索个人博客数据量在几千篇以内这是最可靠的中文检索方案。Flask-SQLAlchemy 3.x 中paginate返回的pagination对象自带iter_pages()方法模板里能直接渲染数字分页条。如果像很多课程设计那样在模板里写死上一页/下一页注意当pagination.has_prev为 False 时要禁用链接否则用户会在第一页点着没反应的按钮。3.3 发布和编辑文章表单校验与权限控制后台管理界面要用模板把新建和编辑两个场景合并判断依据是post对象是否存在。表单页至少包含标题、分类、标签、正文、是否发布五个字段。# app/views/blog.py from flask import render_template, redirect, url_for, abort from flask_login import current_user from functools import wraps from sqlalchemy.exc import IntegrityError from app import db from app.models import Post, Category, Tag from app.views import blog_bp def admin_required(f): wraps(f) from flask_login import login_required login_required def wrapper(*args, **kwargs): if not current_user.is_authenticated: abort(401) return f(*args, **kwargs) return wrapper blog_bp.route(/admin/post/new, methods[GET, POST]) admin_required def new_post(): if request.method POST: title request.form.get(title) content request.form.get(content) slug request.form.get(slug) category_id request.form.get(category_id) is_published request.form.get(is_published) on post Post( titletitle, contentcontent, slugslug or None, category_idcategory_id, is_publishedis_published, user_idcurrent_user.id, ) db.session.add(post) try: db.session.commit() except IntegrityError: db.session.rollback() return slug 已存在请更换后重试, 400 return redirect(url_for(blog.detail, slugpost.slug)) return render_template(admin_post.html, categoriesCategory.query.all())db.session.commit()可能抛IntegrityError典型场景是 slug 撞车或外键不存在必须捕获后rollback()否则后续所有数据库操作都会在脏 session 上继续执行。权限装饰器里current_user.is_authenticated不能省略因为login_required只管有没有登录不管登录的是不是管理员。多用户场景可以给 User 加is_admin字段但单管理员博客判断 authenticated 就够用了。中间还缺了初始化时的login_manager.user_loader这是 Flask-Login 必配项写在app/__init__.py内login_manager.user_loader def load_user(user_id): from .models import User return User.query.get(int(user_id))没有这段代码每次请求 session 里的用户 id 都还原不成 User 对象current_user会一直是匿名用户表现为“登录后立刻退回登录页”。4. 让数据库真正活下来迁移、SQLite 并发与备份同步数据库文件是这套博客项目的生命线。课程设计的评分点、你部署后的文章数据全在这一个project.db里。这一章解决三个实际问题表结构后期怎么改、SQLite 并发读写怎么保护、数据库怎么备份和迁移到别的机器。4.1 用 Flask-Migrate 管理 schema 变更别手动 ALTER TABLE新手最容易犯的错是发现要加一个字段直接用 GUI 工具打开数据库文件执行ALTER TABLE。这样做在开发环境试没问题但迁移脚本一旦手动改过就没法在服务器上复现同样结构了。安装并接入 Flask-Migratepip install flask-migrate# app/__init__.py 中追加 from flask_migrate import Migrate migrate Migrate(app, db)以后每次模型变更执行三步flask db migrate -m add post.views column flask db upgradeflask db migrate会对比 models.py 里的模型和当前数据库的差异自动生成迁移文件flask db upgrade把迁移应用到数据库。迁移文件生成后先打开看一眼再执行确认没有误删列。数据库同步到服务器时直接拷贝project.db文件不够规范因为 SQLite 会有 WAL 文件未合并的情况正确做法见 4.2。4.2 SQLite 生产化配置WAL 模式与 busy_timeoutSQLite 并不是“玩具数据库”个人博客的读写量它能扛住但默认的 rollback journal 模式在并发读取时性能较差。我的常用做法是在建连阶段执行三条 PRAGMAfrom sqlalchemy import event from sqlalchemy.engine import Engine event.listens_for(Engine, connect) def set_sqlite_pragma(dbapi_connection, connection_record): cursor dbapi_connection.cursor() cursor.execute(PRAGMA journal_modeWAL) cursor.execute(PRAGMA foreign_keysON) cursor.execute(PRAGMA busy_timeout10000) cursor.close()journal_modeWAL允许读操作和写操作并发进行是 SQLite 在高并发读场景下的核心优化foreign_keysON让外键约束真正生效SQLAlchemy 模型上的db.ForeignKey写不写这两行在 SQLite 里默认是不检查的很多人删分类时没报错正是因为没开这个开关busy_timeout10000让数据库在锁冲突时等 10 秒而不是立刻抛database is locked。表结构稳定后用.backup命令做在线备份这是 SQLite 官方推荐的备份方式比直接复制文件安全能拿到一致快照sqlite3 project.db .backup /backup/blog_$(date %F).db配一条 crontab 每天凌晨执行即可做到“数据库同步”。注意备份文件要存放在项目目录之外否则迁移项目时会把备份也拷走失去异地容灾的意义。4.3 打标签、搜索和浏览计数三处低成本的实用改造标签和文章的多对多关系建好后文章详情页只需要在模板里遍历post.tags就能显示标签。为每篇文章加浏览量字段后更新计数时不要整个对象提交那样会把无关的 updated_at 也改掉Post.query.filter_by(idpost_id).update({Post.views: Post.views 1}) db.session.commit()全文搜索不建议贸然用 SQLite FTS5原因是它对中文默认没有合适的分词器MATCH中文关键词经常搜不出结果。几千篇以内的博客用LIKE %关键词%就够量大以后再换 Elasticsearch。这里也顺带解释一个模棱两可的说法数据库同步工具并不是解决 schema 变更的工具它解决的是主从复制和多机一致性问题单机 SQLite 博客用不上主从做好备份就够了。5. 部署验证用 gunicorn 启动服务并检查发布是否真的活了5.1 用 gunicornLinux或 waitressWindows接管生产服务python run.py启动的是 Flask 自带的开发服务器它带 reloader 和 debugger但性能和稳定性都不适合对外提供服务并发请求稍多就会出现阻塞。Linux 服务器上最常见的做法是 gunicornWindows 服务器则用 waitress两者用法等价且都支持多 workerpip install gunicorn gunicorn -w 2 -b 0.0.0.0:8000 --access-logfile - --error-logfile - run:app-w 2表示两个 worker 进程对 SQLite 博客来说这是比较稳妥的值太少扛不住并发太多会导致 SQLite 写入锁竞争明显。run:app指定导入路径gunicorn 要求必须是“模块:变量”的形式所以工厂函数要提前在run.py里实例化成全局app。--access-logfile -把访问日志输出到标准输出systemd 或 docker 部署时方便统一收集。不要直接在 gunicorn 命令行参数里写--reload生产环境代码变更要用git pull加重启进程的方式发布开着自动重载容易被语法错误打挂服务。Nginx 反代127.0.0.1:8000做静态文件和 HTTPS 终结这才是完整的生产拓扑。5.2 用 curl 验证“发布是否真的活了”部署最后一步是验证不是看着 gunicorn 启动成功就完事。我从三个层面检查curl -s -o /dev/null -w HTTP %{http_code} - %{time_total}s\n https://你的域名/状态码 200 和返回时间能确认前端可访问。第二步验证登录和管理功能注意真实项目中开启 CSRF 后直接 POST 表单会 400要先 GET 登录页提取 tokenCSRF$(curl -s -c /tmp/cookie.txt http://127.0.0.1:8000/auth/login | grep -oP namecsrf_token value\K[^]) curl -s -b /tmp/cookie.txt -c /tmp/cookie.txt \ -d csrf_token$CSRFusernameadminpassword你的密码 \ -e http://127.0.0.1:8000/auth/login \ -o /dev/null -w login: %{http_code} - %{redirect_url}\n \ http://127.0.0.1:8000/auth/login-e指定 Referer 头有些 CSRF 校验会检查来源%{redirect_url}能看到登录成功后 302 跳到了哪个地址如果跳回登录页就说明user_loader或 session 配置有问题。最后验证静态资源路径打开浏览器开发者工具看 Network 里的 404 和favicon.ico再查看 gunicorn error log 是否出现Traceback。上线后每周用curl -sI巡检一次对博客这类低频更改的站点来说比任何监控告警都实在。本文还有配套的精品资源点击获取
返回列表