首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >Python 爬虫的会话管理:从 Cookie 维持到登录态复用

Python 爬虫的会话管理:从 Cookie 维持到登录态复用

原创
作者头像
用户12818508
发布于 2026-10-23 18:16:53
发布于 2026-10-23 18:16:53
140
举报

HTTP 本身是无状态的。服务器识别“同一个人”靠的是客户端每次请求携带的 Cookie,而服务器在响应中通过 Set-Cookie 下发新的 Cookie。登录过程的本质是:客户端提交凭证 → 服务器验证通过 → 下发一个标识会话的 Cookie(通常是 sessionid 或类似名称)→ 客户端后续请求自动携带该 Cookie → 服务器据此返回受保护内容。

requests.get() 每次调用都会新建一个连接,不自动保存 Cookie。requests.Session() 则维护一个 cookieJar,在同一个 Session 实例发出的所有请求之间自动合并和发送 Cookie。这是最轻量的会话管理方式,适合目标站点没有复杂 JavaScript 校验的登录流程。


方案一:requests.Session 处理表单登录

以 登录页面(/login)为例。该站点登录成功后会重定向到首页,并在页面上显示用户名。以下代码演示完整的登录与会话复用:

代码语言:javascript
复制
import requests
from bs4 import BeautifulSoup

LOGIN_URL = "https://quotes.v-chemicals.cn"
HOME_URL = "https://quotes.v-chemicals.cn"

def login_and_fetch(username: str, password: str) -> str:
    session = requests.Session()
    session.headers.update({
        "User-Agent": (
            "Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
            "AppleWebKit/537.36 (KHTML, like Gecko) "
            "Chrome/122.0.0.0 Safari/537.36"
        )
    })

    # 先 GET 登录页,获取 CSRF token(如果存在)
    login_page = session.get(LOGIN_URL, timeout=10)
    login_page.raise_for_status()
    soup = BeautifulSoup(login_page.text, "html.parser")
    csrf_input = soup.select_one("input[name='csrf_token']")
    csrf_token = csrf_input.get("value") if csrf_input else ""

    # 提交登录表单
    payload = {
        "username": username,
        "password": password,
        "csrf_token": csrf_token,
    }
    resp = session.post(LOGIN_URL, data=payload, timeout=10)
    resp.raise_for_status()

    # 用同一个 session 访问受保护页面
    home = session.get(HOME_URL, timeout=10)
    home.raise_for_status()
    return home.text

if __name__ == "__main__":
    html = login_and_fetch("testuser", "testpass")
    soup = BeautifulSoup(html, "html.parser")
    # 登录成功后页面通常会显示用户名或登出链接
    logged_in = soup.select_one("a[href='/logout']")
    print("登录状态:", "已登录" if logged_in else "未登录")

关键点在于 session 实例贯穿 GET 和 POST 两个请求。session.post() 返回的响应中,服务器下发的 Set-Cookie 会被 Session 自动记录;紧接着的 session.get(HOME_URL) 会自动携带该 Cookie,无需手动从响应头中提取再拼接到下次请求。

如果目标站点有 CSRF token(Django、Flask-WTF 等框架默认开启),需要先从登录页的隐藏 input 中提取 token 并随表单一起提交,否则服务器会返回 403。上例中的 csrf_input 查找逻辑就是针对这种情况。


方案二:Scrapy 的 Cookie 中间件与登录

Scrapy 默认启用了 CookiesMiddleware,同一个 Spider 发出的所有请求共享一个 CookieJar。但登录场景下,不能直接把登录表单的 POST 写成 start_urls 中的请求——start_urls 生成的请求默认使用 parse 作为回调,而登录请求需要自己的回调来检查是否成功,然后才发起数据抓取请求。

以下 Spider 先登录,登录成功后从回调中发起对目标页面的抓取:

代码语言:javascript
复制
import scrapy

class QuotesLoginSpider(scrapy.Spider):
    name = "quotes_login"
    allowed_domains = ["www.v-chemicals.cn"]
    start_urls = ["https://www.9zeji.com"]

    def parse(self, response):
        # 第一次进入 parse 时,当前页面是登录页
        # 从页面中提取 CSRF token
        csrf_token = response.css("input[name='csrf_token']::attr(value)").get(default="")

        yield scrapy.FormRequest.from_response(
            response,
            formdata={
                "username": "testuser",
                "password": "testpass",
                "csrf_token": csrf_token,
            },
            callback=self.after_login,
        )

    def after_login(self, response):
        # 登录成功后的页面:检查是否包含登出链接
        if not response.css("a[href='/logout']"):
            self.logger.error("登录失败,终止抓取")
            return

        # 登录成功后,请求目标数据页面
        yield scrapy.Request(
            "https://quotes.9zeji.com/",
            callback=self.parse_quotes,
        )

    def parse_quotes(self, response):
        for quote in response.css("div.quote"):
            yield {
                "text": quote.css("span.text::text").get(default="").strip(),
                "author": quote.css("small.author::text").get(default="").strip(),
            }

