Python+Flask 1小时搭建个人博客:比WordPress轻量10倍
WordPress 很强大,但它也太"重"了。
装完之后你面对的是:数据库配置、插件管理、主题兼容、安全更新、缓存优化......光是让一个博客跑起来,你就已经花了半天在跟系统较劲,而不是在写内容。
如果你只是想有一个干净的地方放自己的文章,Flask 可能是更好的选择。整个项目可以跑在一个几MB的虚拟环境里,部署完内存占用不到100MB,而同等规模的 WordPress 实例轻松吃掉 500MB 以上。不是夸张,是实测。
下面我会带你从零开始,用大约1小时搭一个能跑、能写、能看的个人博客。不需要前端框架,不需要复杂的数据库设计,SQLite + Flask + 几个扩展就够了。
开始前你需要准备什么
- Python 3.10+(3.12 也行)
- 一个终端
- 一个代码编辑器(VS Code 就够)
- 基本的 Python 语法知识
不需要会前端,不需要会 SQL,跟着做就行。
第一步:项目骨架(10分钟)
先建项目目录和虚拟环境:
bash
mkdir flask-blog && cd flask-blog
python -m venv venv
source venv/bin/activate # Windows 用 venv\Scripts\activate
装依赖:
pip install flask flask-sqlalchemy flask-migrate markdown pygments
为什么选这些:
- Flask:核心框架,轻量到极致
- Flask-SQLAlchemy:ORM,让你用 Python 类操作数据库,不用写 SQL
- Flask-Migrate:数据库版本管理,后面改表结构不用手动迁移
- Markdown:写文章用 Markdown,不是富文本编辑器
- Pygments:代码高亮,技术博客必备
项目结构长这样:
arduino
flask-blog/
├── app/
│ ├── __init__.py
│ ├── models.py
│ ├── routes.py
│ └── templates/
│ ├── base.html
│ ├── index.html
│ ├── post.html
│ └── admin.html
│ └── static/
│ └── style.css
├── config.py
└── run.py
第二步:Flask 应用初始化(10分钟)
config.py------配置项集中放这里:
lua
import os
basedir = os.path.abspath(os.path.dirname(__file__))
class Config:
SECRET_KEY = os.environ.get('SECRET_KEY') or 'dev-key-change-in-production'
SQLALCHEMY_DATABASE_URI = os.environ.get('DATABASE_URL') or \
'sqlite:///' + os.path.join(basedir, 'app', 'blog.db')
SQLALCHEMY_TRACK_MODIFICATIONS = False
app/__init__.py------应用的心脏:
python
from flask import Flask
from flask_sqlalchemy import SQLAlchemy
from flask_migrate import Migrate
from config import Config
db = SQLAlchemy()
migrate = Migrate()
def create_app():
app = Flask(__name__)
app.config.from_object(Config)
db.init_app(app)
migrate.init_app(app, db)
from app import routes
app.register_blueprint(routes.bp)
return app
run.py------启动入口:
ini
from app import create_app
app = create_app()
if __name__ == '__main__':
app.run(debug=True)
第三步:数据模型(10分钟)
app/models.py------博客只需要两张表:文章和标签。
ini
from datetime import datetime
from app import db
# 文章和标签的多对多关系表
post_tags = db.Table(
'post_tags',
db.Column('post_id', db.Integer, db.ForeignKey('post.id'), primary_key=True),
db.Column('tag_id', db.Integer, db.ForeignKey('tag.id'), primary_key=True)
)
class Post(db.Model):
id = db.Column(db.Integer, primary_key=True)
title = db.Column(db.String(200), nullable=False)
slug = db.Column(db.String(200), unique=True, nullable=False)
body_markdown = db.Column(db.Text, nullable=False) # 原始 Markdown
body_html = db.Column(db.Text) # 渲染后的 HTML
summary = db.Column(db.String(300)) # 摘要,列表页用
created_at = db.Column(db.DateTime, default=datetime.utcnow)
updated_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
published = db.Column(db.Boolean, default=True)
tags = db.relationship('Tag', secondary=post_tags, backref='posts')
def __repr__(self):
return f'<Post {self.title}>'
class Tag(db.Model):
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(50), unique=True, nullable=False)
def __repr__(self):
return f'<Tag {self.name}>'
设计思路很简单:文章存 Markdown 原文,展示时实时渲染成 HTML。这样你随时可以改渲染逻辑,不用回写数据库。
第四步:路由和视图(15分钟)
app/routes.py:
python
from flask import Blueprint, render_template, request, redirect, url_for, abort
from app import db
from app.models import Post, Tag
import markdown
from markdown.extensions.codehilite import CodeHiliteExtension
from markdown.extensions.fenced_code import FencedCodeExtension
from markdown.extensions.toc import TocExtension
import re
bp = Blueprint('main', __name__)
def render_markdown(text):
"""把 Markdown 转成 HTML,带代码高亮"""
md = markdown.Markdown(extensions=[
CodeHiliteExtension(linenums=False, css_class='highlight'),
FencedCodeExtension(),
TocExtension(permalink=False),
'tables',
'nl2br',
])
return md.convert(text)
@bp.route('/')
def index():
page = request.args.get('page', 1, type=int)
posts = Post.query.filter_by(published=True)\
.order_by(Post.created_at.desc())\
.paginate(page=page, per_page=10)
return render_template('index.html', posts=posts)
@bp.route('/post/<slug>')
def post(slug):
post = Post.query.filter_by(slug=slug, published=True).first_or_404()
# 渲染 Markdown
post.body_html = render_markdown(post.body_markdown)
return render_template('post.html', post=post)
@bp.route('/tag/<tag_name>')
def tag(tag_name):
tag = Tag.query.filter_by(name=tag_name).first_or_404()
posts = tag.posts.filter_by(published=True)\
.order_by(Post.created_at.desc()).all()
return render_template('tag.html', tag=tag, posts=posts)
# ---- 管理后台(极简版)----
@bp.route('/admin', methods=['GET', 'POST'])
def admin():
if request.method == 'POST':
title = request.form.get('title')
body = request.form.get('body')
slug = request.form.get('slug') or title.lower().replace(' ', '-')
slug = re.sub(r'[^a-z0-9-]', '', slug)
post = Post(
title=title,
slug=slug,
body_markdown=body,
summary=body[:150] + '...' if len(body) > 150 else body
)
# 处理标签
tag_names = request.form.get('tags', '').split(',')
for name in tag_names:
name = name.strip()
if not name:
continue
tag = Tag.query.filter_by(name=name).first()
if not tag:
tag = Tag(name=name)
db.session.add(tag)
post.tags.append(tag)
db.session.add(post)
db.session.commit()
return redirect(url_for('main.post', slug=post.slug))
posts = Post.query.order_by(Post.created_at.desc()).all()
return render_template('admin.html', posts=posts)
第五步:模板(10分钟)
app/templates/base.html------所有页面的骨架:
xml
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{% block title %}我的博客{% endblock %}</title>
<link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}">
</head>
<body>
<header>
<nav>
<a href="{{ url_for('main.index') }}" class="logo">我的博客</a>
<a href="{{ url_for('main.admin') }}">管理</a>
</nav>
</header>
<main>
{% block content %}{% endblock %}
</main>
<footer>
<p>Powered by Flask · © 2026</p>
</footer>
</body>
</html>
app/templates/index.html------文章列表:
xml
{% extends "base.html" %}
{% block content %}
<div class="posts">
{% for post in posts.items %}
<article class="post-preview">
<h2><a href="{{ url_for('main.post', slug=post.slug) }}">{{ post.title }}</a></h2>
<time>{{ post.created_at.strftime('%Y-%m-%d') }}</time>
<p class="summary">{{ post.summary }}</p>
<div class="tags">
{% for tag in post.tags %}
<a href="{{ url_for('main.tag', tag_name=tag.name) }}" class="tag">{{ tag.name }}</a>
{% endfor %}
</div>
</article>
{% endfor %}
</div>
{% if posts.has_prev or posts.has_next %}
<nav class="pagination">
{% if posts.has_prev %}<a href="{{ url_for('main.index', page=posts.prev_num) }}">← 上一页</a>{% endif %}
{% if posts.has_next %}<a href="{{ url_for('main.index', page=posts.next_num) }}">下一页 →</a>{% endif %}
</nav>
{% endif %}
{% endblock %}
app/templates/post.html------文章详情页:
javascript
{% extends "base.html" %}
{% block title %}{{ post.title }}{% endblock %}
{% block content %}
<article class="post">
<header>
<h1>{{ post.title }}</h1>
<time>{{ post.created_at.strftime('%Y-%m-%d') }}</time>
</header>
<div class="content">
{{ post.body_html | safe }}
</div>
<div class="tags">
{% for tag in post.tags %}
<a href="{{ url_for('main.tag', tag_name=tag.name) }}" class="tag">{{ tag.name }}</a>
{% endfor %}
</div>
</article>
<a href="{{ url_for('main.index') }}">← 返回首页</a>
{% endblock %}
app/templates/admin.html------管理后台,一个表单加列表:
xml
{% extends "base.html" %}
{% block title %}管理后台{% endblock %}
{% block content %}
<h1>管理后台</h1>
<form method="POST" class="editor">
<input type="text" name="title" placeholder="文章标题" required>
<input type="text" name="slug" placeholder="URL slug(留空自动生成)">
<textarea name="body" placeholder="用 Markdown 写文章..." rows="15" required></textarea>
<input type="text" name="tags" placeholder="标签,逗号分隔">
<button type="submit">发布</button>
</form>
<h2>已有文章</h2>
<ul class="post-list">
{% for post in posts %}
<li>
<a href="{{ url_for('main.post', slug=post.slug) }}">{{ post.title }}</a>
<span>{{ post.created_at.strftime('%Y-%m-%d') }}</span>
</li>
{% endfor %}
</ul>
{% endblock %}
第六步:样式(5分钟)
app/static/style.css------干净、克制、能看就行:
css
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif;
line-height: 1.7;
color: #1a1a1a;
max-width: 720px;
margin: 0 auto;
padding: 0 20px;
}
header {
padding: 2rem 0 1rem;
border-bottom: 1px solid #eee;
margin-bottom: 2rem;
}
nav {
display: flex;
justify-content: space-between;
align-items: center;
}
.logo { font-weight: 700; font-size: 1.2rem; text-decoration: none; color: #1a1a1a; }
.post-preview { margin-bottom: 2.5rem; }
.post-preview h2 a { text-decoration: none; color: #1a1a1a; }
.post-preview h2 a:hover { text-decoration: underline; }
.post-preview time { color: #888; font-size: 0.9rem; }
.summary { color: #555; margin-top: 0.5rem; }
.tags { margin-top: 0.5rem; }
.tag {
display: inline-block;
background: #f0f0f0;
padding: 2px 8px;
border-radius: 4px;
font-size: 0.8rem;
text-decoration: none;
color: #555;
margin-right: 4px;
}
.post h1 { margin-bottom: 0.5rem; }
.post time { color: #888; }
.content { margin: 2rem 0; }
.content pre {
background: #f6f8fa;
padding: 1rem;
overflow-x: auto;
border-radius: 6px;
margin: 1rem 0;
}
.content code { background: #f6f8fa; padding: 2px 6px; border-radius: 3px; font-size: 0.9em; }
.content pre code { background: none; padding: 0; }
.editor { display: flex; flex-direction: column; gap: 1rem; margin-bottom: 3rem; }
.editor input, .editor textarea {
padding: 0.75rem;
border: 1px solid #ddd;
border-radius: 6px;
font-family: inherit;
font-size: 1rem;
}
.editor button {
padding: 0.75rem;
background: #1a1a1a;
color: white;
border: none;
border-radius: 6px;
cursor: pointer;
font-size: 1rem;
}
.editor button:hover { background: #333; }
.pagination { display: flex; justify-content: space-between; margin: 2rem 0; }
.post-list { list-style: none; }
.post-list li { display: flex; justify-content: space-between; padding: 0.5rem 0; border-bottom: 1px solid #f0f0f0; }
footer { margin-top: 4rem; padding: 2rem 0; text-align: center; color: #888; font-size: 0.9rem; }
第七步:跑起来(5分钟)
初始化数据库:
csharp
flask db init
flask db migrate -m "initial"
flask db upgrade
启动:
arduino
python run.py
打开浏览器访问 http://localhost:5000,你应该能看到一个干净的博客首页。
去 /admin 写一篇文章试试。标题随便写,正文用 Markdown 写,标签用逗号分隔。提交后回到首页,文章就出现了。
和 WordPress 比,你得到了什么、失去了什么
得到的:
| 维度 | Flask 博客 | WordPress |
|---|---|---|
| 内存占用 | ~80MB | ~500MB+ |
| 启动时间 | 毫秒级 | 需要 PHP + MySQL + Web Server |
| 数据格式 | SQLite 单文件,拷走就能跑 | MySQL 数据库,迁移麻烦 |
| 写文章 | 纯 Markdown,版本可控 | 富文本编辑器,内容锁在数据库里 |
| 定制 | 改 Python 代码就行 | 主题/插件体系,改起来牵一发动全身 |
失去的:
- 没有可视化主题商店
- 没有插件生态(评论、SEO、缓存都得自己写或集成)
- 没有后台管理界面(上面的 admin 页面很粗糙,只是能用)
- 没有用户系统(如果你需要多用户,得自己加 Flask-Login)
接下来可以加什么
博客跑起来之后,你可以按需扩展:
- 评论:集成 Giscus(GitHub Discussions 驱动的评论系统),零后端
- RSS :用 Flask 写一个
/feed.xml路由,返回 Atom 格式 - 搜索:加 Whoosh 或接入 Algolia
- 部署:用 Gunicorn + Nginx,扔到一台 2GB 内存的 VPS 上,每月几块钱
- CI/CD:用 GitHub Actions 自动部署
这些都不是必须的。一个博客的核心价值是内容,不是功能堆砌。Flask 的好处恰恰是它不会诱惑你花三天调插件------它太简单了,简单到你没得折腾,只能去写文章。
最后
WordPress 适合需要完整 CMS 的人。但如果你只是想写点东西、有一个属于自己的角落,Flask 给你的是:一个 SQLite 文件、几十行 Python 代码、一个能跑的博客。
轻量 10 倍不是营销话术,是架构决定的。没有 PHP 运行时、没有 MySQL 服务进程、没有插件加载链------它就是一个 Python 进程,读一个文件,渲染一个模板,返回一段 HTML。
够了。