Comparisons

粘性会话与轮换会话对比:验证码场景下怎么选

判据只有一句:同一流程里的请求要不要彼此认得。要就用粘性会话,不要就用轮换会话。

原因在出口 IP:token 在哪个 IP 下识别出来,站点往往就比对哪个 IP。中途换 IP,token 大概率作废;反过来,独立请求钉在同一 IP 上也更快撞限流。

工作流 模式 原因
登录、注册、带验证码的表单 粘性 token 绑定识别时的出口 IP
结算、支付、多页跳转 粘性 cookie 要跨请求延续
批量采集、搜索结果页 轮换 摊薄单 IP 密度
高频轮询、价格监控 轮换 请求彼此独立

下面两个数字来自内部观测样本,仅供参考:多步骤流程走轮换的触发率约 30–50%,改用粘性后约 5–10%,请在自有环境中测量。


站点看到的两种模式

STICKY SESSION:
  Request 1 ──▶ IP: 192.168.1.50 ──▶ site.com/login
  Request 2 ──▶ IP: 192.168.1.50 ──▶ site.com/dashboard
  Request 3 ──▶ IP: 192.168.1.50 ──▶ site.com/account
  (Same IP for all requests in the session window)

ROTATING SESSION:
  Request 1 ──▶ IP: 192.168.1.50 ──▶ site.com/page1
  Request 2 ──▶ IP: 10.0.0.77   ──▶ site.com/page2
  Request 3 ──▶ IP: 172.16.5.22 ──▶ site.com/page3
  (Different IP per request)

粘性模式下是一个连续操作的访客,轮换模式下是一群陌生访客。


必须用粘性会话:登录、下单、带验证码的表单

一次登录至少三步:加载页面、识别验证码、带 token 提交,三步要走同一出口 IP。用 requests.Session 钉住最省事。

import requests
import time

CAPTCHAAI_KEY = "YOUR_API_KEY"
CAPTCHAAI_URL = "https://ocr.captchaai.com"

# Sticky session: same IP for the entire login flow
STICKY_PROXY = {
    "http": "http://user-session-abc123:[email protected]:5000",
    "https": "http://user-session-abc123:[email protected]:5000",
}

session = requests.Session()
session.proxies = STICKY_PROXY
session.headers.update({
    "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
    "AppleWebKit/537.36 Chrome/126.0.0.0 Safari/537.36",
})


def login_with_captcha(url, sitekey, username, password):
    """Full login flow — MUST use same IP throughout."""
    # Step 1: Load login page (IP: 192.168.1.50)
    session.get(url)

    # Step 2: Solve CAPTCHA (solved against IP: 192.168.1.50)
    token = solve_recaptcha(sitekey, url)

    # Step 3: Submit login (MUST be IP: 192.168.1.50)
    resp = session.post(url, data={
        "username": username,
        "password": password,
        "g-recaptcha-response": token,
    })

    return resp.status_code == 200


def solve_recaptcha(sitekey, pageurl):
    resp = requests.post(f"{CAPTCHAAI_URL}/in.php", data={
        "key": CAPTCHAAI_KEY,
        "method": "userrecaptcha",
        "googlekey": sitekey,
        "pageurl": pageurl,
        "json": 1,
    })
    task_id = resp.json()["request"]

    for _ in range(60):
        time.sleep(5)
        result = requests.get(f"{CAPTCHAAI_URL}/res.php", params={
            "key": CAPTCHAAI_KEY, "action": "get",
            "id": task_id, "json": 1,
        })
        data = result.json()
        if data["request"] != "CAPCHA_NOT_READY":
            return data["request"]

    raise TimeoutError("CAPTCHA solve timeout")

两个踩坑点:session.proxies 要在第一个请求前设好;字段名 g-recaptcha-response 不要改写,Turnstile 用 cf-turnstile-response

token 被接受还是被拒,就差这一步

Sticky session:

  1. Browser loads page    → IP: 5.5.5.5
  2. CAPTCHA solved        → Token bound to session
  3. Form submitted        → IP: 5.5.5.5 ✅ Token accepted

