ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

Python网页开发实战:从Flask博客到上线部署全流程

2026/9/28 5:18:37 拓冰建站 浏览量
Python网页开发实战:从Flask博客到上线部署全流程 我一直觉得学Python的人到了某个阶段心里都会冒出一个念头我想做个自己的网站。有人学了爬虫能把数据抓下来有人学了数据分析能做出一堆漂亮的图表有人刚把语法啃完天天看教程看到头晕。可一旦想动手做点真正能用的东西就卡住了——不知道从哪里下手。就拿网页设计这四个字来说群里十个人有九个会反问Python不是做爬虫和数据分析的吗怎么写网页这里得先把一个误解掰清楚单独的Python写不了一个好看的网页因为网页的视觉层面靠的是HTML、CSS和前端技术。但网站的灵魂——谁来决定显示什么内容、用户提交的数据去哪里、点击按钮之后发生了什么——这些全都要靠Python这样的后端语言来完成。所以基于Python的网页设计实操下来其实是Python后端 前端页面的组合拳。这篇文章我就拿一个个人博客网站当例子把这个组合拳从头到尾拆给你看从框架选型、数据库设计、页面模板到上线部署每一步都带着原因讲保证你能在自己的电脑上跑起来甚至部署到服务器让别人访问。这个项目适合谁适合学过Python基础语法或者爬虫、但没接触过Web开发的人也适合学校要交网页设计作业、前端想学但后端一片空白的同学当然你要是就想搭个属于自己的博客把笔记、日记、作品放上去这更是现成的路子。1. Python写网页的正确打开方式先聊需求和选型1.1 个人博客网站最少要满足哪些需求做任何项目前我都习惯先把功能边界写在纸上。个人博客说是个人但麻雀虽小五脏俱全。一个最小的可运行版本至少要满足这么几件事首页能看到文章列表按时间倒序排点进某篇文章能看到完整的标题、发布时间和正文能写新文章写错了也能改能删掉不要的文章页面要像样不能裸奔把这些需求翻译成功能清单大概长这样功能对应页面核心逻辑文章列表首页从数据库按时间倒序查询文章详情详情页根据文章ID查询单条记录写文章编辑页表单提交插入数据库修改文章编辑页表单回填更新记录删除文章列表页/详情页按ID删除记录页面样式全部页面模板CSS统一渲染我做这个项目的时候故意把功能砍到了最低限度。为什么因为功能做得多坑也多初学者很容易在中途放弃。先把主链路跑通之后加标签、加评论、加分类这些都是水到渠成的事。你如果一上来就想着我要做一个带多用户、带评论回复、带暗黑模式的大平台那这个项目大概率烂尾。1.2 Flask和Django选哪个提到Python写网站绕不开两个名字Flask和Django。这两者的定位差别很大用一句话概括——Django像精装修交付的房子你拎包入住什么都有但什么是钢筋混凝土、什么是承重墙你不一定清楚Flask是毛坯房水电管线给你预埋好了剩下的格局你自己砌。选Django还是Flask取决于场景。Django自带后台管理、ORM、认证体系适合功能复杂的项目但它的学习曲线比Flask陡不少对初学者来说光是理解项目和应用的区别就够喝一壶的。个人博客这种单机小玩具用Django有一种杀鸡用牛刀的感觉。我推荐Flask理由很现实学习成本低。Flask的核心就一个app对象加上几个装饰器花十分钟就能写出Hello World。结构自由。你想怎么组织代码就怎么组织适合理解Web开发背后的运行机制。文档友好。官方文档和教程非常多遇到问题一搜一大把。当然如果你以后的职业方向就是重型业务系统Django依然值得学我自己也是后来才补的Django。但作为第一个上手项目Flask真的是最合适的入口。2. 搭骨架项目目录、数据库表与Flask初始化2.1 项目目录为什么要这样分先看目录结构blog/ ├── app.py # Flask应用入口 ├── blog.db # SQLite数据库文件首次运行自动生成 ├── requirements.txt # 依赖清单 ├── static/ # 静态资源CSS、图片、JS │ ├── css/ │ │ └── style.css │ └── images/ └── templates/ # HTML模板 ├── base.html ├── index.html ├── post.html └── edit.html这个结构不是随便拍的。我把代码逻辑和展示层分开app.py只管后端逻辑templates目录存放所有HTML页面static目录放CSS和图片。这样做的核心原因是职责分离——改样式你不至于动到Python代码后端逻辑变化也不会牵连前端页面。很多初学者喜欢把HTML写在Python字符串里比如用print拼一段HTML这种写法在你只有一页时没问题但一旦页面多起来就是灾难。模板系统之所以存在就是为了让HTML回到HTML该待的地方。2.2 数据库为什么用SQLite数据存储我选了SQLite。理由还是那个字简单。SQLite是Python内置支持的关系型数据库不需要单独安装服务端一个.db文件就是一个库增删改查用的还是标准SQL。单用户、低并发的个人博客用它绰绰有余。等你以后访问量上来了再迁到MySQL也不迟。表结构我设计得很精简CREATE TABLE posts ( id INTEGER PRIMARY KEY AUTOINCREMENT, title TEXT NOT NULL, content TEXT NOT NULL, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP );就这么一张表没有第二张。id是自增主键title存标题content存正文created_at自动记录发布时间。有人可能会问不加分类吗不加标签吗我再说一次——第一版不要想那么多。分类和标签本质上都是多对多关系在SQLite里要用关联表平白多出两层复杂度。等你把基础版跑通了后面给的扩展方案里有现成的路。2.3 Flask初始化与建表接下来写app.py。先处理环境依赖pip install flask这里我强烈建议你先创建一个虚拟环境再安装python -m venv venvWindows下激活命令是venv\Scripts\activatemacOS/Linux下是source venv/bin/activate。虚拟环境能避免项目之间的依赖冲突我见过太多人把所有包都装进全局环境里最后升级一个库把另一个项目搞挂的。这个习惯越早养成越好。如果你发现自己网络下载太慢也可以临时用国内镜像源安装pip install flask --index-url https://pypi.tuna.tsinghua.edu.cn/simple然后是app.py基础部分import os import sqlite3 from flask import Flask, render_template, request, redirect, url_for, abort app Flask(__name__) BASE_DIR os.path.dirname(os.path.abspath(__file__)) DB_PATH os.path.join(BASE_DIR, blog.db) def get_db(): conn sqlite3.connect(DB_PATH) conn.row_factory sqlite3.Row return conn def init_db(): with sqlite3.connect(DB_PATH) as conn: conn.execute( CREATE TABLE IF NOT EXISTS posts ( id INTEGER PRIMARY KEY AUTOINCREMENT, title TEXT NOT NULL, content TEXT NOT NULL, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ) ) if __name__ __main__: init_db() app.run(debugTrue)这里有几个细节我特意说明一下都是新手容易踩的点。第一conn.row_factory sqlite3.Row这行很容易被忽略但很重要。它让查询结果像字典一样通过字段名取值而不需要用数字下标。比如查出来的row你不用写成row[1]然后心里默念第1列是标题而是直接写row[title]代码可读性完全不一样。第二数据库路径必须用os.path.join(BASE_DIR, ...)。我见过很多新手踩的坑是直接用相对路径blog.db结果程序在别的目录运行时数据库文件跑到别的地方去了明明写入成功却找不到数据。第三我把init_db()放在运行入口而不是模块顶部是为了避免每次导入app.py都去建表只有真正执行主程序时才初始化。好骨架有了接下来实现功能。3. 博客的核心功能文章列表、详情、写作与编辑3.1 路由设计一个页面背后是什么先说路由。Flask里最核心的概念就是路由通俗点说就是用户访问某个URL时对应执行哪段Python函数。我的路由表如下URL方法功能/GET文章列表/post/ int:post_idGET文章详情/newGET / POST写新文章/edit/ int:post_idGET / POST编辑文章/delete/ int:post_idGET删除文章在Flask里同一个URL可以用不同的HTTP方法GET就是浏览器直接访问POST是提交表单这样一页两用很常见。写文章的/new页面GET进来是展示空表单POST提交才是执行插入数据库的逻辑。3.2 首页把文章列表查出来app.route(/) def index(): conn get_db() posts conn.execute(SELECT * FROM posts ORDER BY created_at DESC).fetchall() conn.close() return render_template(index.html, postsposts)代码只有三行但逻辑很清晰连接数据库按时间倒序把所有文章查出来交给模板渲染。注意我每次都手动conn.close()这是SQLite使用的好习惯。连接资源不释放时间长了会累积文件句柄尤其是开发模式下debugTrue频繁触发重载问题会更明显。如果你嫌每次写close烦可以用上下文管理器把这段逻辑封装一下这就是后话了。3.3 详情页404怎么处理app.route(/post/int:post_id) def post_detail(post_id): conn get_db() post conn.execute(SELECT * FROM posts WHERE id ?, (post_id,)).fetchone() conn.close() if post is None: abort(404) return render_template(post.html, postpost)这里有个新手容易忽视的点用户可能直接输入/post/999访问一个不存在的文章ID如果什么都不处理模板里收到的post可能是None页面渲染的时候就会报错。我加了abort(404)让不存在的文章返回一个标准的404页面。这个判断虽然简单但体现的是防御性编程的意识——不是所有用户都会乖乖顺着你的按钮点。3.4 写作与编辑同一个模板的两个复用场景写文章和编辑文章虽然业务逻辑不同——一个是插入一个是更新——但页面长得几乎一样所以我把它们共用一个edit.html模板区别只在于有没有传post对象进来。app.route(/new, methods[GET, POST]) def new_post(): if request.method POST: title request.form[title].strip() content request.form[content].strip() if not title or not content: return 标题和内容不能为空, 400 conn get_db() conn.execute(INSERT INTO posts (title, content) VALUES (?, ?), (title, content)) conn.commit() conn.close() return redirect(url_for(index)) return render_template(edit.html) app.route(/edit/int:post_id, methods[GET, POST]) def edit_post(post_id): conn get_db() post conn.execute(SELECT * FROM posts WHERE id ?, (post_id,)).fetchone() conn.close() if post is None: abort(404) if request.method POST: title request.form[title].strip() content request.form[content].strip() if not title or not content: return 标题和内容不能为空, 400 conn get_db() conn.execute(UPDATE posts SET title ?, content ? WHERE id ?, (title, content, post_id)) conn.commit() conn.close() return redirect(url_for(post_detail, post_idpost_id)) return render_template(edit.html, postpost)写作和编辑有几个共同的细节值得说对表单内容一定要.strip()去掉首尾空格避免用户手抖多敲了空格存出标题前面一个空格这种看着难受的数据。用参数化的SQL写法(?, ?)而不是用字符串拼接。这是防SQL注入的最基本手段。哪怕是你自己的博客没人在意这个习惯必须从一开始养成。提交成功后用redirect(url_for(...))而不是直接render_template。因为如果用户刷新页面POST请求会被重复提交产生重复数据——这就是著名的表单重复提交问题。重定向到GET请求页面后刷新就不再有副作用了。删除的逻辑更简单核心一行SQLapp.route(/delete/int:post_id) def delete_post(post_id): conn get_db() conn.execute(DELETE FROM posts WHERE id ?, (post_id,)) conn.commit() conn.close() return redirect(url_for(index))不过我必须指出这个删除用的是GET方法也就是用户点击链接就直接删这样不太好。一方面是搜索引擎爬虫可能顺着链接把文章删了另一方面是恶意访问防不住。正规做法应该用POST提交。你在学习阶段可以先用这个简单版本但心里要有数等部署上线前一定要改成用表单POST的方式。到这个阶段后端逻辑已经完整了。浏览器里访问http://127.0.0.1:5000你会看到一个有点丑的页面。别急着嫌弃下一步就是让页面变好看。4. 用模板和CSS把页面变好看继承、动态渲染与样式细节4.1 模板继承为什么能少写一半重复代码传统写网页每个页面都要把DOCTYPE、head、header、footer完整写一遍三个页面就要写三次导航栏。Jinja2模板引擎提供了继承机制把公共的部分放在base.html里其他页面只在自己的内容区里写特有的部分。base.html长这样!DOCTYPE html html langzh-CN head meta charsetUTF-8 meta nameviewport contentwidthdevice-width, initial-scale1.0 title{% block title %}我的博客{% endblock %}/title link relstylesheet href{{ url_for(static, filenamecss/style.css) }} /head body header h1a href{{ url_for(index) }}我的个人博客/a/h1 nav a href{{ url_for(index) }}首页/a a href{{ url_for(new_post) }}写文章/a /nav /header main {% block content %}{% endblock %} /main footerpPowered by Python Flask/p/footer /body /html然后index.html只需要写这些{% extends base.html %} {% block title %}首页 - 我的博客{% endblock %} {% block content %} {% for post in posts %} article h2a href{{ url_for(post_detail, post_idpost[id]) }}{{ post[title] }}/a/h2 p classmeta{{ post[created_at] }}/p p{{ post[content][:80] }}.../p pa href{{ url_for(edit_post, post_idpost[id]) }}编辑/a/p /article {% else %} p还没有文章快去写第一篇吧。/p {% endfor %} {% endblock %}注意这里有两个容易踩的细节。第一所有的URL生成都用url_for()而不是写死路径。比如我要链接到第3篇文章写死成/post/3当然可以但以后如果改了路由结构所有写死的链接就全废了。url_for按函数名自动生成地址改路由换地址也不怕。第二模板里的post[content][:80]是Python切片语法在Jinja2里可以直接用用来截取前80个字符作为摘要。这样列表页不会整篇正文全堆出来。4.2 url_for的三种用途说到url_for值得单独花一段讲。它大概是Flask最常用的辅助函数至少有三个典型场景生成静态资源链接url_for(static, filenamecss/style.css)生成页面路由url_for(index)、url_for(post_detail, post_id1)重定向redirect(url_for(index))前两种用于模板里后一种用于Python代码里。它的好处是解耦URL和代码——URL变了代码里的链接不会跟着断。4.3 CSS和前端细节从能看到耐看模板写好后就该轮到style.css上场了。我给这个博客用的是一套极简风格不搞花哨配色重点在留白和阅读舒适度body { font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Microsoft YaHei, sans-serif; max-width: 800px; margin: 0 auto; padding: 24px 16px; line-height: 1.8; color: #333; background: #fafafa; } header { border-bottom: 2px solid #eee; padding-bottom: 12px; margin-bottom: 24px; } header h1 a { color: #333; text-decoration: none; } nav { margin-top: 8px; } nav a { margin-right: 16px; color: #4a90d9; text-decoration: none; } article { background: #fff; border-radius: 8px; padding: 16px 20px; box-shadow: 0 1px 4px rgba(0,0,0,0.08); margin-bottom: 16px; } .meta { color: #999; font-size: 14px; } .content { word-wrap: break-word; } textarea, input[typetext] { width: 100%; padding: 8px; font-size: 16px; border: 1px solid #ddd; border-radius: 4px; } button { background: #4a90d9; color: #fff; border: none; padding: 8px 24px; border-radius: 4px; cursor: pointer; } media (max-width: 480px) { body { padding: 12px; } header { text-align: center; } }这里面我刻意用了一组蓝色系按钮和链接色其他全是黑白灰是为了让页面看起来安静。做个人博客内容才是主角样式是衬托。那些大红大绿的配色方案看着热闹文章正文真的没人读得下去。有两点实用经验我没有写死任何绝对宽度用max-width: 800px加auto居中这样在宽屏和窄屏上都能读。最后那段media媒体查询是响应式的最小实现手机访问时缩小间距和字号不至于排版乱掉。如果你想把页面做得更精致可以加一点JS效果比如点击标题展开目录或者给文章加个回到顶部按钮。我在正式模板里还加了导航栏在滚动时置顶的效果这些小东西纯粹是锦上添花不要为了炫技在首版就堆上去。4.4 safe过滤器一个好看又危险的开关在详情页模板里如果想让多段正文正确换行我用了这样一行div classcontent{{ post[content] | replace(\n, br) | safe }}/div这里必须提醒| safe这个过滤器意味着我把这段内容当成可信的HTML直接输出。如果用户在文章里输入了scriptalert(xss)/script这段脚本就会在你网站上执行——这就是XSS攻击。放在你自己的博客上最多是给自己添堵但如果日后开放给访客评论这个过滤器就是安全隐患。安全写法是转义后换行比如在Python那边把\n替换成HTML标签再渲染或者在模板里结合escape过滤器处理。首版图省事可以用| safe但上线前最好请熟悉安全的朋友看一遍。5. 本地跑起来数据填充、调试与常见报错排雷5.1 第一次运行三步验证代码写完之后在项目目录执行python app.py正常你会看到* Running on http://127.0.0.1:5000浏览器打开就能看到空白的博客首页。到这里骨架算是活了。5.2 填充测试数据三种姿势空模板下你可能不知道页面长什么样需要先塞几篇文章。我给你三种方式。方式一直接通过页面写。访问/new填好标题和正文提交回首页刷新。方式二用Python交互式一行插入python -c import sqlite3; connsqlite3.connect(blog.db); conn.execute(\INSERT INTO posts(title,content) VALUES(第一篇,hello world)\); conn.commit(); conn.close()方式三写个seed.py小脚本一次性生成10篇不同主题的文章用来测试翻页效果。这是我最推荐的方式毕竟你后面还要调样式、测功能数据多才有测试价值。5.3 排雷经验那些年我见过的报错这个项目我前前后后带不少人做过把高频报错整理成一张表比你自己一个个踩效率高得多报错信息原因解决办法ModuleNotFoundError: No module named flask没装Flask或装错环境pip install flaskjinja2.exceptions.TemplateNotFound模板文件名或目录错检查templates目录、文件名大小写sqlite3.OperationalError: no such table: posts没执行建表或db文件路径不对确认init_db()被调用检查DB_PATHUnicodeDecodeError文件编码不是UTF-8所有文件保存为UTF-8html声明charset502 Bad Gateway部署后应用没被正确托管检查Gunicorn进程与端口端口5000被占用其它程序占用了端口换端口app.run(port8000)除了表格里的还有两个经典困惑我要展开讲。第一个是本机localhost能访问、但手机打开电脑IP访问不了。原因是app.run默认只听本机需要在启动时加一句话app.run(host0.0.0.0, port5000)。不过加了之后你还要注意局域网防火墙Windows和macOS都会拦这个不是代码问题别急着改代码。第二个是修改代码后浏览器看不到变化。如果你用的是Flask内置服务器debugTrue模式会自动检测文件变化并重载但浏览器缓存可能挡住新CSS。这时候强制刷新CtrlF5往往就好了和代码无关。5.4 从体验角度再看一遍别忘了这些细节本地跑通之后我习惯从使用者的角度把整个流程重新走一遍而不是从开发者的角度。你会发现好几个可以优化的地方。首页文章列表的摘要我用切片[:80]直接截。如果正文里包含中英文混排截断位置可能在单词中间。更稳的是按字符数做自适应截断或者干脆用Markdown提取纯文本后截断。编辑文章页面进入时textarea里的内容是HTML转义的注意不要被| safe带偏这里应该原样显示渲染时才处理。发布后返回首页最好有个提示比如发布成功。我在正式版本里用flash消息机制做了这个功能——session里放一条消息渲染后自动清除。这个实现起来也不复杂但新手版本一般都会漏。6. 让博客在服务器上跑通部署、安全与后续扩展6.1 从localhost到公网最小可用的部署路径本地开发环境里Flask自带的开发服务器只适合调试。真要部署到服务器、面对真实访问至少要交给专业的WSGI服务器比如Gunicorn来托管应用进程再由Nginx做反向代理把80端口的请求转发给Gunicorn。留个参考的部署步骤在服务器上安装Python和项目依赖用Gunicorn启动应用gunicorn -w 4 -b 127.0.0.1:8000 app:app配置Nginx反向代理到127.0.0.1:8000用systemd把Gunicorn守护起来实现进程崩溃自动拉起开启HTTPS现在免费证书申请很方便为什么中间要绕一层GunicornNginx直接用Python自带的服务器不行吗不行。Flask自带的server是单进程单线程的开发时方便调试生产环境是并发瓶颈和安全短板。Gunicorn负责扩并发量Nginx负责处理静态文件CSS、图片等和负载均衡两者配合才是个人博客最低成本的稳健架构。服务器选择上国内云服务商都有便宜的学生机或轻量应用服务器一年几百块钱跑个人博客绰绰有余。首次部署建议把环境变量、项目代码路径、启动命令都按模板写清楚省得运维时手忙脚乱。6.2 上线前必做的安全自查部署不是把代码丢上去就算完我列一个安全自查清单做过一遍心里才有底确保app.run(debugFalse)debug模式会输出调试信息和堆栈生产环境是安全隐患删除或隐蔽危险路由/delete这类操作要改为POST并增加CSRF验证保存前永远用参数化SQL杜绝SQL注入引入secret_key设置涉及到session和flash消息都要依赖它建议加一层简单的访问认证比如管理员密码不然任何人都能发文章删文章数据库文件不要放在web目录下防止被直接下载别觉得个人博客没人攻击就掉以轻心。我在服务器日志里见过不少扫描器专门扫常见的路径和后门。防护的意识从第一天就要有不然等到被挂马再救就晚了。6.3 功能扩展博客长大的路线图到了这一步你的博客已经是一个能跑、能写、能部署的个人站点。接下来往哪个方向扩展我按工程量从小到大给你排个序给文章加标签和分类在posts这张表旁边加tag表、category表用外键关联支持Markdown写作引入Markdown解析库变**加粗**为加粗文字加评论系统先把本地方案做明白再考虑接入第三方全文搜索用SQLite的LIKE模糊查询文章多了再换全文索引文章归档和按月份筛选按月分组展示很有博客味RSS订阅写一个XML动态生成路由就能被RSS阅读器订阅我当时就是从个人博客这个项目开始一步步把标签、Markdown、RSS都做进去了后来顺理成章用Django重构了一个功能更重的站点。回过头看最初那个能跑的最小版本就是最好的起跑线。最后分享一个我自己的体会做这个项目别一上来就想着功能要多全、UI要多炫。我最开始那份Flask博客CSS只有二十来行页面就是白底黑字加一个链接但因为它是我自己跑通、自己部署、自己往里写文章的那份成就感比后来用什么都强。先让它跑起来再一点点变好看、变聪明这是做个人项目最舒服的节奏。如果这篇文章帮你跑通了第一个Python博客那你下一步不妨试试把你的读书笔记、学习心得、日常记录真的写进去坚持一个月回头看看这个由代码支撑起来的小站。到时候你会理解为什么我说用Python写出的不是网页而是一个能承载内容的角落这个角落会一直等你回来。