Django 实战:从零开发一个完整的博客系统(附带文章、分类、标签)

IT策士 10余年一线大厂经验,专注 IT 思维、架构、职场进阶。我会在公众号、今日头条持续发布最新文章,助你少走弯路。

从这篇文章开始Django 实战教程,我们将从零开始,一步步构建一个功能完整的博客系统,包含文章、分类和标签。整个过程会提供大量可直接运行的代码示例与控制台输出,帮助新手理解每一步在做什么,也为进阶开发者梳理最佳实践。


1. 为什么选择 Django?

Django 是一个高级 Python Web 框架,它鼓励快速开发和清晰、务实的设计。用 Django 搭建一个博客,你能在极短时间内获得包含后台管理、文章发布、分类标签筛选等功能的完整站点。它自带的管理后台更是让内容管理变得零成本。

本文将带你从环境搭建到功能上线,逐步实现:

  • 文章(Post)的发布、列表与详情展示

  • 分类(Category)与标签(Tag)的管理和多对多关联

  • 利用 Django Admin 高效管理内容

  • 视图、模板与 URL 的完整对接

  • 进阶实践:分页、Markdown 渲染与搜索

所有步骤都配有真实的控制台输出,让你感受"所见即所得"的开发体验。


2. 环境准备与项目创建

2.1 安装 Python 与虚拟环境

确保已安装 Python 3.8+。接着创建并激活虚拟环境:

bash 复制代码
# 创建项目目录
mkdir myblog
cd myblog

# 创建虚拟环境
python -m venv venv

# 激活虚拟环境(Linux/macOS)
source venv/bin/activate
# 或 Windows
venv\Scripts\activate

控制台会显示环境名称:

bash 复制代码
(venv) user@host:~/myblog$

2.2 安装 Django 并创建项目

bash 复制代码
pip install django
django-admin startproject config .

注意最后的 . 表示在当前目录创建项目,避免多余嵌套。执行后目录结构如下:

bash 复制代码
myblog/
├── config/
│   ├── __init__.py
│   ├── settings.py
│   ├── urls.py
│   └── wsgi.py
├── manage.py
└── venv/

验证安装:

bash 复制代码
python manage.py --version
# 输出:4.2.5 (示例版本)

2.3 创建博客应用

Django 项目由多个"应用"组成,我们将博客功能独立为一个应用:

bash 复制代码
python manage.py startapp blog

现在项目结构变为:

bash 复制代码
myblog/
├── blog/
│   ├── migrations/
│   ├── __init__.py
│   ├── admin.py
│   ├── apps.py
│   ├── models.py
│   ├── tests.py
│   └── views.py
├── config/
├── manage.py
└── venv/

注册应用 :打开 config/settings.py,找到 INSTALLED_APPS,加入 'blog'

bash 复制代码
INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'blog',                      # 我们的博客应用
]

3. 定义数据模型:文章、分类、标签

博客的核心是数据。我们在 blog/models.py 中定义三个模型,并理清它们的关系:一篇文章属于一个分类(ForeignKey),可以拥有多个标签(ManyToManyField)。

bash 复制代码
from django.db import models
from django.urls import reverse
from django.utils.text import slugify


class Category(models.Model):
    name = models.CharField(max_length=100, unique=True)
    slug = models.SlugField(max_length=100, unique=True)
    description = models.TextField(blank=True)

    class Meta:
        verbose_name = '分类'
        verbose_name_plural = '分类'
        ordering = ['name']

    def __str__(self):
        return self.name

    def get_absolute_url(self):
        return reverse('blog:category_detail', args=[self.slug])

    def save(self, *args, **kwargs):
        if not self.slug:
            self.slug = slugify(self.name)
        super().save(*args, **kwargs)


class Tag(models.Model):
    name = models.CharField(max_length=50, unique=True)
    slug = models.SlugField(max_length=50, unique=True)

    class Meta:
        verbose_name = '标签'
        verbose_name_plural = '标签'
        ordering = ['name']

    def __str__(self):
        return self.name

    def get_absolute_url(self):
        return reverse('blog:tag_detail', args=[self.slug])

    def save(self, *args, **kwargs):
        if not self.slug:
            self.slug = slugify(self.name)
        super().save(*args, **kwargs)