Rotating session:

  1. Browser loads page    → IP: 5.5.5.5
  2. CAPTCHA solved        → Token bound to session
  3. Form submitted        → IP: 9.9.9.9 ❌ Token may be rejected

排查顺序固定下来:识别前和提交前各打印一次出口 IP,一致了再怀疑 pageurl 或 sitekey。


轮换会话更划算:批量采集与搜索结果页

请求之间不共享状态时,钉住 IP 只是负担。把请求摊到更大的 IP 池,单 IP 密度下降,触发概率随之降低。

import concurrent.futures

ROTATING_PROXY = {
    "http": "http://user:[email protected]:5000",
    "https": "http://user:[email protected]:5000",
}


def scrape_page(url):
    """Each request gets a fresh IP automatically."""
    resp = requests.get(
        url,
        proxies=ROTATING_PROXY,
        headers={"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64)"},
        timeout=30,
    )
    if resp.status_code == 200:
        return url, resp.text
    return url, None


urls = [f"https://example.com/products?page={i}" for i in range(1, 501)]

with concurrent.futures.ThreadPoolExecutor(max_workers=10) as pool:
    results = list(pool.map(scrape_page, urls))
    success = sum(1 for _, html in results if html)
    print(f"Scraped: {success}/{len(urls)}")

搜索结果页对单 IP 查询量更敏感,几十次就可能撞上阈值:

def scrape_search(query, pages=10):
    results = []
    for page in range(pages):
        # Each request → new IP
        resp = requests.get(
            "https://search-engine.example.com/search",
            params={"q": query, "start": page * 10},
            proxies=ROTATING_PROXY,
            timeout=20,
        )
        results.append(resp.text)
        time.sleep(2)
    return results

time.sleep(2) 别删:换 IP 不等于能无限提速,请求间隔本身就是特征。


混合策略:登录走粘性,列表走轮换

真实项目很少只用一种模式,用一个管理类收口:

class HybridProxyManager:
    """Sticky for multi-step flows, rotating for single requests."""

    def __init__(self, provider_host, username, password, port=5000):
        self.host = provider_host
        self.username = username
        self.password = password
        self.port = port

    def get_rotating_proxy(self):
        return {
            "http": f"http://{self.username}:{self.password}@{self.host}:{self.port}",
            "https": f"http://{self.username}:{self.password}@{self.host}:{self.port}",
        }

    def get_sticky_proxy(self, session_id, duration_min=10):
        sticky_user = f"{self.username}-session-{session_id}-ttl-{duration_min}"
        return {
            "http": f"http://{sticky_user}:{self.password}@{self.host}:{self.port}",
            "https": f"http://{sticky_user}:{self.password}@{self.host}:{self.port}",
        }


proxy_mgr = HybridProxyManager("proxy.example.com", "user", "pass")


def scrape_with_auto_strategy(url, needs_login=False):
    if needs_login:
        # Multi-step → sticky
        import uuid
        session_id = uuid.uuid4().hex[:8]
        proxy = proxy_mgr.get_sticky_proxy(session_id)
    else:
        # Single page → rotating
        proxy = proxy_mgr.get_rotating_proxy()

    return requests.get(url, proxies=proxy, timeout=30)

session ID 取 uuid4() 前 8 位就够。服务商多把会话 ID 写进用户名,如 user-session-abc123,格式见 Bright Data 对接指南Smartproxy 配置说明Oxylabs 集成实践


TTL 设多久:按流程 P95 耗时的两倍

TTL 适用场景 代价
1 分钟 单次快速表单 稍慢就中途过期
5 分钟 标准登录流程 平衡点,建议默认
10 分钟 多页面复杂流程 单 IP 请求累积
30 分钟以上 长时间连续浏览 特征明显,慎用

宁可略长也别卡上限,中途过期整个流程都要重来。服务商只是尽力维持同一 IP,代码里要留重新识别的退路。


Node.js 版本:同一份配置贯穿三步

逻辑与 Python 一致,改用 axios 的 proxy 配置项:

const axios = require("axios");

const CAPTCHAAI_KEY = "YOUR_API_KEY";
const PROXY_HOST = "proxy.example.com";
const PROXY_PORT = 5000;

