第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 路径"。

配图1 - 配图1

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"
}

配图2 - 配图2

状态码:服务器在说什么?

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 什么的,评论区聊聊,老粉优先回复!

声明:本站所有文章,如无特殊说明或标注,均为本站原创发布。任何个人或组织,在未征得本站同意时,禁止复制、盗用、采集、发布本站内容到任何网站、书籍等各类媒体平台。如若本站内容侵犯了原著者的合法权益,可联系我们进行处理。