requests Session 教程:Cookie 自动保持、验证码重试与 403 排查

本文从 requests.Session 的状态保持机制出发,说明 Cookie、验证码接口、失败重试和 403 排查之间的关系,适合需要处理登录态、验证码校验或翻页采集的 Python 爬虫场景。

8 分钟阅读 CookierequestsPython爬虫Session403验证码识别模拟登录失败重试

requests.Session 适合解决什么问题

requests.Session 最适合解决一类问题:多次请求之间需要共享状态

普通的 requests.get()requests.post() 更像一次性请求。你发一次请求,拿一次响应;下一次请求默认不会自动继承上一次响应里种下的 Cookie。

但很多爬虫场景并不是“一次请求拿完数据”:

场景 为什么需要 Session
验证码图片 + 表单提交 验证码通常和 Cookie、token 或 uuid 绑定
登录后请求数据接口 登录成功后需要继续携带登录态
多页翻页采集 后续分页请求要保持同一个访问身份
反复遇到 403 可能是 Cookie 丢失、请求链路断了

所以,如果你遇到“代码看起来没错,请求参数也对,但服务端就是不认”的情况,第一件事不是继续调 OCR,也不是马上换代理,而是先确认:这些请求是不是在同一个会话里完成的。

验证码请求为什么需要保持同一个会话

验证码不是一张孤立的图片。

很多网站在返回验证码图片时,会同时给当前访问者种下一份标识。这个标识可能放在响应头里的 Set-Cookie,也可能出现在接口返回的 tokenuuidcaptcha_id 里。

服务端真正校验时,通常不是只看你提交的验证码文字,而是会一起判断:

  1. 这张验证码是不是刚才发给你的。
  2. 你提交验证码时是否带回了同一份 Cookie。
  3. token、uuid、captcha_id 是否和图片请求匹配。
  4. 验证码是否已经过期或已经被使用。
  5. 请求头、Referer、Content-Type 是否符合页面正常流程。

也就是说,OCR 只解决图片文字识别问题;服务端还会校验验证码和当前会话是否匹配。

这也是验证码接口经常需要 Session 的原因:图片请求用了一次 requests.get(),提交答案又新发了一次 requests.post(),中间没有保持 Cookie,服务端就可能把它当成另一条请求链路。

浏览器访问网站时,会自动处理 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 串起图片、提交和翻页

验证码采集流程通常可以拆成四步:

  1. 请求验证码图片。
  2. 识别验证码。
  3. 提交验证码和查询参数。
  4. 继续翻页采集数据。

这四步最容易出问题的是第一步和第三步之间的状态保持。

推荐把 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 交。

如果页面还有 uuidtokencaptcha_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-AgentRefererContent-Type
请求方式是否错误 浏览器是 GET、POST、JSON 还是表单
频率是否过高 降低并发,加间隔和重试上限

如果你不确定是哪一步错了,最有效的方法是对照浏览器抓包:

  1. 在浏览器里正常完成一次验证码提交。
  2. 找到验证码图片请求。
  3. 找到提交数据的请求。
  4. 对比两次请求之间 Cookie、参数、请求头的变化。
  5. 用代码复现同样的请求链路。

别一看到 403 就急着加代理。代理只能改变出口 IP,解决不了 Cookie 丢失、token 不匹配、请求体错误这些问题。

失败重试怎么写才不容易失控

验证码识别不是百分百准确,请求也可能因为网络波动失败,所以重试是必要的。但重试一定要有边界。

推荐原则:

  1. 每一页设置最大重试次数。
  2. 验证码失败后重新拉取新验证码。
  3. 403 不要无脑无限重试。
  4. 每次失败记录原因,方便后续排查。
  5. 加短暂等待,避免把目标服务打得太急。

示例:

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

会。只要服务端在响应里通过 Set-Cookie 返回 Cookie,同一个 requests.Session 对象后续请求会自动带上对应 Cookie。

为什么我打印 response.cookies 有值,但下一次请求还是失败?

先确认下一次请求是否使用了同一个 session。另外,验证码可能还绑定了 token、uuid、captcha_id 等参数,只带 Cookie 不一定够。

403 一定是 IP 被封了吗?

不一定。Cookie 丢失、验证码过期、请求头缺失、请求方式错误、参数不匹配,都可能导致 403。建议先对照浏览器抓包排查请求链路。

Session 能解决所有验证码问题吗?

不能。Session 解决的是状态保持问题,不负责识别验证码,也不能绕过复杂的人机校验。复杂验证码可能还会校验行为轨迹、浏览器环境、加密参数和设备特征。

同一个 Session 可以一直复用吗?

不建议无限复用。真实项目里应设置超时、失败重试上限和必要的重新登录逻辑。学习环境里也要控制请求频率,避免给服务端造成压力。

继续阅读

如果你还不熟悉网络异常和超时处理,可以先看 Python 爬虫 requests 异常处理完全指南

如果你想系统理解请求头里的 User-AgentRefererCookieContent-Type,可以继续看 HTTP 请求头与响应头实战:User-Agent、Referer、Cookie、Content-Type

Practice

读完这一节,去靶场里验证一下。

去挑战广场练习