function getProxy(mode, sessionId = null) {
  const user =
    mode === "sticky" ? `user-session-${sessionId}` : "user";

  return {
    proxy: {
      host: PROXY_HOST,
      port: PROXY_PORT,
      auth: { username: user, password: "pass" },
    },
  };
}

// Rotating: bulk scrape
async function scrapePages(urls) {
  const results = [];
  for (const url of urls) {
    const config = getProxy("rotating");
    const resp = await axios.get(url, { ...config, timeout: 30000 });
    results.push({ url, data: resp.data });
  }
  return results;
}

// Sticky: login flow
async function loginFlow(loginUrl, sitekey, credentials) {
  const sessionId = Date.now().toString(36);
  const config = getProxy("sticky", sessionId);

  // Step 1: Load page
  await axios.get(loginUrl, config);

  // Step 2: Solve CAPTCHA
  const submitResp = await axios.post(
    "https://ocr.captchaai.com/in.php",
    null,
    {
      params: {
        key: CAPTCHAAI_KEY,
        method: "userrecaptcha",
        googlekey: sitekey,
        pageurl: loginUrl,
        json: 1,
      },
    }
  );
  const taskId = submitResp.data.request;

  let token;
  for (let i = 0; i < 60; i++) {
    await new Promise((r) => setTimeout(r, 5000));
    const res = await axios.get("https://ocr.captchaai.com/res.php", {
      params: { key: CAPTCHAAI_KEY, action: "get", id: taskId, json: 1 },
    });
    if (res.data.request !== "CAPCHA_NOT_READY") {
      token = res.data.request;
      break;
    }
  }

  // Step 3: Submit with same IP
  return axios.post(
    loginUrl,
    {
      ...credentials,
      "g-recaptcha-response": token,
    },
    config
  );
}

关键在 loginFlow():同一份 config 既加载页面也提交表单。改做 Turnstile 时 method 换成 turnstile、字段换成 cf-turnstile-response


国内项目要多算两件事

验证码类型不一样。 国内站点多用 GeeTest(极验)、网易易盾、腾讯防水墙,reCAPTCHA 多见于出海站。国内风控同样看重会话连续性,TTL 建议从 5 分钟起测,再按实测耗时上调。CaptchaAI 支持 GeeTest v3(v4 即将支持),不支持 hCaptcha 与 FunCaptcha(Arkose Labs),CaptchaFox、Friendly Captcha、Lemin 为测试版。

测量地点影响结论。 reCAPTCHA 依赖 Google 托管脚本,国内网络下耗时波动大,本地 P95 偏高,据此推出的 TTL 会虚高,基准测量放到境外 staging 更可信。采集范围限定在自有或已授权系统,遵循 robots 协议与《网络安全法》《数据安全法》、PIPL。


出问题时先查这张表

现象 根因 处理方式
识别成功但 token 被拒 中途换了出口 IP 改用粘性,全程复用同一 session
单页采集触发率飙升 同一 IP 访问页面过多 切回轮换或缩短 TTL
轮换模式变慢 每次新 IP 重建连接 复用连接池,降低并发

CaptchaAI 按线程计费,不按次计费:BASIC($15/月,5 线程)到 VIP-3($7,500/月,5,000 线程),识别次数不限。选错模式不多付识别费,代价在吞吐——重试照样占线程。


常见问题

改成粘性会话后 token 还是被拒,怎么排查?

先确认三步是否真的走在同一条 session 上。常见错误是建了 requests.Session(),某一步却写成模块级 requests.get()。再核对 pageurl 与提交页是否一致。

怎么确认出口 IP 到底变没变?

在识别前和提交前各调一次自有的 echo 接口,把来源 IP 打进日志。两次不一致,问题就在会话模式;一致却仍被拒,再查 sitekey 和字段名。

Turnstile 的 token 也要求同一出口 IP 吗?

按同样的规则处理最省事。cf-turnstile-response 同样是一次性凭据,站点服务端可以连同请求上下文一起校验,严不严格取决于站点配置。


延伸阅读


会话模式和工作流对上号,是提升通过率最便宜的一步。领取 CaptchaAI API Key,两种模式各跑一轮。

该文章已禁用评论。