requests Session 教程:Cookie 自动保持、验证码重试与 403 排查
本文从 requests.Session 的状态保持机制出发,说明 Cookie、验证码接口、失败重试和 403 排查之间的关系,适合需要处理登录态、验证码校验或翻页采集的 Python 爬虫场景。
requests.Session 适合解决什么问题
requests.Session 最适合解决一类问题:多次请求之间需要共享状态。
普通的 requests.get()、requests.post() 更像一次性请求。你发一次请求,拿一次响应;下一次请求默认不会自动继承上一次响应里种下的 Cookie。
但很多爬虫场景并不是“一次请求拿完数据”:
| 场景 | 为什么需要 Session |
|---|---|
| 验证码图片 + 表单提交 | 验证码通常和 Cookie、token 或 uuid 绑定 |
| 登录后请求数据接口 | 登录成功后需要继续携带登录态 |
| 多页翻页采集 | 后续分页请求要保持同一个访问身份 |
| 反复遇到 403 | 可能是 Cookie 丢失、请求链路断了 |
所以,如果你遇到“代码看起来没错,请求参数也对,但服务端就是不认”的情况,第一件事不是继续调 OCR,也不是马上换代理,而是先确认:这些请求是不是在同一个会话里完成的。
验证码请求为什么需要保持同一个会话
验证码不是一张孤立的图片。
很多网站在返回验证码图片时,会同时给当前访问者种下一份标识。这个标识可能放在响应头里的 Set-Cookie,也可能出现在接口返回的 token、uuid、captcha_id 里。
服务端真正校验时,通常不是只看你提交的验证码文字,而是会一起判断:
- 这张验证码是不是刚才发给你的。
- 你提交验证码时是否带回了同一份 Cookie。
- token、uuid、captcha_id 是否和图片请求匹配。
- 验证码是否已经过期或已经被使用。
- 请求头、Referer、Content-Type 是否符合页面正常流程。
也就是说,OCR 只解决图片文字识别问题;服务端还会校验验证码和当前会话是否匹配。
这也是验证码接口经常需要 Session 的原因:图片请求用了一次 requests.get(),提交答案又新发了一次 requests.post(),中间没有保持 Cookie,服务端就可能把它当成另一条请求链路。
Cookie、Set-Cookie 和 Session 的关系
浏览器访问网站时,会自动处理 Cookie。
服务端返回:
Set-Cookie: sessionid=abc123; Path=/; HttpOnly
浏览器会把它保存下来。后续访问同一个站点时,浏览器自动带上:
Cookie: sessionid=abc123
服务端看到这份 Cookie,就能知道“这几个请求来自同一个访问会话”。
requests.Session 做的事情很像浏览器:它会在同一个 session 对象里自动保存响应里的 Cookie,并在后续请求里自动带上。
最关键的一点是:验证码图片、验证码提交、数据翻页,必须尽量使用同一个 session 对象。
requests.Session 的最小可运行示例
先看一个最小示例:
import requests
session = requests.Session()
headers = {
"User-Agent": "Mozilla/5.0",
}
response = session.get("https://example.com/captcha", headers=headers, timeout=10)
print(response.status_code)
print(session.cookies)
submit_response = session.post(
"https://example.com/submit",
data={"captcha": "1234"},
headers=headers,
timeout=10,
)
print(submit_response.status_code)
这段代码里,第一次请求验证码图片后,如果服务端通过 Set-Cookie 返回了 Cookie,session.cookies 会自动保存它。第二次提交验证码时,同一个 session 会自动把 Cookie 带回去。
注意,不要这样写:
import requests
requests.get("https://example.com/captcha")
requests.post("https://example.com/submit", data={"captcha": "1234"})
这两次请求之间没有共享状态。它们看起来是连续的,但对服务端来说,很可能不是同一个会话。
如果你想确认 Cookie 是否真的保存下来了,可以打印:
print(session.cookies.get_dict())
也可以查看某次响应里服务端有没有种 Cookie:
print(response.headers.get("Set-Cookie"))
验证码场景:同一个 Session 串起图片、提交和翻页
验证码采集流程通常可以拆成四步:
- 请求验证码图片。
- 识别验证码。
- 提交验证码和查询参数。
- 继续翻页采集数据。
这四步最容易出问题的是第一步和第三步之间的状态保持。
推荐把 session 作为参数传进去,让所有请求共用它:
import time
import requests
BASE_URL = "https://example.com"
def fetch_captcha(session):
response = session.get(f"{BASE_URL}/captcha", timeout=10)
response.raise_for_status()
return response.content
def recognize_captcha(image_bytes):
# 这里替换成你自己的 OCR 或人工输入逻辑
return "1234"
def fetch_page(session, page, captcha_text):
payload = {
"page": page,
"captcha": captcha_text,
}
response = session.post(f"{BASE_URL}/api/list", data=payload, timeout=10)
if response.status_code == 403:
return None, "forbidden"
response.raise_for_status()
return response.json(), None
def main():
session = requests.Session()
session.headers.update({
"User-Agent": "Mozilla/5.0",
"Accept": "application/json, text/plain, */*",
})
for page in range(1, 6):
captcha_image = fetch_captcha(session)
captcha_text = recognize_captcha(captcha_image)
data, error = fetch_page(session, page, captcha_text)
if error == "forbidden":
print(f"第 {page} 页被拒绝,准备重新获取验证码")
time.sleep(1)
continue
print(f"第 {page} 页数据:", data)
if __name__ == "__main__":
main()
这只是流程模板,不是让你直接照抄目标站。真实页面里,验证码字段名、接口地址、提交方式、返回结构都要以浏览器抓包为准。
关键思路只有一个:验证码图片从哪个 session 拿,验证码答案就用哪个 session 交。
如果页面还有 uuid、token、captcha_id 这类字段,也要一起保存并提交:
captcha_info = session.get(f"{BASE_URL}/api/captcha", timeout=10).json()
captcha_id = captcha_info["captcha_id"]
image_base64 = captcha_info["image"]
payload = {
"captcha_id": captcha_id,
"captcha": "1234",
"page": 1,
}
response = session.post(f"{BASE_URL}/api/list", json=payload, timeout=10)
不要只盯着验证码图片本身。很多时候,真正决定成败的是图片背后的绑定参数。
403 排查清单:不是所有失败都是验证码问题
403 Forbidden 表示服务端拒绝了这次请求。它不一定代表验证码错了,也不一定代表 IP 被封了。
可以按这个顺序排查:
| 排查项 | 怎么看 |
|---|---|
| Cookie 是否丢失 | 打印 session.cookies.get_dict() |
| 是否使用同一个 session | 图片、提交、翻页不能混用不同 session |
| 验证码是否过期 | 重新请求图片后立刻提交 |
| token/uuid 是否缺失 | 对照浏览器请求参数 |
| 请求头是否差异过大 | 对比 User-Agent、Referer、Content-Type |
| 请求方式是否错误 | 浏览器是 GET、POST、JSON 还是表单 |
| 频率是否过高 | 降低并发,加间隔和重试上限 |
如果你不确定是哪一步错了,最有效的方法是对照浏览器抓包:
- 在浏览器里正常完成一次验证码提交。
- 找到验证码图片请求。
- 找到提交数据的请求。
- 对比两次请求之间 Cookie、参数、请求头的变化。
- 用代码复现同样的请求链路。
别一看到 403 就急着加代理。代理只能改变出口 IP,解决不了 Cookie 丢失、token 不匹配、请求体错误这些问题。
失败重试怎么写才不容易失控
验证码识别不是百分百准确,请求也可能因为网络波动失败,所以重试是必要的。但重试一定要有边界。
推荐原则:
- 每一页设置最大重试次数。
- 验证码失败后重新拉取新验证码。
- 403 不要无脑无限重试。
- 每次失败记录原因,方便后续排查。
- 加短暂等待,避免把目标服务打得太急。
示例:
import time
def fetch_page_with_retry(session, page, max_retries=3):
for attempt in range(1, max_retries + 1):
captcha_image = fetch_captcha(session)
captcha_text = recognize_captcha(captcha_image)
data, error = fetch_page(session, page, captcha_text)
if data is not None:
return data
print(f"第 {page} 页第 {attempt} 次失败: {error}")
time.sleep(1)
return None
这个模板的重点不是代码多复杂,而是把失败控制在可预期范围内。
如果连续多次 403,就应该停下来排查链路,而不是继续高速重试。尤其是学习和测试环境之外,不要对第三方站点做高频请求。
Session 和模拟登录有什么关系
模拟登录本质上也是“保持登录态”。
常见登录态有两类:
| 登录方式 | 后续请求怎么证明身份 |
|---|---|
| Cookie 登录态 | 服务器通过 Set-Cookie 种下 sessionid,后续请求自动带 Cookie |
| JWT / Token 登录态 | 登录接口返回 token,后续请求放到 Authorization 请求头 |
requests.Session 对 Cookie 登录态特别方便,因为它会自动保存和携带 Cookie。
例如:
import requests
session = requests.Session()
login_response = session.post(
"https://example.com/login",
data={
"username": "demo",
"password": "demo",
},
timeout=10,
)
login_response.raise_for_status()
data_response = session.get("https://example.com/api/user/data", timeout=10)
print(data_response.text)
如果网站使用的是 JWT,重点就不在 Cookie,而在请求头:
token = "登录接口返回的 access_token"
session.headers.update({
"Authorization": f"Bearer {token}",
})
Cookie 和 JWT 的实现方式不同,但思路一样:先拿到身份凭证,再让后续请求持续带着它。
如果页面登录过程很复杂,比如有验证码、加密参数、浏览器环境校验,也可以用 Playwright 先完成登录,再把 Cookie 交给 requests.Session 继续请求数据接口。这个思路适合放到模拟登录专题里单独讲。
常见问题 FAQ
requests.Session 会自动带 Cookie 吗?
会。只要服务端在响应里通过 Set-Cookie 返回 Cookie,同一个 requests.Session 对象后续请求会自动带上对应 Cookie。
为什么我打印 response.cookies 有值,但下一次请求还是失败?
先确认下一次请求是否使用了同一个 session。另外,验证码可能还绑定了 token、uuid、captcha_id 等参数,只带 Cookie 不一定够。
403 一定是 IP 被封了吗?
不一定。Cookie 丢失、验证码过期、请求头缺失、请求方式错误、参数不匹配,都可能导致 403。建议先对照浏览器抓包排查请求链路。
Session 能解决所有验证码问题吗?
不能。Session 解决的是状态保持问题,不负责识别验证码,也不能绕过复杂的人机校验。复杂验证码可能还会校验行为轨迹、浏览器环境、加密参数和设备特征。
同一个 Session 可以一直复用吗?
不建议无限复用。真实项目里应设置超时、失败重试上限和必要的重新登录逻辑。学习环境里也要控制请求频率,避免给服务端造成压力。
继续阅读
如果你还不熟悉网络异常和超时处理,可以先看 Python 爬虫 requests 异常处理完全指南。
如果你想系统理解请求头里的 User-Agent、Referer、Cookie 和 Content-Type,可以继续看 HTTP 请求头与响应头实战:User-Agent、Referer、Cookie、Content-Type。
Practice