第4章 4.2 路由与请求处理
🎯 为什么你需要一个"导航系统"?
上一章我们学会了用 http 模块搭起了最简单的服务器,就像建了一个只有一个人的公司——不管谁来访,都是同一个人接待。
但现实是这样的吗?
你去银行,柜员、大堂经理、VIP接待员——每个人只处理特定业务。你去餐厅,点菜找服务员、结账找收银台、开发票找前台。如果所有人都干同一件事,效率早就完蛋了。
你的网站也一样:
- 用户访问 /login → 应该显示登录页
- 用户访问 /api/users → 应该返回用户数据
- 用户访问 /admin → 应该显示管理后台
没有路由的服务器,就像一个不会分诊的挂号台——不管看什么病都让你找同一个医生。
这一章我们要学的是:怎么让不同的"病人"(请求)找到对的"科室"(处理函数)。
学完你能做到:
1. 让 /hello 和 /goodbye 走不同的处理逻辑
2. 从 URL 中提取参数(比如 /user/123 里的 123)
3. 读取 GET 参数(?name=张三)和 POST 数据
4. 返回不同状态码(404 Not Found、200 OK、500 错误)
🧱 基础:路由到底是什么?
路由 = 地图 + 导航
想象你第一次去北京南锣鼓巷:
你(请求)
想去"猫咖"
地图(路由表)
"猫咖在巷子中间位置"
导航到正确的店铺(处理函数)
路由表就是这张地图,路由就是根据地图找路的过程。
用 Flask 5 分钟搭一个路由服务器
Flask 是 Python 里最流行的轻量级 Web 框架,就像一个"路由乐高"——你想搭什么房子,它就给你提供什么砖块。
# 1. 导入 Flask(从 flask 包里拿 Flask 类)
from flask import Flask
# 2. 创建应用实例(实例化Flask对象)
app = Flask(__name__)
# 3. 定义路由:用户访问 /hello 时,交给 say_hello 处理
@app.route('/hello')
def say_hello():
return '你好,欢迎光临!'
# 4. 再来一个路由
@app.route('/goodbye')
def say_goodbye():
return '下次见!'
# 5. 启动服务器
if __name__ == '__main__':
app.run(port=3000)
运行这个程序,然后在浏览器访问:
- http://localhost:3000/hello → 看到 "你好,欢迎光临!"
- http://localhost:3000/goodbye → 看到 "下次见!"
发生了什么?
@app.route('/hello') 这行代码做了三件事:
1. 注册路由:告诉 Flask "以后有人访问 /hello,来找我"
2. 绑定函数:把 say_hello 函数绑定到这个路径
3. 返回响应:当访问发生时,执行函数并返回结果
这里的
@符号叫做"装饰器",你可以把它想象成贴标签——在函数身上贴一个标签,写上"这个函数对应 /hello 路径"。

