Python+Flask 1小时搭建个人博客:比WordPress轻量10倍

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。

够了。

相关推荐
闪学it24 分钟前
Python测试开发进阶线上班28期
后端
Go_error24 分钟前
Badu/bus:Go 轻量级泛型发布/订阅事件总线
后端·go
Go_error27 分钟前
Go-redis:执行 Lua 脚本
后端·go
IT毕设实战小研1 小时前
电影数据可视化推荐系统
大数据·后端·爬虫·python·算法·信息可视化·课程设计
ssshooter1 小时前
AI 时代你不能不知道的 git worktree
前端·后端·面试
Cosolar1 小时前
一文弄懂 Agent Harness 与 Agent Runtime 的区别
java·后端·github
长大19882 小时前
Python代码慢?这5个性能优化技巧让速度提升100倍
后端
超超不吵吵2 小时前
Java AI转型实战(十):Agent 智能体初探
后端
步行cgn2 小时前
@AllArgsConstructor 是 Lombok 提供的注解
后端