class Post(models.Model):
    STATUS_CHOICES = (
        ('draft', '草稿'),
        ('published', '已发布'),
    )

    title = models.CharField(max_length=200)
    slug = models.SlugField(max_length=200, unique_for_date='created')
    author = models.ForeignKey(
        'auth.User',
        on_delete=models.CASCADE,
        related_name='blog_posts'
    )
    body = models.TextField()
    category = models.ForeignKey(
        Category,
        on_delete=models.SET_NULL,
        null=True,
        related_name='posts'
    )
    tags = models.ManyToManyField(Tag, blank=True, related_name='posts')
    status = models.CharField(max_length=10, choices=STATUS_CHOICES, default='draft')
    created = models.DateTimeField(auto_now_add=True)
    updated = models.DateTimeField(auto_now=True)

    class Meta:
        ordering = ['-created']
        verbose_name = '文章'
        verbose_name_plural = '文章'
        indexes = [
            models.Index(fields=['-created']),
        ]

    def __str__(self):
        return self.title

    def get_absolute_url(self):
        return reverse('blog:post_detail', args=[self.created.year,
                                                 self.created.month,
                                                 self.created.day,
                                                 self.slug])

    def save(self, *args, **kwargs):
        if not self.slug:
            self.slug = slugify(self.title)
        super().save(*args, **kwargs)

模型设计解释

  • CategoryTag 都有 slug 字段,用于生成对 SEO 友好的 URL。

  • Post.category 是外键,删除分类时文章不会被删除,而是将其分类设为 NULLSET_NULL)。

  • Post.tags 是多对多,一篇文章可以有多个标签,一个标签也能对应多篇文章。

  • unique_for_date 保证同一天内 slug 唯一,避免 URL 冲突。

  • get_absolute_url() 定义了访问每个对象详情页的规范路径,后续在模板和管理后台中都会用到。

3.1 生成并执行迁移

bash 复制代码
python manage.py makemigrations blog
python manage.py migrate

控制台输出示例:

bash 复制代码
Migrations for 'blog':
  blog/migrations/0001_initial.py
    - Create model Category
    - Create model Tag
    - Create model Post
    - Add field tags to post
Operations to perform:
  Apply all migrations: admin, auth, blog, contenttypes, sessions
Running migrations:
  Applying blog.0001_initial... OK

现在 SQLite 数据库文件 db.sqlite3 已生成,我们的表结构已就绪。


4. 玩转 Django 管理后台

4.1 创建超级用户

bash 复制代码
python manage.py createsuperuser

按提示输入用户名、邮箱和密码(密码输入时没有回显):

bash 复制代码
用户名: admin
电子邮件地址: admin@example.com
Password: ********
Password (again): ********
Superuser created successfully.

4.2 将模型注册到 Admin

编辑 blog/admin.py

bash 复制代码
from django.contrib import admin
from .models import Category, Tag, Post


@admin.register(Category)
class CategoryAdmin(admin.ModelAdmin):
    list_display = ('name', 'slug', 'description')
    prepopulated_fields = {'slug': ('name',)}
    search_fields = ('name',)


@admin.register(Tag)
class TagAdmin(admin.ModelAdmin):
    list_display = ('name', 'slug')
    prepopulated_fields = {'slug': ('name',)}
    search_fields = ('name',)


@admin.register(Post)
class PostAdmin(admin.ModelAdmin):
    list_display = ('title', 'slug', 'author', 'category', 'status', 'created')
    list_filter = ('status', 'created', 'category', 'tags')
    search_fields = ('title', 'body')
    prepopulated_fields = {'slug': ('title',)}
    raw_id_fields = ('author',)
    date_hierarchy = 'created'
    ordering = ('status', '-created')
    filter_horizontal = ('tags',)  # 多对多选择更友好

prepopulated_fields 使得在输入标题时自动生成 slug,filter_horizontal 给多对多字段一个美观的双栏选择器。

4.3 启动开发服务器

bash 复制代码
python manage.py runserver

控制台输出:

bash 复制代码
Watching for file changes with StatReloader
Performing system checks...

System check identified no issues (0 silenced).
May 16, 2026 - 10:15:20
Django version 4.2.5, using settings 'config.settings'
Starting development server at http://127.0.0.1:8000/
Quit the server with CONTROL-C.

打开 http://127.0.0.1:8000/admin/,用超管账户登录。现在你可以随意创建分类、标签和文章了。

动手试一下:创建分类"Python"、标签"Django"和"Web",然后发布一篇测试文章,标题为"Hello Django"。这些数据将成为我们接下来开发视图和模板的素材。


5. 构建前端视图与 URL

我们希望博客具备以下页面:

  • 文章列表(首页)

  • 文章详情页

  • 按分类筛选文章

  • 按标签筛选文章

5.1 编写视图

打开 blog/views.py,从通用视图开始,最简洁高效:

bash 复制代码
from django.views.generic import ListView, DetailView
from .models import Post, Category, Tag