4 种请求方法:GET、POST、PUT、DELETE
你去奶茶店:
| 动作 | 现实类比 | HTTP方法 | 做什么 |
|---|---|---|---|
| 看菜单 | 只是浏览,不需要告诉店员什么 | GET |
获取数据 |
| 点单 | 告诉店员你要什么 | POST |
提交新数据 |
| 修改订单 | 改了备注/加个料 | PUT |
更新现有数据 |
| 取消订单 | 不要这杯了 | DELETE |
删除数据 |
默认情况下,@app.route() 只响应 GET 请求。如果你想处理 POST,需要明确指定:
from flask import Flask, request
app = Flask(__name__)
# methods 参数告诉 Flask:这个路由接受哪些方法
@app.route('/submit', methods=['GET', 'POST'])
def handle_submit():
# request.method 可以获取当前请求的方法
if request.method == 'POST':
return '收到POST请求!'
else:
return '这是GET请求'
# 单独写一个只接受POST的路由
@app.route('/api/data', methods=['POST'])
def create_data():
return '创建数据成功!'
从 URL 中提取参数:动态路由
有时候 URL 里要有变量,比如 /user/123——123 是用户 ID。
from flask import Flask
app = Flask(__name__)
# <int:user_id> 表示这个位置是个整数,名字叫 user_id
@app.route('/user/<int:user_id>')
def get_user(user_id):
return f'你正在查看用户ID: {user_id}'
# <string:name> 表示字符串类型
@app.route('/greet/<string:name>')
def greet(name):
return f'你好,{name}!'
# 也可以用默认的字符串类型
@app.route('/blog/<path:category>')
def show_category(category):
return f'当前分类:{category}'
访问效果:
- /user/456 → 输出 "你正在查看用户ID: 456"
- /greet/小明 → 输出 "你好,小明!"
- /blog/python/advanced → 输出 "当前分类:python/advanced"
类型转换器:
<int:>只会匹配整数,<string:>匹配字符串(不含斜杠),<path:>匹配包含斜杠的路径。
读取查询参数:?name=张三&age=20
URL 后面可以带参数,格式是 ?key=value&key2=value2。
http://localhost:3000/search?keyword=苹果&category=水果
在 Flask 里,用 request.args 来获取这些参数:
from flask import Flask, request
app = Flask(__name__)
@app.route('/search')
def search():
# request.args 是字典类型,类似 dict.get()
keyword = request.args.get('keyword', '') # 如果没有keyword,默认空字符串
category = request.args.get('category', '')
return f'搜索关键词:{keyword},分类:{category}'
# 访问 /search?keyword=苹果&category=水果
# 输出:搜索关键词:苹果,分类:水果
读取 POST 表单数据
GET 参数在 URL 里,POST 数据在请求体里。Flask 用 request.form 获取:
from flask import Flask, request
app = Flask(__name__)
@app.route('/login', methods=['GET', 'POST'])
def login():
if request.method == 'POST':
# 从表单数据中获取 username 和 password
username = request.form.get('username')
password = request.form.get('password')
if username == 'admin' and password == '123456':
return '登录成功!'
else:
return '用户名或密码错误', 401 # 401 = 未授权
else:
# GET 请求显示登录表单
return '''
<form method="POST">
用户名:<input name="username"><br>
密码:<input name="password" type="password"><br>
<button type="submit">登录</button>
</form>
'''
返回 JSON 数据
现在的应用大多用 JSON 做数据交换。Flask 提供了 jsonify 函数:
from flask import Flask, jsonify
app = Flask(__name__)
@app.route('/api/user/<int:user_id>')
def get_user(user_id):
# 构建一个字典
user_data = {
'id': user_id,
'name': '张三',
'email': 'zhangsan@example.com'
}
# jsonify 把字典转成 JSON 字符串,并设置正确的 Content-Type
return jsonify(user_data)
访问 /api/user/1 会返回:
{
"id": 1,
"name": "张三",
"email": "zhangsan@example.com"
}

