【Django】-2- 处理HTTP请求
一、request 请求
先理解:Request
是啥?
用户访问你的网站时,会发一个 “请求包” 📦 ,里面装着:
- 想访问啥路径?用啥方法(GET/POST 等)?
- 带了啥头信息(比如 Cookies)?
- 有没有传表单、文件?
- 客户端 IP 是啥?
- 用户登录状态是啥?
Django 把这些信息封装成 HttpRequest
对象(一般叫 request
),视图函数里拿到它,就能 “拆包” 提取信息啦~
Request 请求的分类角度 🌐
这张图是 “总纲”,告诉你可以从 三个维度 分析 request
:
角度 | 关注啥内容? | 类比(超通俗) |
---|---|---|
HTTP 协议角度 | 请求行(方法、路径)、请求头、请求正文 | 快递单上的 “地址、电话、包裹内容” |
TCP 协议角度 | 客户端 IP(用户的网络地址) | 快递单上的 “发件人 IP 地址” |
网站功能角度 | 用户身份、Session(会话数据) | 快递单上的 “收件人信息、专属标记” |
HTTP 协议角度 👉 拆 “请求的核心内容”
代码里的 echo
视图,把 HTTP 请求的关键部分 提取出来,返回给用户看~
def echo(request: HttpRequest):# 用 f-string 拼接 HTML 内容,把 request 的信息嵌入进去html = f"""请求行 请求方法:{request.method} # 比如 GET/POST/PUT 等,告诉服务器“想干啥”请求路径:{request.path} # 比如 /hello123 ,告诉服务器“访问哪个页面”查询字符串:{request.GET} # 比如 ?name=beifan ,是 URL 里带的参数请求头 请求头:{request.headers} # 装着浏览器信息、Cookies 等,像“附加说明”请求正文 {request.body} # POST 请求时,表单、JSON 数据会存在这里"""return HttpResponse(html)
超通俗解释:
- 你访问网站时,浏览器会发一个 “超级详细的快递” 给服务器~
request.method
是 “快递单上的操作类型”(比如 “我要查询”“我要寄件”)request.path
是 “快递单上的地址”(比如 “北京市朝阳区 xxx 路”)request.headers
是 “快递单上的备注”(比如 “ fragile 易碎”“需要冷藏”)request.body
是 “包裹里的东西”(如果是 POST 请求,表单、文件会存在这里)
TCP 协议角度 👉 拿 “客户端 IP”
代码里的 echo
视图,专门提取 用户的 IP 地址 :
def echo(request: HttpRequest):# 提取客户端 IP ,这里处理了“代理转发”的情况(很多网站会用反向代理,比如 Nginx)ip = request.META.get("HTTP_X_FORWARDED_FOR", request.META['REMOTE_ADDR'])html = f"""客户端IP:{ip} # 用户的网络地址,比如 192.168.1.100"""return HttpResponse(html)
超通俗解释:
- 每个设备上网都有 “网络身份证”(IP 地址),服务器需要知道 “谁在访问我”~
- 有时候用户是通过 “代理服务器” 访问的(比如公司网络、CDN),
HTTP_X_FORWARDED_FOR
就是 “真实身份证”,REMOTE_ADDR
是 “代理的身份证”~ - 这段代码会优先拿 “真实身份证”,拿不到就用 “代理的身份证”~
网站功能角度 👉 拿 “用户身份 & Session”
代码里的 echo
视图,提取 和用户身份、会话相关的信息 :
def echo(request: HttpRequest):html = f"""当前用户:{request.user} # 登录后显示用户名,没登录可能是匿名用户Session数据:{request.session.get("name", "beifan")} # 从 Session 里取数据,没有就默认 "beifan""""return HttpResponse(html)
超通俗解释:
request.user
:告诉你 “当前是谁在访问”~ 登录后是用户名,没登录就是 “匿名用户”(像 “快递单上的收件人姓名”)request.session
:是服务器给用户的 “专属小本本”📒 ,用户每次访问都带着,能存登录状态、偏好设置等~- 比如你登录后,
session
里存了{"name": "beifan"}
,下次访问就能直接拿到!
- 比如你登录后,
核心逻辑 🌟
所有代码都是围绕 “如何从 request
里提取信息” 展开:
- 从 HTTP 协议角度:拆请求行、头、正文 → 看 “请求的原始内容”
- 从 TCP 协议角度:拆客户端 IP → 看 “谁在访问”
- 从 网站功能角度:拆用户身份、Session → 看 “用户是谁,有啥专属数据”
最终目的:让你明白 request
里 “藏着这么多有用的信息”,以后写视图时,想拿啥就从对应的地方取~
二、认识几个格式转换的函数
json.dumps
是 Python 标准库json
中的一个函数 ,它的主要作用是将 Python 对象(如字典、列表等)转换为 JSON 格式的字符串。
import json# 示例1:转换字典
data_dict = {"name": "Alice", "age": 25}
json_str = json.dumps(data_dict)
print(json_str)
# 输出: {"name": "Alice", "age": 25}# 示例2:转换列表
data_list = [1, 2, 3, "four"]
json_str = json.dumps(data_list)
print(json_str)
# 输出: [1, 2, 3, "four"]
ensure_ascii
:默认为True
,在这种情况下,非 ASCII 字符会被转义成 Unicode 编码形式(如\uXXXX
)。如果设置为False
,则会以原始的非 ASCII 字符形式输出。
data = {"名字": "张三"}
print(json.dumps(data))
# 输出: {"\u540d\u5b57": "\u5f20\u4e09"}print(json.dumps(data, ensure_ascii=False))
# 输出: {"名字": "张三"}
indent
:用于设置缩进,使输出的 JSON 字符串更具可读性,常用于调试或格式化输出。它可以是一个整数,表示缩进的空格数,也可以是字符串(如\t
表示制表符)。
data = {"name": "Bob", "hobbies": ["reading", "swimming"]}
print(json.dumps(data, indent=4))
# 输出:
# {
# "name": "Bob",
# "hobbies": [
# "reading",
# "swimming"
# ]
# }
sort_keys
:默认为False
,如果设置为True
,在转换字典时,会按照键的字母顺序对键值对进行排序后输出。
data = {"c": 3, "a": 1, "b": 2}
print(json.dumps(data))
# 输出: {"c": 3, "a": 1, "b": 2}print(json.dumps(data, sort_keys=True))
# 输出: {"a": 1, "b": 2, "c": 3}
json.loads
是 Python 的 json
模块里的一个函数,作用是 把 JSON 格式的字符串,转换成 Python 能直接用的数据类型(比如字典、列表)
data = json.loads(request.body)
request.body
:Django 里request
对象的body
属性,存的是 客户端发过来的原始数据(二进制字符串),比如客户端 POST 一个 JSON 数据{"c": 3}
,request.body
拿到的是b'{"c": 3}'
(二进制格式的 JSON 字符串 )。json.loads(...)
:把这个二进制字符串(转成普通字符串后),解析成 Python 的字典{"c": 3}
,这样data
就可以像普通字典一样用(比如data['c']
取到3
)。
json.loads
的核心作用:“JSON → Python”
import json# 这是一个 JSON 格式的字符串(字符串里的内容符合 JSON 语法)
json_str = '{"c": 3}'# 用 json.loads 解析,转成 Python 字典
python_dict = json.loads(json_str)print(python_dict) # 输出: {'c': 3}
print(type(python_dict)) # 输出: <class 'dict'>
- 输入:JSON 格式的字符串(必须用双引号,符合 JSON 规范 )。
- 输出:Python 的字典(或列表、数字等,取决于 JSON 内容 )。
三、为什么需要 json.loads
?
因为客户端和服务器通信时,只能传 “字符串”(HTTP 协议的限制 )。
如果客户端要传复杂数据(比如字典、列表),必须先转成 JSON 字符串(json.dumps
干的事 ),服务器收到后,再用 json.loads
转成 Python 能处理的数据类型。
就像你给外国朋友写信,得用 “国际通用语言(JSON 字符串)”,朋友收到后,用 json.loads
翻译成自己能懂的语言(Python 字典 )。
和 json.dumps
的关系(“反向操作”)
json.dumps
:把 Python 数据(字典、列表等)转成 JSON 字符串(Python → JSON
)。json.loads
:把 JSON 字符串转成 Python 数据(JSON → Python
)。
这俩是 “一对”,经常一起用在前后端数据交互场景:
- 前端用
JSON.stringify
(类似json.dumps
)把数据转成 JSON 字符串,发给后端。 - 后端用
json.loads
把 JSON 字符串转成 Python 数据,处理完后,再用json.dumps
转回 JSON 字符串,返回给前端。
三、认识Querydict类型
QueryDict
本质是 Python 对象,但它又和普通 dict
不太一样,这得从 Django 的设计逻辑说起
⭐ 先明确:所有能在 Python 里操作的东西,都是 “Python 对象”
Python 里几乎一切皆对象:
- 普通字典
{"a": 1}
是对象(dict
类型) - 字符串
'abc'
是对象(str
类型) - 甚至数字
123
也是对象(int
类型)
所以 QueryDict
作为 Django 定义的自定义类,自然也是 Python 对象,只不过它是 Django 为了处理 HTTP 请求参数,专门设计的 “特殊字典”~
⭐ QueryDict
是 “特殊定制的 Python 对象”
Django 里的 QueryDict
继承自 Python 的 dict
,但增强了处理 HTTP 请求参数的能力,核心是为了应对两个场景:
1. 支持 “一个键对应多个值”(HTTP 查询参数的特性)
HTTP 的查询参数(如 ?a=1&a=2
)允许同一个键(a
)出现多次,普通 dict
会直接覆盖值(只能存 {'a': 2}
),但 QueryDict
可以存成列表({'a': ['1', '2']}
)。
比如你的请求 URL 是 ?a=1&a=2
,Django 会把参数解析成:
request.GET # QueryDict: {'a': ['1', '2']}
这样就能正确处理 “同一个键传多个值” 的场景(比如复选框多选)。
2. 提供更贴合 HTTP 请求的方法
QueryDict
还加了很多实用方法,比如:
getlist('a')
:直接获取键a
对应的所有值(返回['1', '2']
)urlencode()
:把QueryDict
转成 URL 编码的字符串(a=1&a=2
)
这些方法让处理 HTTP 请求参数更方便,是普通 dict
做不到的~
四、为什么字典是单引号 json是双引号
这个问题涉及 Python 语法和 JSON 规范的差异,本质是两种 “数据格式” 的设计约定,咱们用 “方言 vs 国际语” 的思路理解~ 🗣️
Python 字典用单引号:语法灵活性
Python 里的字典,键值对的字符串可以用 单引号 或 双引号,甚至混用,这是 Python 语法的灵活性:
# 合法的 Python 字典
my_dict = {'name': "beifan", # 单引号、双引号混用"age": 18
}
Python 这么设计,是为了让开发者写代码时更自由(比如字符串里本身有双引号,就可以用单引号包起来,避免转义 )。
JSON 用双引号:国际标准规范
JSON 是 “跨语言的数据交换格式”,它的语法有严格规范,其中一条就是:字符串必须用双引号包裹。
这是因为 JSON 的设计目标是 “让所有语言都能统一解析”,而不同语言对字符串引号的支持不同:
- 比如 JavaScript 里,对象的键必须用双引号(
{"name": "beifan"}
是合法的,{'name': 'beifan'}
会报错 )。 - 为了兼容所有语言,JSON 强制规定用双引号,这样不管是 Python、JavaScript、Java 还是其他语言,都能一致解析。
json.dumps
的作用:自动转换引号
当你用 json.dumps
把 Python 字典转成 JSON 字符串时,Python 会自动做两件事:
- 把字典里的单引号,替换成双引号(符合 JSON 规范)。
- 处理其他 Python 特有的语法(比如
None
转成null
,True
转成true
)。
my_dict = {'name': 'beifan'}
json_str = json.dumps(my_dict)
# 输出: '{"name": "beifan"}'(双引号)
总结:两种格式的设计目标不同
- Python 字典:是 Python 语言内部使用的数据结构,语法灵活,方便开发者写代码。
- JSON:是跨语言的 “数据交换协议”,语法严格,确保所有语言都能统一解析。
所以 Python 字典用单引号(或双引号)都行,但转成 JSON 后必须用双引号 —— 这是为了让其他语言能看懂,实现 “跨语言交流”~
简单说:单引号是 Python 的 “方言”,双引号是 JSON 的 “国际语”,json.dumps
就是 “翻译官”,把 Python 方言翻译成国际通用的 JSON 语~ 😊
五、测试用例实操
项目结构
Django 服务端(views.py
):
写了 3 个视图函数(echo
/submit
/result
),负责接收请求、处理数据、返回响应
测试客户端(test_api.py
):
用 requests
库模拟客户端,给 Django 服务发 POST 请求(带 JSON 数据)
验证服务端返回的响应是否符合预期
🐇 echo
视图函数
def echo(request: HttpRequest):# 将 request.GET(QueryDict 类型)转成 JSON 字符串返回html = f"{json.dumps(request.GET)}" return HttpResponse(html)
(request:HttpRequest)类型注释:别人看到 request: HttpRequest
,不用猜就知道:这个参数是 Django 封装的 “请求对象”,里面有 method
、body
这些属性
request.GET
:Django 中专门用来接收 URL 查询参数(如?a=1
)的对象,类型是QueryDict
- 作用:访问该视图时,会把 URL 里的查询参数转成 JSON 字符串返回给客户端。(JSON 是前后端都能 “看懂” 的 “通用语言”,把数据转成 JSON 再返回,能让客户端(浏览器、App 等)更方便地处理数据~)
在这个 echo
视图函数中,HttpResponse(html)
会将经过处理后的 html
(这里是包含 URL 查询参数的 JSON 字符串 )作为响应体,加上默认的响应头(如 Content-Type: text/html; charset=utf-8
)和状态码 200,一起返回给客户端。
客户端(通常是浏览器)接收到这个 HTTP 响应后,会根据响应头中的 Content-Type
来决定如何处理响应体内容。如果是 text/html
,就会渲染展示 HTML 页面;如果是 application/json
,则可能会将其解析为 JavaScript 对象进行后续操作。
HttpResponse(html)
是 Django 视图函数将处理结果返回给客户端的关键步骤,它按照 HTTP 协议的规范,将数据包装成包含状态行、响应头和响应体的完整 HTTP 响应,从而实现服务器与客户端之间的数据交互和信息传递。
🐇 submit
视图函数
def submit(request):print(request.body) # 打印请求体(一般 POST 请求会用到,这里可能无实际数据)# 读取本地 submit.html 文件内容并返回html = open('submit.html', encoding="utf-8").read() return HttpResponse(html)
- 作用:访问该视图时,返回
submit.html
的内容,常用来展示前端页面。
🐇 test_api.py
测试逻辑
import json
import requests # 发送 GET 请求(带查询参数 ?a=1)
resp = requests.get("http://127.0.0.1:8000/beifan/echo?a=1")
print(resp.status_code) # 打印响应状态码(如 200 表示成功)
print(resp.text) # 打印响应内容(echo 视图返回的 JSON 字符串)# 断言:期望响应内容是 {"a": 1} 的 JSON 字符串
assert resp.text == json.dumps({"a": "1"})
为什么表单里面没有数据类型({"a":"1"}而不是{"a":1}) 都按照字符串进行处理 ?
表单是 “传递文本的载体”,类型由后端决定
表单的作用就像 “快递单”:前端只负责把用户输入的内容以 “字符串” 形式传给服务器,不关心内容的实际类型。而服务器(后端)拿到字符串后,再根据业务场景转成需要的类型(数字、布尔值、日期等)。
这种设计看似 “粗糙”,实则是 Web 开发中 “兼容性” 和 “灵活性” 的平衡 —— 既保证了所有系统都能互通,又让开发者能按需处理数据~
想解决 QueryDict
转字典后,值带列表中括号 [ ]
的问题!
比如想把 {"b": ["2"]}
变成 {"b": "2"}
,这确实是 Django 处理表单数据时的常见需求,咱们一步步讲清楚怎么实现~
先明白为什么会有中括号 [ ]
?
request.POST
是 QueryDict
类型,它的设计是 “一个键可以对应多个值”(比如表单里多个同名的复选框),所以即使只有一个值,也会存成列表形式(["2"]
)。
- 表单提交
b=2
,request.POST
实际是QueryDict({'b': ['2']})
- 直接转字典会变成
{'b': ['2']}
,JSON 序列化后就是{"b": ["2"]}
- 前端表单里的字段(比如输入框、单选框)只会传一个值(比如
b=2
不可能同时传b=2&b=3
)。 - 你明确知道 “每个键只有一个值”,列表里的其他元素不存在(或者不需要)。
这时候,取列表的第一个元素 v[0]
是安全的,比如:
# 原代码:v 是列表 ['2'],存成 data[k] = v → 结果 {'b': ['2']}
# 改后代码:取 v[0] → 结果 {'b': '2'}
for k, v in request.POST.items():data[k] = v[0] # 只取列表第一个元素,自然去掉了中括号
六、基于 Django Session 的 “访问计数” 功能
整体流程:“客户端请求 → 服务端计数 → 测试验证”
- Django 服务端:
echo
视图通过request.session
记录用户的 “访问次数”,每次访问计数 +1,再返回当前计数。 - 测试客户端:用
requests.Session()
模拟同一个客户端,多次发送 POST 请求到echo
视图,验证每次返回的计数是否符合预期(第一次1
、第二次2
、第三次3
…)。
Django 服务端:echo
视图(核心计数逻辑)
from django.http import HttpRequest, HttpResponse
import jsondef echo(request: HttpRequest):# 1. 从 Session 中获取 'num',默认值为 0(第一次访问时,Session 中无 'num')num = request.session.get("num", 0) # 2. 计数 +1(每次访问,计数自增)num = num + 1 # 3. 把新的计数存回 Session(下次访问时,能拿到更新后的值)request.session['num'] = num # 4. 转成 JSON 字符串,返回给客户端html = f"{json.dumps({'num': num})}" return HttpResponse(html)
num
在这里是一个 “计数器变量”,专门用来记录 “同一个用户访问当前视图的次数”
⭐ request.session
:用户的 “专属储物柜”
request.session
是 Django 为每个访问网站的用户(客户端)准备的 “专属储物柜”,用来存这个用户的临时数据(比如登录状态、访问次数等)。
- 每个用户的
session
是独立的(就像每个储物柜有不同的钥匙),A 用户的session
里的数据,B 用户看不到。 - 这个 “储物柜” 里的数据以 键值对 形式存储(类似字典),比如可以存
{"num": 3, "username": "张三"}
。
⭐ get("num", 0)
:安全地取数据
这是字典(或类字典对象)的常用方法,作用是:
- 尝试从
session
里找 键为num
的值(比如之前存过num=2
,就会取到2
)。 - 如果找不到这个键(比如用户第一次访问,
session
里还没有num
),就返回 默认值0
。
⭐ 访问计数
假设这是用户第一次访问网站:
request.session
里还没有num
这个键,所以request.session.get("num", 0)
会返回0
→num = 0
。
用户第二次访问时:
- 因为第一次访问后,代码里已经把
num
存回了session
(比如request.session['num'] = 1
),所以这次会取到1
→num = 1
。
以此类推,每次访问都会拿到上一次存的计数,实现 “累加” 效果。
测试客户端:requests.Session()
模拟用户请求
测试代码用 requests
库模拟客户端,重点是用 requests.Session()
保持 Session(Cookie),让服务端认为是 “同一个用户的多次请求”。
import json
import requests# 1. 创建 Session 对象(自动管理 Cookie,模拟同一个客户端)
session = requests.Session() # 2. 第一次请求:模拟客户端访问 echo 视图
resp = session.post("http://127.0.0.1:8000/beifan/echo?a=1",cookies={"d": "4"} # 可选:手动设置 Cookie(实际 Django 会自动处理 Session Cookie)
)
print(resp.status_code) # 打印状态码(预期 200)
print(resp.text) # 打印响应内容(预期 {"num": 1})
# 验证:第一次访问,计数应为 1
assert resp.text == json.dumps({"num": 1}) # 3. 第二次请求:同一个 Session,再次访问 echo 视图
resp = session.post("http://127.0.0.1:8000/beifan/echo?a=1",cookies={"d": "4"}
)
print(resp.status_code)
print(resp.text) # 预期 {"num": 2}
# 验证:第二次访问,计数应为 2
assert resp.text == json.dumps({"num": 2}) # 4. 第三次请求:继续验证计数
resp = session.post("http://127.0.0.1:8000/beifan/echo?a=1",cookies={"d": "4"}
)
print(resp.status_code)
print(resp.text) # 预期 {"num": 3}
# 验证:第三次访问,计数应为 3
assert resp.text == json.dumps({"num": 3})
客户端代码里的 session.post("http://127.0.0.1:8000/beifan/echo?a=1")
,通过 URL 路径 /beifan/echo
,借助 Django 的路由系统,精准 “命中” 了 echo
视图。
关键逻辑:requests.Session()
保持 Session
为什么用
requests.Session()
?requests.Session()
会自动保存服务器返回的 Cookie,并在后续请求中自动带上这些 Cookie。这样,服务端通过 Cookie 识别到 “同一个用户”,request.session
就能正确关联到之前的会话,计数才会持续 +1。如果不用
Session()
:
每次用requests.post()
发请求,都是 “新的客户端”,服务端会认为是不同用户,num
每次都会从0
开始(第一次请求num=1
,第二次请求又会从0
开始,导致计数错误)。
完整执行流程(以第一次、第二次请求为例)
第一次请求(客户端→服务端→客户端)
- 测试代码执行
session.post(...)
,发送第一次请求。 - 服务端
echo
视图:request.session.get("num", 0)
→ 取到0
(第一次访问,Session 中无num
)。num = 0 + 1 = 1
→ 存回session["num"] = 1
。- 返回
{"num": 1}
。
- 测试客户端收到响应,
assert
验证resp.text == '{"num": 1}'
→ 通过。
第二次请求(同一个客户端再次访问)
- 测试代码执行
session.post(...)
,Session 对象自动带上第一次请求的 Cookie。 - 服务端
echo
视图:request.session.get("num", 0)
→ 取到1
(Session 中已存num=1
)。num = 1 + 1 = 2
→ 存回session["num"] = 2
。- 返回
{"num": 2}
。
- 测试客户端收到响应,
assert
验证resp.text == '{"num": 2}'
→ 通过。
代码的核心目的
- 服务端:通过
Session
实现 “用户访问计数”,每次访问计数 +1,体现 “状态保持”(知道是同一个用户的多次请求)。 - 测试端:用
requests.Session()
模拟同一个客户端,验证每次访问的计数是否正确(第一次1
、第二次2
、第三次3
…),体现 “接口自动化测试”。