class PostListView(ListView):
    model = Post
    context_object_name = 'posts'
    template_name = 'blog/post_list.html'
    paginate_by = 5

    def get_queryset(self):
        return Post.objects.filter(status='published').select_related('category', 'author').prefetch_related('tags')


class PostDetailView(DetailView):
    model = Post
    context_object_name = 'post'
    template_name = 'blog/post_detail.html'

    def get_queryset(self):
        return Post.objects.filter(status='published').select_related('category', 'author').prefetch_related('tags')

    def get_object(self, queryset=None):
        # 使用 年/月/日/slug 作为查找字段
        return Post.objects.filter(
            status='published',
            created__year=self.kwargs['year'],
            created__month=self.kwargs['month'],
            created__day=self.kwargs['day'],
            slug=self.kwargs['slug']
        ).first()


class CategoryPostListView(ListView):
    template_name = 'blog/post_list.html'
    context_object_name = 'posts'
    paginate_by = 5

    def get_queryset(self):
        self.category = Category.objects.get(slug=self.kwargs['category_slug'])
        return Post.objects.filter(
            status='published',
            category=self.category
        ).select_related('category', 'author').prefetch_related('tags')

    def get_context_data(self, **kwargs):
        context = super().get_context_data(**kwargs)
        context['category'] = self.category
        return context


class TagPostListView(ListView):
    template_name = 'blog/post_list.html'
    context_object_name = 'posts'
    paginate_by = 5

    def get_queryset(self):
        self.tag = Tag.objects.get(slug=self.kwargs['tag_slug'])
        return Post.objects.filter(
            status='published',
            tags=self.tag
        ).select_related('category', 'author').prefetch_related('tags')

    def get_context_data(self, **kwargs):
        context = super().get_context_data(**kwargs)
        context['tag'] = self.tag
        return context

我们使用了 select_related 来优化外键查询,prefetch_related 来优化多对多查询,这是进阶中的常见性能优化。

5.2 配置 URL

blog 目录下新建 urls.py

bash 复制代码
from django.urls import path
from . import views

app_name = 'blog'

urlpatterns = [
    path('', views.PostListView.as_view(), name='post_list'),
    path('<int:year>/<int:month>/<int:day>/<slug:slug>/',
         views.PostDetailView.as_view(), name='post_detail'),
    path('category/<slug:category_slug>/',
         views.CategoryPostListView.as_view(), name='category_detail'),
    path('tag/<slug:tag_slug>/',
         views.TagPostListView.as_view(), name='tag_detail'),
]

然后将博客的 URL 包含进项目主路由 config/urls.py

bash 复制代码
from django.contrib import admin
from django.urls import path, include

urlpatterns = [
    path('admin/', admin.site.urls),
    path('', include('blog.urls')),
]

现在访问 http://127.0.0.1:8000/ 就会进入文章列表页(不过我们还没有模板)。


6. 模板设计:让博客"活"起来

6.1 基础模板

blog 应用下创建 templates/blog/ 目录(如果不存在)。首先创建 base.html