状态码:服务器在说什么?
HTTP 状态码是服务器给浏览器的"一句话说明":
| 状态码 | 意思 | 什么时候用 |
|---|---|---|
| 200 | OK,一切正常 | 成功响应 |
| 201 | Created,创建成功 | 新资源被创建 |
| 400 | Bad Request,请求有问题 | 参数错误、格式不对 |
| 401 | Unauthorized,没授权 | 需要登录 |
| 403 | Forbidden,禁止访问 | 没权限 |
| 404 | Not Found,找不到 | 资源不存在 |
| 500 | Internal Server Error,服务器崩了 | 代码出错 |
from flask import Flask, abort
app = Flask(__name__)
@app.route('/user/<int:user_id>')
def get_user(user_id):
if user_id > 1000:
# 用户ID超过1000的暂时不支持
abort(404) # 直接返回404
return f'用户 {user_id} 的信息'
@app.route('/admin')
def admin():
# 你可以手动返回不同状态码
return '管理员面板', 403 # 明确返回403禁止访问
响应头:额外的元数据
响应头是给浏览器看的"使用说明书":
from flask import Flask, jsonify, make_response
app = Flask(__name__)
@app.route('/data')
def get_data():
data = {'name': '测试数据'}
response = make_response(jsonify(data))
# 设置自定义响应头
response.headers['X-Custom-Header'] = 'Hello'
response.headers['Access-Control-Allow-Origin'] = '*'
return response
@app.route('/download')
def download():
# 设置 Content-Disposition 让浏览器下载而不是显示
response = make_response('这是要下载的文本内容')
response.headers['Content-Disposition'] = 'attachment; filename=demo.txt'
response.headers['Content-Type'] = 'text/plain; charset=utf-8'
return response
🔥 实战:3 个递进项目
项目 1:简易计算器(5 分钟)
目标:做一个支持加减乘除的计算器
from flask import Flask, request, jsonify
app = Flask(__name__)
@app.route('/calc')
def calc():
# 从 URL 参数获取 a、b 和操作符
a = float(request.args.get('a', 0))
b = float(request.args.get('b', 0))
op = request.args.get('op', '+') # 默认加法
if op == '+':
result = a + b
elif op == '-':
result = a - b
elif op == '*':
result = a * b
elif op == '/':
if b == 0:
return jsonify({'error': '除数不能为0'}), 400
result = a / b
else:
return jsonify({'error': f'不支持的操作符: {op}'}), 400
return jsonify({
'a': a,
'b': b,
'op': op,
'result': result
})
if __name__ == '__main__':
app.run(port=3000)
测试方式:浏览器访问
- http://localhost:3000/calc?a=10&b=5&op=+ → {"a": 10.0, "b": 5.0, "op": "+", "result": 15.0}
- http://localhost:3000/calc?a=10&b=3&op=/ → {"a": 10.0, "b": 3.0, "op": "/", "result": 3.333...}
一句话解释:用 request.args.get() 读取 URL 参数,根据 op 执行对应运算,返回 JSON 结果。
项目 2:待办清单 API(15 分钟)
目标:做一个带增删改查的待办清单后端,数据存在内存里(重启会丢失,生产环境要用数据库)。
from flask import Flask, request, jsonify
app = Flask(__name__)
# 模拟数据库:用列表存储待办事项
todos = [
{'id': 1, 'title': '买牛奶', 'done': False},
{'id': 2, 'title': '写周报', 'done': True},
]
next_id = 3 # 下一个新任务的ID
# ---------- 获取所有待办 ----------
@app.route('/api/todos', methods=['GET'])
def list_todos():
return jsonify(todos)
# ---------- 创建新待办 ----------
@app.route('/api/todos', methods=['POST'])
def create_todo():
global next_id
data = request.get_json() # 获取POST请求的JSON数据
title = data.get('title', '')
if not title:
return jsonify({'error': '标题不能为空'}), 400
todo = {
'id': next_id,
'title': title,
'done': False
}
todos.append(todo)
next_id += 1
return jsonify(todo), 201 # 201 = Created
# ---------- 获取单个待办 ----------
@app.route('/api/todos/<int:todo_id>', methods=['GET'])
def get_todo(todo_id):
todo = next((t for t in todos if t['id'] == todo_id), None)
if todo is None:
return jsonify({'error': '待办不存在'}), 404
return jsonify(todo)
# ---------- 更新待办(标记完成/修改标题) ----------
@app.route('/api/todos/<int:todo_id>', methods=['PUT'])
def update_todo(todo_id):
todo = next((t for t in todos if t['id'] == todo_id), None)
if todo is None:
return jsonify({'error': '待办不存在'}), 404
data = request.get_json()
if 'title' in data:
todo['title'] = data['title']
if 'done' in data:
todo['done'] = data['done']
return jsonify(todo)
# ---------- 删除待办 ----------
@app.route('/api/todos/<int:todo_id>', methods=['DELETE'])
def delete_todo(todo_id):
global todos
original_len = len(todos)
todos = [t for t in todos if t['id'] != todo_id]
if len(todos) == original_len:
return jsonify({'error': '待办不存在'}), 404
return jsonify({'message': '删除成功'})
if __name__ == '__main__':
app.run(port=3000)
测试方式(可以用 Postman 或 curl):
# 获取所有
curl http://localhost:3000/api/todos
# 创建新的
curl -X POST http://localhost:3000/api/todos \
-H "Content-Type: application/json" \
-d '{"title":"学习Flask"}'
# 标记为完成
curl -X PUT http://localhost:3000/api/todos/3 \
-H "Content-Type: application/json" \
-d '{"done":true}'
# 删除
curl -X DELETE http://localhost:3000/api/todos/3
一句话解释:用 RESTful 风格的路由设计,不同 HTTP 方法做不同操作,所有数据存在全局列表里。
项目 3:天气查询小工具(15 分钟)
目标:组合前两个项目的技能,做一个天气查询工具。输入城市名,返回假数据(模拟真实 API 的响应格式)。
from flask import Flask, request, jsonify, abort
app = Flask(__name__)
# 模拟天气数据库
weather_data = {
'北京': {'temp': 25, 'condition': '晴', 'humidity': 40},
'上海': {'temp': 28, 'condition': '多云', 'humidity': 65},
'广州': {'temp': 32, 'condition': '雷阵雨', 'humidity': 80},
'深圳': {'temp': 31, 'condition': '阵雨', 'humidity': 75},
}
# 根路径返回使用说明
@app.route('/')
def index():
return '''
<h1>天气查询小工具</h1>
<p>使用方法:</p>
<ul>
<li>GET /weather?city=城市名 - 查询天气</li>
<li>GET /weather/城市名 - 查询天气(URL方式)</li>
<li>GET /cities - 获取支持的城市列表</li>
</ul>
'''
# 获取支持的城市列表
@app.route('/cities', methods=['GET'])
def list_cities():
cities = list(weather_data.keys())
return jsonify({'cities': cities, 'count': len(cities)})
# 查询天气(参数方式)
@app.route('/weather', methods=['GET'])
def query_weather():
city = request.args.get('city', '')
if not city:
return jsonify({'error': '请提供城市名,格式:/weather?city=北京'}), 400
if city not in weather_data:
return jsonify({'error': f'暂不支持城市:{city}'}), 404
data = weather_data[city]
return jsonify({
'city': city,
'temperature': f"{data['temp']}°C",
'condition': data['condition'],
'humidity': f"{data['humidity']}%"
})
# 查询天气(动态路由方式)
@app.route('/weather/<string:city>', methods=['GET'])
def get_weather(city):
if city not in weather_data:
return jsonify({'error': f'暂不支持城市:{city}'}), 404
data = weather_data[city]
return jsonify({
'city': city,
'temperature': f"{data['temp']}°C",
'condition': data['condition'],
'humidity': f"{data['humidity']}%"
})
# 批量查询
@app.route('/weather/batch', methods=['POST'])
def batch_weather():
data = request.get_json()
cities = data.get('cities', [])
results = []
for city in cities:
if city in weather_data:
info = weather_data[city]
results.append({
'city': city,
'temperature': f"{info['temp']}°C",
'condition': info['condition']
})
else:
results.append({
'city': city,
'error': '不支持的城市'
})
return jsonify({'results': results})
if __name__ == '__main__':
app.run(port=3000)
测试:
- http://localhost:3000/ → 显示使用说明
- http://localhost:3000/weather?city=北京 → 返回北京天气
- http://localhost:3000/weather/上海 → 返回上海天气
- http://localhost:3000/cities → 返回支持的城市列表
# 批量查询
curl -X POST http://localhost:3000/weather/batch \
-H "Content-Type: application/json" \
-d '{"cities":["北京","东京","广州"]}'
一句话解释:把项目1的参数读取和项目2的 RESTful 路由组合起来,实现了单查、批量查两种模式。
💪 进阶:5 个新手常踩的坑
坑 1:路由顺序搞反
❌ 错误:
@app.route('/user/<int:user_id>')
def get_user(user_id):
return f'用户{user_id}'
@app.route('/user/new') # 这个永远匹配不到!因为 /user/123 会先匹配
def new_user():
return '新建用户'
✅ 正确:把静态路由放在动态路由前面
@app.route('/user/new') # 先写静态的
def new_user():
return '新建用户'
@app.route('/user/<int:user_id>') # 再写动态的
def get_user(user_id):
return f'用户{user_id}'
类比:就像查字典,具体词要先查,拼音查字法(动态的)要后查。
坑 2:忘记处理 POST 请求的 Content-Type
❌ 错误:
# 前端发送 JSON,但后端没配置
data = request.form.get('data') # 拿不到!form只管表单格式
✅ 正确:明确指定 JSON 格式
# 前端发送 JSON
data = request.get_json() # 自动解析 Content-Type: application/json
坑 3:路由参数类型写错
❌ 错误:
@app.route('/user/<int:user_id>') # 只接受整数
def get_user(user_id):
return f'用户{user_id}'
# 访问 /user/abc 会返回 404
✅ 正确:根据实际需求选择类型
@app.route('/user/<string:user_id>') # 字符串更宽松
def get_user(user_id):
return f'用户{user_id}'
坑 4:返回中文乱码
❌ 错误:
return '你好' # 某些情况下会乱码
✅ 正确:用 jsonify 或设置编码
from flask import jsonify
return jsonify({'message': '你好'}) # jsonify 自动处理 UTF-8
坑 5:调试模式没开,代码改了要重启
❌ 错误:改了代码,刷新浏览器没反应
✅ 正确:开启调试模式
if __name__ == '__main__':
app.run(port=3000, debug=True) # debug=True 改代码自动重载
调试技巧:打印请求信息
@app.route('/debug-demo')
def debug_demo():
# 把所有请求信息打印出来
print(f'Method: {request.method}')
print(f'URL: {request.url}')
print(f'Args: {request.args}')
print(f'Headers: {dict(request.headers)}')
return '查看终端输出'
在终端运行服务器,所有请求信息都会打印出来,比 console.log 还方便。
✏️ 练习题
练习 1(2 分钟):换个路径
- 输入:把项目 1 计算器的路由从 /calc 改成 /calculate
- 预期输出:访问 /calculate?a=5&b=3&op=* 返回 {"result": 15.0}
- 提示:改 @app.route() 里的路径字符串
练习 2(2 分钟):加个取反功能
- 输入:在计算器里加一个 op='neg' 表示取反(-a)
- 预期输出:/calc?a=10&op=neg 返回 {"result": -10}
- 提示:在 if-elif 链里加一个新分支
练习 3(3 分钟):加个"已完成"的筛选
- 输入:在项目 2 的待办清单里,加一个 ?done=true 参数,只返回已完成的待办
- 预期输出:/api/todos?done=true 只返回 done:true 的项
- 提示:用列表推导式过滤 [t for t in todos if t['done']]
练习 4(3 分钟):串联天气和待办
- 输入:用项目 3 的天气查询,把"记得带伞"加到天气是"雨"的待办里
- 预期输出:查询北京(不下雨)不添加,查询广州(下雨)自动添加一条"记得带伞"
- 提示:用 request.get_json() 拿到城市列表后,查询每个城市的天气
练习 5(5 分钟):分析这个报错
TypeError: 'NoneType' object is not subscriptable
@app.route('/user/<int:user_id>')
def get_user(user_id):
return jsonify(todos[user_id]) # 这行报错
- 预期输出:说出为什么报错,以及怎么修复
- 提示:
todos是列表,索引从 0 开始,不是从 id 开始
作业:做一个个人书签管理器
做一个 Flask 小应用,管理你的网页书签。
需求描述:帮自己收藏的网址分类,支持增删改查。
功能点:
1. GET /bookmarks - 列出所有书签(支持 ?category=编程 筛选)
2. POST /bookmarks - 添加书签,参数:title(标题)、url(链接)、category(分类)
3. DELETE /bookmarks/<id> - 删除指定书签
4. GET /categories - 列出所有分类
加分项:
1. 书签重复检查(同一 URL 不能重复添加)
2. 简单的标签功能(tags 字段,存列表)
验收标准:
- 能跑起来(python app.py)
- 4 个功能都能用 curl 测试通过
- 代码有注释,说明每个路由在干什么
提交方式:把代码贴在评论区,或者丢到 GitHub 把链接发出来。
📚 总结
这一章学了 3 件事:
1. 路由系统:用 @app.route() 把 URL 映射到处理函数
2. 请求对象:request.args 拿 GET 参数,request.form 拿表单,request.get_json() 拿 JSON
3. 响应控制:状态码、响应头、JSON 返回
延伸资源:
- Flask 官方文档:https://flask.palletsprojects.com/ (英文看不懂就开翻译)
- 《Flask Web 开发》—— 实战性很强的书
- 视频:B 站搜索 "Flask 入门" 一大把,选个播放量高的跟着敲
互动钩子:
下一章我们要给这个服务器加上"静态文件服务"——让它能托管 HTML、CSS、图片这些文件。你现在的 Flask 应用只能返回代码,下一章让它变成真正的网站!
你在做项目时遇到过什么路由相关的坑? 比如 / 和 /user 哪个先定义、参数类型写错导致 404 什么的,评论区聊聊,老粉优先回复!

评论(0)