用 Django 从零搭建个人 Blog:从 MVC 到文章发布全流程
《Python 实战》应用项目篇 · 第 8 篇
作者按:以下每一步都在真实服务器上执行,回显为原样复制。涉及密码与密钥处均已打码,不写明文。
一、背景:为什么要用框架而不是裸写
第 7 篇我解决了「数据进出 Excel」的问题。但真实业务往往是「给人用的网页」:别人打开浏览器就能看文章、发文章。用http.server裸写也能跑,但路由、模板、数据库、后台管理全得自己造轮子。Django 把这些一次性打包好,让你专注写业务。
本篇我用一次完整实操,从零搭一个个人 Blog,覆盖:
- Django 的MTV(Model-Template-View)到底和课本里的 MVC 是什么关系;
startproject/startapp之后,模型、迁移、后台、视图、模板如何串起来;- 一个常被新手卡住的坑:
ALLOWED_HOSTS报错,我先真实复现它,再修复。
环境:Ubuntu 24.04 + Python 3.12.3,依赖装在 venv 里(Django 6.0.7)。所有代码放在/root/lab-d/django/myblog。
二、先搞懂 MTV,再看代码不迷路
教科书讲 MVC:Model 管数据、View 管界面、Controller 管调度。Django 官方说法是MTV:
| 角色 | Django 里的对应 | 职责 |
|---|---|---|
| Model | models.py | 定义数据结构,直接映射数据库表 |
| Template | templates/*.html | 负责「长什么样」,用{{ }}占位 |
| View | views.py | 处理请求、查数据、选模板渲染 |
| URLconf | urls.py | 把 URL 路由到对应的 View(相当于 Controller 的路由部分) |
一句话记住:URL 找到 View,View 从 Model 拿数据,塞给 Template 渲染成 HTML。下面全程按这个链路走。
三、建项目与 App(真实回显)
Django 推荐「一个项目(project)多个应用(app)」的结构。个人 Blog 的文章功能就是一个独立 app:articles。
cd/root/lab-d/django django-admin startproject myblogcdmyblog python manage.py startapp articlesmkdir-particles/templates/articles真实回显:
PROJECT_READY注意这里用的是 venv 里的
python(已export PATH=/root/lab-d/venv/bin:$PATH)。Ubuntu 24.04 系统里直接敲python是找不到的,必须用 venv 或python3。
建完后目录大致是:myblog/myblog/(项目配置)、myblog/articles/(应用)。下一步把articles登记进INSTALLED_APPS——这是新手最容易忘的一步,忘了就会报Model class articles.models.Article doesn't declare an explicit app_label:
articles in INSTALLED_APPS: True四、定义 Article 模型并迁移
模型就是数据库的「定义」。我让 Django 自动建表,而不是自己写 SQL:
# articles/models.pyfromdjango.dbimportmodelsclassArticle(models.Model):title=models.CharField("标题",max_length=200)content=models.TextField("正文")created_at=models.DateTimeField("创建时间",auto_now_add=True)def__str__(self):returnself.titlemakemigrations根据模型变化生成迁移文件,migrate真正落到数据库。真实回显:
Migrations for 'articles': articles/migrations/0001_initial.py + Create model Article Operations to perform: Apply all migrations: admin, articles, auth, contenttypes, sessions Running migrations: Applying contenttypes.0001_initial... OK Applying auth.0001_initial... OK Applying admin.0001_initial... OK ... Applying articles.0001_initial... OK ...articles.0001_initial... OK这一行说明文章表已经建好。Django 的迁移是「增量」的:以后你改了模型字段,再makemigrations就会生成一份新的增量迁移,不会动已有数据。这是它比裸 SQL 强的地方。
五、接入 admin 后台:免交互建超管 + 注册模型
Django 自带一个开箱即用的后台。先在admin.py注册模型,让它在后台可见:
# articles/admin.pyfromdjango.contribimportadminfrom.modelsimportArticle@admin.register(Article)classArticleAdmin(admin.ModelAdmin):list_display=("title","created_at")search_fields=("title",)然后建超级用户。交互式createsuperuser会逐个问用户名、邮箱、密码,在自动化脚本里行不通。我用环境变量 +--noinput免交互:
DJANGO_SUPERUSER_PASSWORD='********'\DJANGO_SUPERUSER_USERNAME=admin\DJANGO_SUPERUSER_EMAIL=admin@example.com\python manage.py createsuperuser--noinput合规说明:上面密码处已打码(
********),真实执行时使用了强密码,本文不记录明文。DJANGO_SUPERUSER_PASSWORD只活在环境变量里,不会写进任何文件。
真实回显:
Superuser created successfully.六、写 View + Template:列表页与详情页
视图负责「拿数据、选模板」。我写两个视图:列表页按时间倒序取出全部文章,详情页按主键取单篇。
# articles/views.pyfromdjango.shortcutsimportrender,get_object_or_404from.modelsimportArticledefarticle_list(request):articles=Article.objects.order_by("-created_at")returnrender(request,"articles/list.html",{"articles":articles})defarticle_detail(request,pk):article=get_object_or_404(Article,pk=pk)returnrender(request,"articles/detail.html",{"article":article})模板用 Django 模板语言,{{ }}是变量,{% for %}是循环:
<!-- articles/templates/articles/list.html --><h1>我的博客 - 文章列表</h1><ul>{% for a in articles %}<li><ahref="/article/{{ a.id }}/">{{ a.title }}</a>-- {{ a.created_at|date:"Y-m-d H:i" }}</li>{% endfor %}</ul>urls.py把 URL 接到视图上(项目总路由 + app 子路由):
# myblog/urls.pyfromdjango.contribimportadminfromdjango.urlsimportpath,include urlpatterns=[path("admin/",admin.site.urls),path("",include("articles.urls")),]# articles/urls.pyfromdjango.urlsimportpathfrom.importviews app_name="articles"urlpatterns=[path("",views.article_list,name="list"),path("article/<int:pk>/",views.article_detail,name="detail"),]七、用 shell 灌入测试数据
表里没数据,页面是空的。用manage.py shell -c直接跑一段代码灌 3 篇测试文章(自动化场景比手动进后台方便):
python manage.py shell-c' from articles.models import Article; Article.objects.all().delete(); Article.objects.create(title="Python 入门:从变量到函数", content="本文带你从最基础的变量讲起..."); Article.objects.create(title="Django 的 MTV 到底怎么理解", content="Model 管数据,Template 管展示..."); Article.objects.create(title="用 OpenCV 做一个人脸检测器", content="借助 haarcascade 级联分类器..."); print("seeded count =", Article.objects.count())'真实回显:
seeded count = 3再验证一下落库内容(按 id 排):
1 Python 入门:从变量到函数 2026-07-24 02:24:31.874338+00:00 2 Django 的 MTV 到底怎么理解 2026-07-24 02:24:31.877365+00:00 3 用 OpenCV 做一个人脸检测器 2026-07-24 02:24:31.880525+00:00八、runserver 启动并用 curl 真实验证
开发服务器用python manage.py runserver 0.0.0.0:8000后台启动,再用本机curl验证页面真的能渲染(而不是「我以为能跑」)。真实 HTTP 状态与 HTML 片段:
列表页:
list HTTP=200 <!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="utf-8"> <title>文章列表</title> </head> <body> <h1>我的博客 - 文章列表</h1> <ul> <li><a href="/article/3/">用 OpenCV 做一个人脸检测器</a> -- 2026-07-24 02:24</li> <li><a href="/article/2/">Django 的 MTV 到底怎么理解</a> -- 2026-07-24 02:24</li> <li><a href="/article/1/">Python 入门:从变量到函数</a> -- 2026-07-24 02:24</li> </ul> </body>注意三条按created_at倒序排列(最新的 OpenCV 在最上),说明order_by("-created_at")生效了。
详情页(pk=1):
detail HTTP=200 <h1>Python 入门:从变量到函数</h1> <p><small>发布时间:2026-07-24 02:24</small></p> <div><p>本文带你从最基础的变量讲起,逐步过渡到函数与模块的组织方式。</p></div> <p><a href="/">← 返回列表</a></p>admin 后台:直接访问/admin/会302 跳转到登录页(未登录),登录页本身返回 200:
admin HTTP=302 login HTTP=200 <title>Log in | Django site admin</title> <link rel="stylesheet" href="/static/admin/css/base.css"> ...这说明后台路由、静态资源、登录页全部正常。
纪律:验证完立刻
pkill关掉 runserver,不留长驻进程。本文所有截图对应的服务进程在实验结束后均已确认关闭(NO_RUNSERVER_LEFT)。
九、踩坑实录:ALLOWED_HOSTS 报错(先复现再修复)
这是部署 Django 最高频的坑。默认settings.py里ALLOWED_HOSTS = [],意思是「只允许 localhost」。一旦你用公网 IP 或域名访问,Django 出于安全直接拒绝。我先把它真实复现出来。
用非白名单的Host头访问(等价于拿公网 IP 打过去):
curl-s-H"Host: example.com"http://127.0.0.1:8000/真实回显——浏览器拿到的是 Django 的 400 报错页,日志里写得明明白白:
<title>DisallowedHost at /</title> ... django.core.exceptions.DisallowedHost: Invalid HTTP_HOST header: 'example.com'. You may need to add 'example.com' to ALLOWED_HOSTS. [24/Jul/2026 02:27:50] "GET / HTTP/1.1" 400 60167复现成功:400 DisallowedHost。
修复:把ALLOWED_HOSTS改成允许的主机。开发阶段可用通配(生产请写具体域名):
sed-i's/^ALLOWED_HOSTS = .*/ALLOWED_HOSTS = ["*"]/'myblog/settings.pygrep-nALLOWED_HOSTS myblog/settings.py28:ALLOWED_HOSTS = ["*"]重启服务后再 curl 列表页,直接200,HTML 如前所示。这个「先复现、再修复」的过程,比直接给结论更有价值——你下次线上看到 400,一眼就知道是ALLOWED_HOSTS的事。
十、踩坑清单
| 坑 | 现象 | 解决办法 |
|---|---|---|
python命令找不到 | python: command not found | 用 venv 的python,或export PATH=/root/lab-d/venv/bin:$PATH |
| 模型不识别 | Model class ... isn't in an application in INSTALLED_APPS | 把 app 名加进INSTALLED_APPS(Django 6 默认单引号) |
| 迁移报错 | makemigrations不生成文件 | 确认模型确实被 app 收录;改完模型要重新生成迁移 |
| 超管创建卡交互 | createsuperuser一直等输入 | 用DJANGO_SUPERUSER_PASSWORD/USERNAME/EMAIL+--noinput |
| ALLOWED_HOSTS 400 | DisallowedHost: Invalid HTTP_HOST header | 把访问用的主机加进ALLOWED_HOSTS(生产写具体域名,别用*) |
| pkill 误杀自己 | pkill -f runserver返回 -1、命令中断 | 用pkill -f 'runserv[e]r'避免模式匹配到自身命令行 |
| 后台进程被杀 | 本地驱动被外壳截断、服务起不来 | 用setsid ... < /dev/null > /dev/null 2>&1 &彻底脱离会话 |
十一、总结
这一篇把「写网页」这件事拆成了清晰的链路:模型建表 → 迁移落库 → 后台注册 → 视图取数 → 模板渲染 → URL 路由 → runserver 验证。每一步都有真实回显佐证,不是「理论上能跑」。
几个关键认知:
- MTV 不是新概念,只是把 MVC 的 Controller 拆成了「URL 路由 + View 逻辑」两部分;
- 迁移是增量且可回溯的,这是 Django 管理 Schema 的核心优势;
ALLOWED_HOSTS是安全闸门,开发期图省事用*可以,上线必须收口到具体域名;- 验证要用 curl 看真实 HTTP 状态和 HTML,而不是凭感觉。
至此,一个能看文章、能进后台的个人 Blog 已经跑通。下一篇我把视线转向更「硬核」的方向——用 OpenCV 在图片里把人脸框出来。
本文实验均在华为云 Flexus X 实例(Ubuntu 24.04, Python 3.12.3)上真实执行。文中SECRET_KEY由 Django 自动生成,已打码处理,未以明文呈现。