bash 复制代码
<!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>
    <style>
        body { font-family: sans-serif; max-width: 800px; margin: 0 auto; padding: 1em; }
        nav a { margin-right: 1em; }
        .post { border-bottom: 1px solid #eee; padding: 1em 0; }
        .pagination { margin-top: 2em; }
        .tags span { background: #eee; padding: 2px 6px; border-radius: 4px; margin-right: 4px; }
    </style>
</head>
<body>
    <nav>
        <a href="{% url 'blog:post_list' %}">首页</a>
        <a href="{% url 'admin:index' %}">管理</a>
    </nav>
    <hr>
    {% block content %}
    {% endblock %}
</body>
</html>

6.2 文章列表模板

创建 post_list.html

bash 复制代码
{% extends 'blog/base.html' %}

{% block title %}
  {% if category %}{{ category.name }}{% elif tag %}{{ tag.name }}{% else %}文章列表{% endif %}
{% endblock %}

{% block content %}
  <h1>
    {% if category %}
      分类:{{ category.name }}
    {% elif tag %}
      标签:{{ tag.name }}
    {% else %}
      最新文章
    {% endif %}
  </h1>

  {% for post in posts %}
    <div class="post">
      <h2><a href="{{ post.get_absolute_url }}">{{ post.title }}</a></h2>
      <p>
        分类:<a href="{{ post.category.get_absolute_url }}">{{ post.category.name }}</a> |
        作者:{{ post.author.username }} |
        日期:{{ post.created|date:"Y-m-d" }}
      </p>
      <p>{{ post.body|truncatechars:150 }}</p>
      <div class="tags">
        {% for tag in post.tags.all %}
          <span><a href="{{ tag.get_absolute_url }}">{{ tag.name }}</a></span>
        {% endfor %}
      </div>
    </div>
  {% empty %}
    <p>还没有文章。</p>
  {% endfor %}

  {% if is_paginated %}
    <div class="pagination">
      <span>第 {{ page_obj.number }} 页,共 {{ paginator.num_pages }} 页</span>
      {% if page_obj.has_previous %}
        <a href="?page={{ page_obj.previous_page_number }}">上一页</a>
      {% endif %}
      {% if page_obj.has_next %}
        <a href="?page={{ page_obj.next_page_number }}">下一页</a>
      {% endif %}
    </div>
  {% endif %}
{% endblock %}

6.3 文章详情模板

创建 post_detail.html

bash 复制代码
{% extends 'blog/base.html' %}

{% block title %}{{ post.title }}{% endblock %}

{% block content %}
  <article>
    <h1>{{ post.title }}</h1>
    <p>
      分类:<a href="{{ post.category.get_absolute_url }}">{{ post.category.name }}</a> |
      作者:{{ post.author.username }} |
      发布于:{{ post.created|date:"Y-m-d H:i" }}
      {% if post.updated != post.created %}
        | 更新于:{{ post.updated|date:"Y-m-d H:i" }}
      {% endif %}
    </p>
    <div class="tags">
      {% for tag in post.tags.all %}
        <span><a href="{{ tag.get_absolute_url }}">{{ tag.name }}</a></span>
      {% endfor %}
    </div>
    <hr>
    <div>{{ post.body|linebreaks }}</div>
  </article>
  <a href="{% url 'blog:post_list' %}">← 返回列表</a>
{% endblock %}

现在重启服务器,刷新页面,你应该能看到之前从管理后台发布的文章。点击标题或"阅读更多"即可进入详情页。


7. 用 Shell 验证数据与查询

Django 的交互式 Shell 是调试和学习 ORM 的利器。运行:

你会看到:

bash 复制代码
Python 3.10.12 (main, Nov 20 2023, 15:14:05) [GCC 11.4.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
(InteractiveConsole)
>>> 

7.1 创建示例数据

bash 复制代码
from blog.models import Category, Tag, Post
from django.contrib.auth.models import User

user = User.objects.first()  # 获取管理员用户

python_cat = Category.objects.create(name='Python', description='Python 相关文章')
django_cat = Category.objects.create(name='Django')
tag1 = Tag.objects.create(name='Django')
tag2 = Tag.objects.create(name='Web开发')

post = Post.objects.create(
    title='深入 Django ORM',
    body='Django 的 ORM 非常强大...',
    author=user,
    category=django_cat,
    status='published'
)
post.tags.add(tag1, tag2)

再次执行创建会自动生成 slug,因为模型的 save 方法已处理。

7.2 查询与控制台输出

bash 复制代码
# 获取所有已发布文章
published_posts = Post.objects.filter(status='published')
print(published_posts)
# 输出:<QuerySet [<Post: 深入 Django ORM>]>

# 带优化查询
posts = Post.objects.filter(status='published').select_related('category').prefetch_related('tags')
for p in posts:
    print(f'{p.title} - 分类: {p.category.name} - 标签: {", ".join(t.name for t in p.tags.all())}')
# 输出:深入 Django ORM - 分类: Django - 标签: Django, Web开发

通过 Shell 你能直观感受 ORM 如何工作,这对理解视图层的查询逻辑非常有帮助。


8. 进阶优化与实用功能

8.1 Markdown 支持

博客文章用 Markdown 编写会更优雅。安装库:

Post 模型中添加一个方法,将 Markdown 转为 HTML:

bash 复制代码
import markdown

class Post(models.Model):
    # ... 字段省略 ...

    def body_as_markdown(self):
        return markdown.markdown(self.body, extensions=['extra', 'codehilite'])

在模板 post_detail.html 中,将 {{ post.body|linebreaks }} 替换为:

bash 复制代码
{{ post.body_as_markdown|safe }}

注意markdown 库默认不处理代码高亮,可使用 Pygments 或简单引入 highlight.js 在模板中。

8.2 搜索功能

PostListView 中添加搜索支持。修改 get_queryset

bash 复制代码
class PostListView(ListView):
    # ...
    def get_queryset(self):
        queryset = Post.objects.filter(status='published').select_related('category', 'author').prefetch_related('tags')
        q = self.request.GET.get('q')
        if q:
            queryset = queryset.filter(title__icontains=q)
        return queryset

并在 post_list.html 顶部添加搜索框:

bash 复制代码
<form method="get" action="{% url 'blog:post_list' %}">
    <input type="text" name="q" value="{{ request.GET.q }}" placeholder="搜索文章...">
    <button type="submit">搜索</button>
</form>

8.3 显示所有分类与标签(侧边栏)

可以在列表和详情页的上下文中加入分类和标签,供全局导航使用。最简单的方法是在 base.html 中添加自定义模板标签。创建一个 templatetags 包:

bash 复制代码
blog/
├── templatetags/
│   ├── __init__.py
│   └── blog_tags.py

blog_tags.py:

bash 复制代码
from django import template
from ..models import Category, Tag

register = template.Library()

@register.inclusion_tag('blog/sidebar.html')
def show_sidebar():
    categories = Category.objects.all()
    tags = Tag.objects.all()
    return {'categories': categories, 'tags': tags}

创建 templates/blog/sidebar.html:

bash 复制代码
<h3>分类</h3>
<ul>
  {% for cat in categories %}
    <li><a href="{{ cat.get_absolute_url }}">{{ cat.name }}</a></li>
  {% endfor %}
</ul>

<h3>标签</h3>
<div>
  {% for tag in tags %}
    <span style="background:#eee; padding:2px 6px; margin:2px;">
      <a href="{{ tag.get_absolute_url }}">{{ tag.name }}</a>
    </span>
  {% endfor %}
</div>

base.html 中加载并使用:

bash 复制代码
{% load blog_tags %}
...
<div style="float:right; width:200px;">
  {% show_sidebar %}
</div>
<div style="margin-right:220px;">
  {% block content %}{% endblock %}
</div>

这样所有页面都能显示分类和标签导航了。


9. 最终的控制台旅程

最后我们再通过控制台回顾一下项目的完整运行流程:

bash 复制代码
# 1. 启动开发服务器
python manage.py runserver

# 输出:
# System check identified no issues (0 silenced).
# May 16, 2026 - 14:30:00
# Starting development server at http://127.0.0.1:8000/

# 2. 在另一个终端打开 Shell
python manage.py shell
bash 复制代码
>>> from blog.models import Post
>>> Post.objects.filter(status='published').count()
5    # 假设我们已创建了5篇已发布文章
>>> p = Post.objects.first()
>>> p.title
'深入 Django ORM'
>>> p.body_as_markdown()
'<p>Django 的 ORM 非常强大...</p>'

当你访问 http://127.0.0.1:8000/,你会看到文章列表、分页、搜索框;点击分类或标签会精确过滤;管理后台 http://127.0.0.1:8000/admin/ 让内容管理直观高效。这一切都在不到 300 行核心代码内完成。


10. 总结与下一步

至此,你已经从零开始,利用 Django 构建了一个功能完备的博客系统:

  • 模型设计:Category、Tag、Post,理解一对多与多对多

  • 管理后台:定制化显示、筛选、搜索

  • 视图与模板:基于类的通用视图、模板继承、分页

  • URL 设计:友好的年/月/日/slug 结构

  • 性能优化:select_related 和 prefetch_related

  • 进阶功能:Markdown 渲染、搜索、侧边栏自定义标签

接下来你可以继续扩展:

  • 评论系统(django-comments 或自建模型)

  • RSS 订阅功能(Django 内置 syndication 框架)

  • 站点地图(sitemap)

  • 使用 PostgreSQL 并部署至生产环境(如 Railway、Heroku 或 VPS)

如果觉得不错,还可以去公众号、今日头条搜索「IT策士」,一起升级 IT 思维 !

相关推荐
XovH1 小时前
Django 表单(Forms)与数据验证:处理用户提交与防止常见攻击
后端
fliter1 小时前
从 C 的混乱到 Rust 的优雅:字符串处理为什么这么难
后端
jieyucx1 小时前
Go 语言进阶:结构体指针、new 关键字与匿名结构体/成员详解
开发语言·后端·golang·结构体
IT大家说1 小时前
那些没人主动教你的代码小技巧,写完代码干净又优雅
后端
摇滚侠1 小时前
Spring 面试题 真正的 offer 偏方 Java 基础 Java 高级
java·后端·spring
用户78937733908532 小时前
前端转后端生存指南(中):化身架构师,用 ORM 魔法掌控数据库
后端·python
Master_Azur2 小时前
JavaEE之文件操作 字符集 IO流
后端
传说之后2 小时前
GO 语言单元测试入门
后端
古城小栈2 小时前
Bun从Zig迁移至Rust:有何重大意义?
开发语言·后端·rust