第5章 5.5 综合实战:博客系统后端
🎯 开场 3 分钟:为什么要学这个?
上一章我们学会了用 ORM 连接数据库,再也不用写那些让人头疼的 SQL 语句了。但光会查数据不够——你肯定想要一个真正能用的系统,比如一个博客后端。
你有没有遇到过这种情况:写了个博客系统,数据存是存了,但代码乱成一锅粥?文章、标签、评论分开存,调用时东一块西一块,自己都看不懂了?
这一章我们把这些知识串起来,做一个完整的博客后端 API。学完你可以:
- 用一套代码管理文章、标签、评论
- 告别「代码能跑但看不懂」的尴尬
- 下一章学习 class 的时候,你会有一个真实项目等着它
🧱 基础 25 分钟:核心概念
5.5.1 什么是 RESTful API?
是什么:API 就是「菜单」,RESTful 是「点菜的方式」。
你去餐厅吃饭,菜单上写着「宫保鸡丁 28元」——这就是接口文档。你说「来一份宫保鸡丁」,服务员就给你端菜来。API 也是一样:客户端发请求,服务器返回数据。
为什么要用:想\n\n
\n\n
\n\n象一下,如果没有统一的方式,博客前台、后台、移动端都要自己写一堆乱七八糟的接口。RESTful 规定了一套「怎么说话」的规则,大家统一用,效率翻倍。
怎么用:
# 这是一个简单的 Flask API 示例
from flask import Flask, jsonify
app = Flask(__name__)
# 模拟数据库里的文章
articles = [
{"id": 1, "title": "Python 入门", "content": "内容略..."},
{"id": 2, "title": "Flask 实战", "content": "内容略..."}
]
@app.route("/api/articles", methods=["GET"])
def get_articles():
"""获取所有文章 - 相当于"给我看看菜单" """
return jsonify(articles)
if __name__ == "__main__":
app.run(debug=True)
运行后访问 http://127.0.0.1:5000/api/articles,就能看到文章列表了。
5.5.2 什么是 CRUD?
是什么:CRUD = Create(创建)、Read(读取)、Update(更新)、Delete(删除)。
为什么要用:这四个字涵盖了所有对数据的基本操作。博客系统里:
- 发一篇新文章 = Create
- 看文章列表 = Read
- 修改文章 = Update
- 删除文章 = Delete
怎么用:
from flask import Flask, jsonify, request
app = Flask(__name__)
# 模拟数据库(用列表代替)
articles = []
@app.route("/api/articles", methods=["POST"])
def create_article():
"""创建文章 - POST 请求"""
new_article = request.json # 从请求体获取数据
new_article["id"] = len(articles) + 1
articles.append(new_article)
return jsonify(new_article), 201 # 201 = 创建成功
@app.route("/api/articles/<int:article_id>", methods=["GET"])
def get_article(article_id):
"""读取单篇文章 - GET 请求"""
for article in articles:
if article["id"] == article_id:
return jsonify(article)
return jsonify({"error": "文章不存在"}), 404
@app.route("/api/articles/<int:article_id>", methods=["PUT"])
def update_article(article_id):
"""更新文章 - PUT 请求"""
for article in articles:
if article["id"] == article_id:
article.update(request.json)
return jsonify(article)
return jsonify({"error": "文章不存在"}), 404
@app.route("/api/articles/<int:article_id>", methods=["DELETE"])
def delete_article(article_id):
"""删除文章 - DELETE 请求"""
global articles
articles = [a for a in articles if a["id"] != article_id]
return jsonify({"message": "删除成功"}), 200
if __name__ == "__main__":
app.run(debug=True)
5.5.3 什么是资源关联(标签和评论)?
是什么:一篇文章可以有多个标签,一篇文章也可以有多条评论。标签和评论都「属于」某篇文章。
为什么要用:真实业务里,数据不是孤立的,是一张网。文章和标签是多对多关系(一个文章多个标签,一个标签多个文章),文章和评论是一对多关系。
怎么用:
# 用字典模拟关联关系
articles_db = []
tags_db = [] # 标签表
comments_db = [] # 评论表
def get_article_with_details(article_id):
"""获取文章的同时,把标签和评论也带上"""
article = None
for a in articles_db:
if a["id"] == article_id:
article = a.copy()
break
if not article:
return None
# 筛选出这篇文章的标签
article["tags"] = [t for t in tags_db if article_id in t["article_ids"]]
# 筛选出这篇文章的评论
article["comments"] = [c for c in comments_db if c["article_id"] == article_id]
return article
🔥 实战 35 分钟:3 个递进的小项目
项目 1(5 分钟):Flask 博客 API 基础版
目标:搭建一个能跑起来的博客 API,支持查看所有文章和查看单篇文章。
# blog_api_v1.py
from flask import Flask, jsonify
app = Flask(__name__)
# 模拟数据库
articles = [
{"id": 1, "title": "Python 环境搭建", "content": "首先安装 Python..."},
{"id": 2, "title": "Flask 快速入门", "content": "Flask 是一个轻量级框架..."},
{"id": 3, "title": "SQLAlchemy 基础", "content": "ORM 让数据库操作更简单..."}
]
@app.route("/api/articles", methods=["GET"])
def list_articles():
"""获取文章列表"""
return jsonify(articles)
@app.route("/api/articles/<int:article_id>", methods=["GET"])
def get_article(article_id):
"""获取单篇文章"""
for article in articles:
if article["id"] == article_id:
return jsonify(article)
return jsonify({"error": "文章不存在"}), 404
if __name__ == "__main__":
print("博客 API 已启动,访问 http://127.0.0.1:5000/api/articles")
app.run(debug=True, port=5000)
运行方式:
python blog_api_v1.py
预期输出:
博客 API 已启动,访问 http://127.0.0.1:5000/api/articles
* Running on http://127.0.0.1:5000
访问 http://127.0.0.1:5000/api/articles 看到 JSON 格式的文章列表。
一句话解释:这个项目用 Flask 搭了一个最简单的 API,访问 /api/articles 就返回所有文章。
项目 2(15 分钟):完整 CRUD + 标签功能
目标:在项目 1 基础上,加上创建、修改、删除文章的功能,以及标签管理。
# blog_api_v2.py
from flask import Flask, jsonify, request
app = Flask(__name__)
# 数据库模拟
articles = [
{"id": 1, "title": "Python 环境搭建", "content": "首先安装 Python..."},
{"id": 2, "title": "Flask 快速入门", "content": "Flask 是一个轻量级框架..."}
]
tags = [
{"id": 1, "name": "Python", "article_ids": [1, 2]},
{"id": 2, "name": "Flask", "article_ids": [2]}
]
comments = []
# ===== 文章 CRUD =====
@app.route("/api/articles", methods=["GET"])
def list_articles():
return jsonify(articles)
@app.route("/api/articles", methods=["POST"])
def create_article():
data = request.json
new_article = {
"id": max([a["id"] for a in articles]) + 1 if articles else 1,
"title": data.get("title", ""),
"content": data.get("content", "")
}
articles.append(new_article)
return jsonify(new_article), 201
@app.route("/api/articles/<int:article_id>", methods=["GET"])
def get_article(article_id):
for article in articles:
if article["id"] == article_id:
# 带上这篇文章的标签
article_tags = [t for t in tags if article_id in t["article_ids"]]
result = article.copy()
result["tags"] = article_tags
return jsonify(result)
return jsonify({"error": "文章不存在"}), 404
@app.route("/api/articles/<int:article_id>", methods=["PUT"])
def update_article(article_id):
for article in articles:
if article["id"] == article_id:
data = request.json
article["title"] = data.get("title", article["title"])
article["content"] = data.get("content", article["content"])
return jsonify(article)
return jsonify({"error": "文章不存在"}), 404
@app.route("/api/articles/<int:article_id>", methods=["DELETE"])
def delete_article(article_id):
global articles, tags, comments
articles = [a for a in articles if a["id"] != article_id]
# 清理关联的标签
for tag in tags:
if article_id in tag["article_ids"]:
tag["article_ids"].remove(article_id)
# 清理关联的评论
comments = [c for c in comments if c["article_id"] != article_id]
return jsonify({"message": "删除成功"})
# ===== 标签管理 =====
@app.route("/api/tags", methods=["GET"])
def list_tags():
return jsonify(tags)
@app.route("/api/tags", methods=["POST"])
def create_tag():
data = request.json
new_tag = {
"id": max([t["id"] for t in tags]) + 1 if tags else 1,
"name": data.get("name", ""),
"article_ids": data.get("article_ids", [])
}
tags.append(new_tag)
return jsonify(new_tag), 201
@app.route("/api/tags/<int:tag_id>/articles", methods=["GET"])
def get_articles_by_tag(tag_id):
"""获取某个标签下的所有文章"""
for tag in tags:
if tag["id"] == tag_id:
tag_articles = [a for a in articles if a["id"] in tag["article_ids"]]
return jsonify({"tag": tag["name"], "articles": tag_articles})
return jsonify({"error": "标签不存在"}), 404
# ===== 评论功能 =====
@app.route("/api/articles/<int:article_id>/comments", methods=["GET"])
def list_comments(article_id):
article_comments = [c for c in comments if c["article_id"] == article_id]
return jsonify(article_comments)
@app.route("/api/articles/<int:article_id>/comments", methods=["POST"])
def add_comment(article_id):
# 检查文章是否存在
if not any(a["id"] == article_id for a in articles):
return jsonify({"error": "文章不存在"}), 404
data = request.json
new_comment = {
"id": max([c["id"] for c in comments]) + 1 if comments else 1,
"article_id": article_id,
"author": data.get("author", "匿名"),
"content": data.get("content", "")
}
comments.append(new_comment)
return jsonify(new_comment), 201
if __name__ == "__main__":
print("博客 API v2 已启动")
print("测试命令:curl http://127.0.0.1:5000/api/articles")
app.run(debug=True, port=5000)
测试方法(在另一个终端运行):
# 获取所有文章
curl http://127.0.0.1:5000/api/articles
# 创建新文章
curl -X POST http://127.0.0.1:5000/api/articles \
-H "Content-Type: application/json" \
-d '{"title": "新文章", "content": "这是内容"}'
# 给文章添加评论
curl -X POST http://127.0.0.1:5000/api/articles/1/comments \
-H "Content-Type: application/json" \
-d '{"author": "小明", "content": "写得真好!"}'
预期输出:创建文章后会返回新文章的信息,包含自动生成的 id。
一句话解释:这个版本完整实现了文章和标签的增删改查,还加了评论功能,是一个小而全的博客后端。
项目 3(15 分钟):数据导入导出工具
目标:把博客数据导出成 JSON 文件,也能从 JSON 文件导入。
# blog_data_tool.py
import json
from flask import Flask, jsonify, request
app = Flask(__name__)
# 数据库
articles = []
tags = []
comments = []
DATA_FILE = "blog_data.json"
def save_to_file():
"""把所有数据保存到 JSON 文件"""
data = {
"articles": articles,
"tags": tags,
"comments": comments
}
with open(DATA_FILE, "w", encoding="utf-8") as f:
json.dump(data, f, ensure_ascii=False, indent=2)
print(f"数据已保存到 {DATA_FILE}")
def load_from_file():
"""从 JSON 文件加载数据"""
global articles, tags, comments
try:
with open(DATA_FILE, "r", encoding="utf-8") as f:
data = json.load(f)
articles = data.get("articles", [])
tags = data.get("tags", [])
comments = data.get("comments", [])
print(f"已从 {DATA_FILE} 加载数据")
except FileNotFoundError:
print("没有找到数据文件,将从空数据开始")
# 启动时加载数据
load_from_file()
# 现有的 CRUD API(简化版,省略部分代码)
@app.route("/api/articles", methods=["GET"])
def list_articles():
return jsonify(articles)
@app.route("/api/articles", methods=["POST"])
def create_article():
data = request.json
new_article = {
"id": max([a["id"] for a in articles]) + 1 if articles else 1,
"title": data.get("title", ""),
"content": data.get("content", "")
}
articles.append(new_article)
save_to_file() # 自动保存
return jsonify(new_article), 201
@app.route("/api/articles/<int:article_id>", methods=["DELETE"])
def delete_article(article_id):
global articles
articles = [a for a in articles if a["id"] != article_id]
save_to_file() # 自动保存
return jsonify({"message": "删除成功"})
# ===== 导入导出 API =====
@app.route("/api/export", methods=["GET"])
def export_data():
"""导出所有数据为 JSON"""
save_to_file() # 先保存最新数据
with open(DATA_FILE, "r", encoding="utf-8") as f:
data = f.read()
return data, 200, {"Content-Type": "application/json; charset=utf-8"}
@app.route("/api/import", methods=["POST"])
def import_data():
"""从 JSON 导入数据(会覆盖现有数据)"""
global articles, tags, comments
data = request.json
articles = data.get("articles", [])
tags = data.get("tags", [])
comments = data.get("comments", [])
save_to_file()
return jsonify({"message": "导入成功", "count": len(articles)})
if __name__ == "__main__":
print("数据导入导出工具已启动")
app.run(debug=True, port=5000)
使用场景:
- 定期备份博客数据
- 从测试环境迁移数据到生产环境
- 批量导入预先准备好的文章
一句话解释:这个工具让数据持久化到文件,再也不怕程序重启后数据丢失了。
💪 进阶 20 分钟:常见坑 + 性能小贴士
坑 1:JSON 数据没加 Content-Type
# ❌ 错误:返回 JSON 但没告诉浏览器
return jsonify({"error": "找不到"})
# ✅ 正确:显式设置类型
return jsonify({"error": "找不到"}), 404
Flask 的 jsonify 会自动设置正确的 Content-Type,但如果直接返回字符串就要手动加。
坑 2:删除时没清理关联数据
# ❌ 错误:删了文章,但标签和评论还在
articles = [a for a in articles if a["id"] != article_id]
# ✅ 正确:把关联数据一起清理
articles = [a for a in articles if a["id"] != article_id]
tags = [t for t in tags if article_id not in t["article_ids"]]
comments = [c for c in comments if c["article_id"] != article_id]
数据关联就像绳结,解开一个要记得把其他结也解开。
坑 3:POST 请求没验证数据
# ❌ 危险:直接用,不检查有没有 title
@app.route("/api/articles", methods=["POST"])
def create_article():
new_article = request.json
new_article["id"] = len(articles) + 1
articles.append(new_article)
return jsonify(new_article), 201
# ✅ 安全:先检查必要字段
@app.route("/api/articles", methods=["POST"])
def create_article():
data = request.json
if not data.get("title"):
return jsonify({"error": "标题不能为空"}), 400
# ... 继续创建
坑 4:用列表存数据,生产环境会丢
# ❌ 开发没问题,生产会哭
articles = []
# ✅ 真实项目用数据库
# from flask_sqlalchemy import SQLAlchemy
# db = SQLAlchemy(app)
内存里的列表程序一关就没了。练手可以,真做项目必须上数据库。
坑 5:调试模式开太久
# ❌ 生产环境开 debug=True,会被黑
app.run(debug=True)
# ✅ 生产环境关掉
app.run(debug=False, host="0.0.0.0")
性能小贴士:加索引
# 如果用 SQLAlchemy,给常用查询字段加索引
class Article(db.Model):
__tablename__ = "articles"
id = db.Column(db.Integer, primary_key=True)
title = db.Column(db.String(100), index=True) # 给 title 加索引
索引就像书的目录,让查询快 10 倍不止。
调试技巧:打印请求日志
@app.before_request
def log_request():
print(f"[{request.method}] {request.path} - {request.json}")
每次请求来都会打印,方便追踪数据流。
✏️ 练习题 + 作业题
练习题(5 道,10 分钟)
练习 1(2 分钟):添加一个统计接口
- 输入:访问 /api/articles/count
- 预期输出:{"count": 3}
- 提示:用 len(articles) 直接数
练习 2(2 分钟):给文章列表加标题筛选
- 输入:访问 /api/articles?title=Python
- 预期输出:只返回标题含 "Python" 的文章
- 提示:用 request.args.get("title") 获取查询参数
练习 3(2 分钟):评论加个时间戳
- 输入:创建评论时
- 预期输出:评论里多个 "created_at": "2024-01-15 10:30:00"
- 提示:import datetime,然后 datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
练习 4(2 分钟):标签解耦
- 输入:删除一个标签
- 预期输出:该标签被删除,但不删除关联的文章
- 提示:只删标签本身,不动文章的 tag_ids 列表
练习 5(2 分钟):找出报错原因
- 输入:运行以下代码
@app.route("/api/articles/<int:id>", methods=["GET"])
def get_article(id):
return jsonify(articles[id]) # 报错!
- 预期输出:
{"error": "文章不存在"} - 提示:列表下标从 0 开始,但 id 从 1 开始,不能直接用
articles[id]
作业题(30 分钟 - 2 小时)
作业:做一个「博客评论管理工具」
需求描述:
做一个评论管理小工具,可以查看、添加、删除评论。
功能点:
1. 查看某篇文章的所有评论(GET)
2. 添加评论(POST),评论者名字不能为空
3. 删除评论(DELETE)
4. 统计每篇文章的评论数
加分项:
1. 评论按时间倒序(最新的在前)
2. 支持回复评论(用 parent_id 实现嵌套)
验收标准:
- 能跑起来
- curl 命令能测试通过
- 代码有注释
提交方式:评论区贴代码或 GitHub 链接
📚 总结 + 资源
本章核心
- RESTful API:用 HTTP 方法(GET/POST/PUT/DELETE)操作资源(文章/标签/评论)
- CRUD 模式:Create 创建、Read 读取、Update 更新、Delete 删除
- 数据关联:文章、标签、评论三者之间的关系要一起管理
延伸资源
- Flask 官方文档 - 最权威的学习资料
- RESTful API 设计指南 - 中文参考
- 《Flask Web 开发实战》- 深入学习 Flask 的好书
互动钩子
你在做博客系统的时候,数据关联是怎么处理的?有没有遇到过「删了文章但评论还在」的坑?评论区聊聊,老粉优先回复!
下章预告:
现在我们用列表、字典模拟了数据存储,但代码越写越乱……文章、标签、评论各有一套操作逻辑,函数名都快记不住了。下一章我们要学习 class 与命名空间,用它来把代码组织得更清晰。准备好了吗?

评论(0)