ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

从零搭建Python个人博客:Flask+SQLite实战指南

从零搭建Python个人博客:Flask+SQLite实战指南 做个人博客网站用Python这套技术栈我觉得是目前性价比最高的选择。Python语法简单Web框架成熟Flask和Django都能在半小时内跑起一个能看的站点。今天这篇就手把手带你从零设计一个基于Python的个人博客网站从环境搭建到部署上线全程实操不整虚的。我自己就是从一个小博客开始一步步折腾成现在这个样子的所以这篇东西里的坑你早晚也会踩到提前帮你排掉。这篇内容适合谁刚学完Python基础、想找个练手项目的人或者想搭个个人网站但不想碰前端复杂工具链的朋友。看完这篇你会明白Flask怎么组织路由、模板怎么继承、SQLite怎么存储文章数据以及最后怎么把它扔上服务器。1. 项目概述与整体设计思路1.1 博客需求拆解哪些功能必须有做任何项目之前先别急着写代码。我见过太多人第一天就想要用户注册、评论系统、点赞收藏结果折腾半个月文章还没发出去一篇。个人博客的MVP最小可用产品其实就四个功能展示文章列表、查看文章详情、发布新文章、一个关于页面。这四个功能背后对应的是前后端最核心的三件事数据库存数据、路由取数据、模板渲染数据。把这三条线吃透后面加什么功能都只是叠砖头。我建议你把需求按优先级排个序第一优先级文章列表展示按时间倒序让人一进网站就能看到最新内容。第二优先级文章详情页点标题进正文排版干净。第三优先级发布/编辑文章哪怕只是一个裸的表单也必须有不然博客没法更新。第四优先级分类标签给文章打上“技术”“生活”之类的标签方便检索。第五优先级关于页放一段自我介绍让访客知道你是谁。后面像分页、搜索、评论、RSS订阅全部放到“二期再说”。这不是偷懒而是确保你能在一天内看到完整流程跑通有了正反馈才愿意继续深入。1.2 技术选型Flask SQLite为什么不选DjangoPython做Web的框架很多主流就是Flask和Django。很多人纠结我直接给你结论个人博客这种体量选Flask理由有三点。第一Flask轻。Django自带Admin后台、ORM、表单、认证体系功能全但很“重”初学者很容易被一堆概念绕晕。Flask的核心就一个路由分发其余靠扩展你想用哪个就装哪个心智负担小。第二Flask灵活。你可以自由选择模板引擎、数据库、表单库而不是被框架牵着走。第三Flask的生态对“小而美”的项目特别友好上手的官方文档也写得很清楚。数据库方面选择SQLite。它不是“玩具数据库”生产环境支撑一个小博客绰绰有余。SQLite是一个文件零配置不用安装独立服务直接写在磁盘上备份就是复制文件对于日流量几百的博客来说性能完全够。等以后真的规模大了再迁移到PostgreSQL也不迟因为SQLAlchemy把这一层屏蔽掉了换数据库的代码改动量很小。前后端配合的模式也很传统Flask后端返回HTML页面Jinja2模板负责填充数据Static目录放CSS和JS不搞前后端分离。原因很简单你写技术博客或者个人日志核心是内容不是交互。SPA那套东西等你需要的时候再引入初始阶段会拖慢你的开发速度。1.3 项目目录结构设计我习惯把项目在动手前就规划好目录。杂乱无章的结构会让你越写越乱最终弃坑。下面是我常用的个人博客项目结构你直接复制就行blog_site/ │ ├── app.py # 主应用入口注册路由 ├── models.py # 数据库模型定义 ├── requirements.txt # 依赖清单 ├── templates/ # Jinja2模板文件 │ ├── base.html # 基础模板定义页面骨架 │ ├── index.html # 首页文章列表 │ ├── post.html # 文章详情页 │ ├── about.html # 关于页 │ └── new_post.html # 发布文章页 ├── static/ # 静态资源 │ └── style.css # 样式表 └── venv/ # Python虚拟环境不进入版本管理这个结构有几个好处app.py和models.py分开不会在改模型的时候误触路由templates按模板名平铺小项目不需要blueprint如果以后功能多了再按模块拆static里暂时只放CSS等有图片、JS再分目录。重点提一下venv千万别把依赖装到全局环境不然换个项目你就哭。2. 环境准备与开发工具配置2.1 Python安装与虚拟环境创建如果你还没装Python先去官网下载安装包。Windows用户注意安装最后一步务必要勾选“Add Python to PATH”不然你在命令行里敲python会提示找不到命令。macOS和Linux用户一般自带但还是用python3 --version确认一下最好装到3.10以上版本老版本对类型注解和语法特性支持不好没必要给自己添堵。装好之后进到项目目录执行下面这两行命令python -m venv venv source venv/bin/activate # Windows下用 venv\Scripts\activate为什么非要虚拟环境因为不同项目依赖的包版本可能冲突比如A项目用Flask 2.2B项目用Flask 3.0全局装的话会造成互相覆盖。虚拟环境相当于给每个项目开一个独立的小房间里面的pip、Python版本互不干扰。激活之后命令行前面会出现(venv)字样这时候再装包就只装进这个环境里了。2.2 安装Flask依赖与VSCode开发环境配置激活虚拟环境后安装依赖pip install flask flask-sqlalchemy建议顺手把flask-sqlalchemy也装了因为后面操作数据库会用。另外再装一个python-dotenv方便管理环境变量现在用不上但提前备着。装完之后用pip list确认一下能看到包名和版本号就妥了。编辑器我推荐VSCode免费、插件生态好。装好之后去扩展商店装Python插件Microsoft官方那个它会自动识别虚拟环境。第一次打开项目时按CtrlShiftP输入Python: Select Interpreter选到刚才那个venv目录下的Python。这里有个细节如果选错解释器你写代码的时候会看到一堆红色波浪线但运行时又正常原因就是编辑器用的Python和你终端用的Python不是同一个很多人被这个坑过。再优化一下调试体验在.vscode/launch.json里配置Flask启动{ version: 0.2.0, configurations: [ { name: Python: Flask, type: python, request: launch, module: flask, env: { FLASK_APP: app.py, FLASK_ENV: development }, args: [run, --no-debugger, --no-reload] } ] }这里的FLASK_ENVdevelopment会开启调试模式改动代码后自动重载省去手动重启的步骤。2.3 验证环境跑通第一个Flask应用环境配好先做一次最小化验证别等到写了三屏代码才测试。在项目根目录建一个app.py写上from flask import Flask app Flask(__name__) app.route(/) def index(): return Hello, Blog! Your Flask is working. if __name__ __main__: app.run(debugTrue)然后运行python app.py浏览器访问http://127.0.0.1:5000能看到Hello, Blog!就说明环境没问题。如果端口5000被占用改成app.run(debugTrue, port5001)。如果浏览器连不上检查一下终端的输出有没有报错信息大概率是虚拟环境没激活或者解释器选错。这一步跑通后你就拥有了一个最基础的Web服务。接下来所有功能都是在这个基础上长出来的。3. 核心模块实现手把手写代码3.1 数据库模型用SQLAlchemy定义文章表博客的核心数据是文章我们将用SQLAlchemy定义一张posts表。这里我单独建一个models.py把模型和主应用分离清晰一些。# models.py from flask_sqlalchemy import SQLAlchemy from datetime import datetime db SQLAlchemy() class Post(db.Model): __tablename__ posts id db.Column(db.Integer, primary_keyTrue) title db.Column(db.String(200), nullableFalse) content db.Column(db.Text, nullableFalse) category db.Column(db.String(50), default随笔) created_at db.Column(db.DateTime, defaultdatetime.utcnow) def __repr__(self): return fPost {self.title}几个字段解释一下id是主键自动递增用来定位每一篇文章title设为String(200)标题再长也就这个数够用同时加nullableFalse表示这个字段不能为空防止你提交空的标题content用Text类型因为它可能是一大长篇category默认给个“随笔”省得每次写文章都要选分类。创建模型之后需要让它和二进制的数据库文件绑定。在app.py里初始化db并建表# app.py from flask import Flask, render_template, request, redirect, url_for from models import db, Post app Flask(__name__) app.config[SQLALCHEMY_DATABASE_URI] sqlite:///blog.db app.config[SQLALCHEMY_TRACK_MODIFICATIONS] False app.secret_key dev-key-change-me db.init_app(app) app.before_first_request def create_tables(): db.create_all()这里有个细节如果你用的是新版本Flaskbefore_first_request已经被废弃我建议直接在if __name__ __main__里执行db.create_all()像这样if __name__ __main__: with app.app_context(): db.create_all() app.run(debugTrue)因为建表是幂等操作重复执行不会有副作用所以每次启动前建一次表是最稳妥的。数据库文件blog.db是自动生成的你不需要也不能手动编辑它所有操作都通过SQLAlchemy完成。3.2 路由与视图实现首页、详情、分类、关于页路由是Web框架的灵魂。一个路由就像一个快递驿站用户访问某个URL比如/Flask把这个请求送到对应的函数处理函数拿到数据后渲染模板返回给浏览器。下面实现博客的四个核心路由。from datetime import datetime app.context_processor def inject_now(): return {now: datetime.utcnow()} app.route(/) def index(): posts Post.query.order_by(Post.created_at.desc()).all() return render_template(index.html, postsposts) app.route(/post/int:post_id) def post_detail(post_id): post Post.query.get_or_404(post_id) return render_template(post.html, postpost) app.route(/category/name) def category(name): posts Post.query.filter_by(categoryname).all() return render_template(index.html, postsposts, categoryname) app.route(/about) def about(): return render_template(about.html)你注意到post_detail里用了get_or_404这是一个隐藏的“避坑点”如果用户手动输入一个不存在的文章IDget_or_404会直接返回404页面而不是让你在模板里小心翼翼地判空。这种“防御式编程”能省掉很多麻烦。inject_now是模板上下文处理器这样在base.html底部就能直接用{{ now.year }}来显示当前年份符合版权信息习惯。3.3 模板与页面渲染Jinja2模板继承与代码复用模板的作用是把Python传来的数据填充进HTML页面。直接写裸HTML会非常冗余因为每个页面都有相同的导航栏和页脚Jinja2的模板继承机制完美解决这个问题。先写基础模板base.html!doctype html html langzh-CN head meta charsetutf-8 meta nameviewport contentwidthdevice-width, initial-scale1 title{% block title %}我的博客{% endblock %}/title link relstylesheet href{{ url_for(static, filenamestyle.css) }} /head body nav a href{{ url_for(index) }}首页/a a href{{ url_for(about) }}关于/a a href{{ url_for(new_post) }}写文章/a /nav main {% block content %}{% endblock %} /main footer© {{ now.year }} 我的小站/footer /body /html{% block %}就是占位符子模板只需要填充这些block不需要重复写head、nav和footer。url_for是Flask最常用的函数它根据视图函数名反推出对应URL这样一来哪怕你以后改了路由路径所有链接也会自动更新。比如/about这个URL对应的视图函数是about模板里写url_for(about)而不是硬编码href/about。然后是index.html{% extends base.html %} {% block title %}首页{% endblock %} {% block content %} h1{% if category %}分类{{ category }}{% else %}最新文章{% endif %}/h1 {% if posts %} ul classpost-list {% for post in posts %} li h2a href{{ url_for(post_detail, post_idpost.id) }}{{ post.title }}/a/h2 p classmeta{{ post.category }} · {{ post.created_at.strftime(%Y-%m-%d) }}/p /li {% endfor %} /ul {% else %} p还没有文章去写第一篇吧。/p {% endif %} {% endblock %}这里用strftime格式化日期只显示年月日干净不啰嗦。列表无内容时的“还没有文章”提示很重要不然访客会以为网站是坏的。post.html是文章正文页{% extends base.html %} {% block title %}{{ post.title }}{% endblock %} {% block content %} article h1{{ post.title }}/h1 p classmeta{{ post.category }} · {{ post.created_at.strftime(%Y-%m-%d %H:%M) }}/p div classcontent {{ post.content | safe }} /div /article pa href{{ url_for(index) }}← 返回首页/a/p {% endblock %}注意{{ post.content | safe }}这里有一个安全权衡。Jinja2会对变量做HTML转义防止用户提交的HTML代码直接渲染造成XSS攻击。但博客文章本来就是自己写的如果正文里写了b标签不过滤的话会原样显示出来。用了| safe过滤器之后Flask就会停止转义HTML标签才能正常解析。如果这篇文章是你一个人写的这个方方便没问题但如果你开放了用户投稿千万就别加safe了这个差异我在后面“常见问题”里还会重点提醒。about.html简单一点{% extends base.html %} {% block title %}关于{% endblock %} {% block content %} h1关于我/h1 p这里写你的自我介绍以及这个博客的初衷。/p {% endblock %}3.4 文章发布功能写一个能提交表单的页面一个字也不能少的核心功能写文章。先建一个new_post.html模板{% extends base.html %} {% block title %}写文章{% endblock %} {% block content %} h1写一篇新文章/h1 form methodpost p label fortitle标题/labelbr input typetext idtitle nametitle required maxlength200 /p p label forcategory分类/labelbr input typetext idcategory namecategory value随笔 /p p label forcontent正文/labelbr textarea idcontent namecontent rows15 required/textarea /p button typesubmit发布/button /form {% endblock %}然后补上对应的处理函数。在app.py里新增app.route(/new, methods[GET, POST]) def new_post(): if request.method POST: title request.form.get(title) content request.form.get(content) category request.form.get(category, 随笔) if title and content: post Post(titletitle, contentcontent, categorycategory) db.session.add(post) db.session.commit() return redirect(url_for(post_detail, post_idpost.id)) return render_template(new_post.html)这里面的逻辑很简单GET请求就显示表单页面POST请求就处理提交的数据。校验一下title和content非空然后写入数据库。这里有个原则要记住永远不要信任前端传来的数据。虽然HTML表单里写了required但那只是一种体验优化因为用户完全可以绕过前端直接构造POST请求。后端校验才是真正的安全防线所以我在这里用if title and content再检查了一遍。写完这四步一个能“新增文章、看文章列表、进文章详情”的博客就完整了。现在就差样式和部署了。4. 本地运行与调试技巧4.1 调试模式与自动重载运行命令是python app.py如果一切正常终端会输出* Serving Flask app app * Debug mode: on * Running on http://127.0.0.1:5000我通常会把app.run(debugTrue)这个开关一直开着因为Flask的调试模式有两大优势第一是热重载也就是改代码保存后服务自动重启不用频繁手动重启第二是报错页面更友好浏览器里会直接显示一条红色的报错堆栈还能用交互式调试器查看调用栈。不过注意调试模式只能用于本机开发如果你把服务部署到公网还开着debug等于把门钥匙挂在门上别人可以直接调用调试器接口存在严重安全风险。上线前务必要关闭debug改成app.run(host0.0.0.0, port8000)。4.2 常见报错排查实战根据我自己的踩坑经验新手最容易碰到的问题无非这几个而且几乎每天都会有新人踩到。我整理了一个速查表建议你直接收藏报错信息原因分析处理办法ModuleNotFoundError: No module named flask当前Python环境里没装Flask或者虚拟环境没激活执行source venv/bin/activate再pip install flaskjinja2.exceptions.TemplateNotFound: index.html模板文件不在templates目录下或文件名拼错检查templates/目录是否存在文件名是否一致sqlalchemy.exc.OperationalError: no such table: posts数据库初始化建表没有执行启动前先运行python -c from app import app; with app.app_context(): from models import db; db.create_all()OSError: [Errno 98] Address already in use端口被其他进程占用改用port5001或杀掉占用端口的进程Linux用lsof -i:5000找页面能打开但静态CSS加载不出来静态文件路径引用错了检查模板里是否用url_for(static, filenamestyle.css)而不是写死/static/style.cssGET /new 404路由没注册或methods没包含GET确认视图函数有methods[GET, POST]表单提交后报403CSRF保护机制介入暂时没加CSRF可以忽略以后加了就要在模板里加{{ csrf_token() }}排查问题时最有效的方法不是瞎猜而是看两个地方浏览器开发者工具F12的Network面板和终端里的日志输出。Network面板能告诉你请求是成功了还是404/500终端里能看到Python的异常堆栈。把这两个信息组合起来大部分问题都能定位。4.3 数据库与模板的实操心得数据库这块SQLite文件在项目根目录叫blog.db。开发阶段你可以随时删除它再重新创建但发布文章后就别随便删了不然文章全丢。我建议你每次大改动前备份一份cp blog.db blog_backup.db。还有一个容易忽视的点SQLAlchemy默认的主键是自增整数但如果你手滑删了一些记录再用get_or_404访问那些被删除的ID会正常返回404这没问题。另外如果你以后加了“评论”功能也建议用同样的模型方式新增一张表然后通过外键关联到PostSQLAlchemy会处理好这些关系。模板方面我有一个“永远不要手动拼链接”的原则。写完一个功能后一定要在页面源码里检查一下链接到底指向哪用url_for会保证绝对不出错。还有一个好的习惯是把重复的布局尽量都收敛到base.html里比如未来想给右侧加一个侧边栏只需要改base.html就够了所有页面同步生效。5. 从个人站到公网部署上线与进阶扩展5.1 用gunicorn把Flask跑起来本地的app.run()只能用来开发因为werkzeug自带的服务器性能很弱扛不住真正的流量。生产环境需要用一个正式的WSGI服务器。Linux服务器上我用的是最常见的gunicorn。先在项目目录安装pip install gunicorn然后运行gunicorn -w 4 -b 0.0.0.0:8000 app:app这里的-w 4表示启动4个worker进程-b指定监听地址app:app指的是“app.py文件里的app实例”。4个worker意味着能同时处理4个并发请求对于个人博客来说绰绰有余。如果部署环境不支持gunicornWindows服务器也可以用waitress用法和gunicorn很相似。注意部署时你的app.py里的app.run()只会在直接执行python app.py时才运行使用gunicorn启动时不会走那段代码所以不用担心端口冲突。5.2 Nginx反向代理与静态文件处理光有gunicorn还不够我习惯在前面再加一层Nginx理由是Nginx处理静态文件CSS、图片的速度比Python快得多同时还能做HTTPS终止和日志记录。Nginx配置核心部分是server { listen 80; server_name yourdomain.com; location /static { alias /path/to/blog_site/static/; } location / { proxy_pass http://127.0.0.1:8000; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } }明确分开所有以/static/开头的请求由Nginx直接读取磁盘文件返回不经过Flask其他动态请求转发给gunicorn。这样你就真正把博客发布到了公网。反向代理还有个好处以后加HTTPS证书只需要在Nginx层配置不用改动Python代码。5.3 下一步扩展分页、搜索、评论博客跑起来后你一定会觉得“不够用”想加东西。我给你一条清晰的扩展路径先是列表页没有分页文章多了往下滑很长就在index路由里加page参数用Flask-SQLAlchemy的paginate方法返回Pagination对象模板里渲染页码链接接着加站内搜索直接用SQLAlchemy的filter做模糊匹配Post.query.filter(Post.title.contains(keyword)).all()注意用contains而不是裸式LIKE这样会更安全然后加评论你需要新建Comment模型并关联Post的外键评论提交后做必要的审核不然很容易被垃圾评论轰炸。还有进阶一点的玩法用flask-caching给首页做缓存减少数据库查询用flask-sitemap生成XML地图方便搜索引擎收录用flask-admin放弃手写的文章管理页面直接使用现成的后台界面。关于“python量化交易策略代码”这种热搜说明很多人想用Python做点自动化的事其实和个人博客并不冲突。你自己有了一个站就可以顺手写点小工具脚本挂上去比如定时抓取数据生成报表展示在首页这也是Python技术栈的魅力所在——前后端通吃没有断层。我个人回过头看这个博客项目最值钱的部分不是代码而是“怎么把一个想法拆成可执行的步骤”。数据库模型想清楚、路由想清楚、模板想清楚剩下就是填代码。靠这个思路你后面学什么框架都不怕。最后提个醒处理好日常备份博客时代最惨的不是写不出文章而是服务器格式化后文章全没了。把SQLite文件和templates目录一起用云盘或Git仓库管起来每天自动备份一次这比任何花哨功能都重要。
返回列表