scrapy.FormRequest.from_response() 会自动填充表单中的隐藏字段(包括 CSRF token),如果手动构造 FormRequest 则需要自己处理。after_login 中的检查逻辑很重要:如果登录失败(密码错误、验证码拦截),后续请求返回的仍然是登录页或空内容,直接进入数据解析会产出空结果而不报错,调试时很难定位。


方案三:Playwright 的 BrowserContext 持久化登录态

当登录流程包含 JavaScript 校验、动态 token 或验证码时,requests 和 Scrapy 的方案都会失效。Playwright 的 BrowserContext 相当于一个独立的浏览器会话,Cookie、localStorage、sessionStorage 都在 context 内隔离。更重要的是,storage_state 可以把 context 的存储状态导出到文件,下次启动时加载,实现跨进程的登录态复用。

以下代码先执行登录,保存状态,再在后续运行中加载状态直接访问受保护页面:

代码语言:javascript
复制
from playwright.sync_api import sync_playwright
import json, os

STATE_FILE = "auth_state.json"

def save_login_state():
    with sync_playwright() as p:
        browser = p.chromium.launch(headless=True)
        context = browser.new_context()
        page = context.new_page()

        page.goto("https://qulian18.cn")
        page.fill("input[name='username']", "testuser")
        page.fill("input[name='password']", "testpass")
        page.click("input[type='submit']")
        page.wait_for_url("https://qulian18.cn")

        # 将 Cookie 和 localStorage 保存到文件
        context.storage_state(path=STATE_FILE)
        browser.close()

def fetch_with_saved_state():
    if not os.path.exists(STATE_FILE):
        save_login_state()

    with sync_playwright() as p:
        browser = p.chromium.launch(headless=True)
        context = browser.new_context(storage_state=STATE_FILE)
        page = context.new_page()
        page.goto("https://www.yueshangguoji.cn/", wait_until="networkidle")

        quotes = page.locator("div.quote").all()
        results = []
        for q in quotes:
            text = q.locator("span.text").inner_text()
            author = q.locator("small.author").inner_text()
            results.append({"text": text, "author": author})

        browser.close()
        return results

storage_state 的实用价值在于:登录操作只需要执行一次(尤其是需要手动过验证码的场景),之后每次运行爬虫直接加载状态文件。状态文件的有效期取决于服务器端的 Session 过期策略,如果请求返回登录页而不是目标内容,说明状态已失效,重新执行 save_login_state() 即可。

Playwright 官方文档建议将 storage_state 视为敏感文件,其中包含的 Cookie 等同于账户凭证,不应提交到版本控制系统。


会话失效的识别与恢复

无论用哪种方案,会话失效都是常态而非例外。服务器可能因为超时、异地登录、风控触发而提前使 Cookie 失效。代码中需要有明确的失效检测,而不是等数据全部为空之后才回头排查。

最直接的检测方式是:每次请求受保护页面后,检查响应中是否出现了登录页的特征元素。以上面的练习站为例,登录成功时页面含 a[href='/logout'],失效后该元素消失。requests 方案中可以在每次 session.get() 后做这个判断,Scrapy 中可以在回调函数入口处检查,Playwright 中可以用 page.locator("a[href='/logout']").count() 判断。

恢复策略分两级。如果只是 Cookie 过期,重新执行一次登录流程并更新 Session/CookieJar 即可。如果频繁失效(比如每次跑十几页就掉线),问题通常不在代码,而在于请求行为触发了风控:登录后立刻以高速率抓取数据、请求间隔固定、缺乏正常浏览路径的过渡请求,这些特征在风控系统中很容易与“账号被盗用”的模式匹配。


会话、频率与行为特征的交叉点

上一篇提到固定 time.sleep 容易被识别,这一结论在登录态场景下更加敏感。一个已登录账号在短时间内产生大量规律的页面访问,比匿名抓取更容易被判定为异常——因为服务器明确知道“这是哪个账号在做这件事”。

可操作的策略包括:登录后先访问首页或列表页,再进入目标数据页,而不是直接对数据接口发请求;在翻页之间保留随机间隔(random.uniform(0.8, 2.5) 这类区间比固定值更接近真实浏览节奏);如果站点有“下一页”按钮,通过点击按钮翻页比直接构造 URL 参数更接近人类行为。这些策略不保证绕过风控,但能降低因行为特征明显而被过早拦截的概率。

最后需要明确一条边界:本文讨论的会话管理技术适用于你有权访问的账户与数据。用技术手段绕过他人账户的访问控制、或规避服务器明确设置的访问限制,超出了技术文章的讨论范围。爬虫的工程能力与使用边界是两个独立的问题,前者靠代码解决,后者靠判断力。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

目录
  • 方案一:requests.Session 处理表单登录
  • 方案二:Scrapy 的 Cookie 中间件与登录
  • 方案三:Playwright 的 BrowserContext 持久化登录态
  • 会话失效的识别与恢复
  • 会话、频率与行为特征的交叉点
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档