Explainers

轮换自有服务器基础设施:验证码解决的最佳实践

先说结论:IP 换得越勤不一定越好。 从页面加载到 token 提交必须是同一个出口 IP,否则 token 多半被判无效——排查半天的那个 403,根源常在这里。真正要调的只有两件事:何时换 IP,以及换的粒度。


先选策略:三种轮换粒度怎么挑

判断只有一条:流程里是否存在"上一页拿到的东西要在下一页用"? 有就会话保持,没有按请求轮换;多站则按域固定。

1. 按请求轮换(默认行为)

import requests

proxies = {
    "http": "http://user:[email protected]:7777",
    "https": "http://user:[email protected]:7777",
}

# Each request gets a new IP
for url in urls:
    resp = requests.get(url, proxies=proxies, timeout=30)
    # New IP after each request

适合: 跨站点抓大量页面。风险: 无法维持会话。

2. 会话保持轮换

import random
import string

def get_sticky_session(duration_min=10):
    session_id = "".join(random.choices(string.ascii_lowercase, k=8))
    return {
        "http": f"http://user-session-{session_id}:[email protected]:7777",
        "https": f"http://user-session-{session_id}:[email protected]:7777",
    }

# Same IP for the whole CAPTCHA workflow
session_proxy = get_sticky_session(duration_min=10)

# Step 1: Load page
resp = requests.get("https://staging.example.com/qa-form", proxies=session_proxy)

# Step 2: Solve CAPTCHA (same IP)
token = solve_captcha("https://staging.example.com/qa-form", sitekey)

# Step 3: Submit (same IP — required for token validity)
resp = requests.post("https://target.com/submit", proxies=session_proxy, data={
    "g-recaptcha-response": token,
})

适合: 带验证码的表单和多步流程。风险: 很低。

注意第 2、3 步:识别期间不能换 IP。服务端会比对签发与提交的来源,中途换出口等于作废。

3. 按域固定

domain_sessions = {}

def get_domain_proxy(domain):
    """Same IP per domain, different IP per domain."""
    if domain not in domain_sessions:
        session_id = "".join(random.choices(string.ascii_lowercase, k=8))
        domain_sessions[domain] = get_sticky_session()
    return domain_sessions[domain]

# site-a.com always uses the same IP
proxy_a = get_domain_proxy("site-a.com")
# site-b.com gets a different IP
proxy_b = get_domain_proxy("site-b.com")

IP 来源为什么影响验证码频率

出口类型 IP 归属 验证码出现率
数据中心 云服务商 / 主机商 高(30–70%)
家庭宽带 真实家庭 ISP 低(5–15%)
蜂窝网络 移动运营商 很低(1–5%)

以上区间基于公开资料与内部观测样本,仅供参考,请在自有环境测量。

原因在于 reCAPTCHA 会查 IP 信誉库:真实 ISP 地址有正常历史流量,信任度更高;机房段被大量自动化用过,命中率高。


降低验证码频率的五个做法

1. 出口地区和目标站点对得上

# Target site is US-based? Use US residential IP
us_proxy = "http://user-country-us:[email protected]:7777"

# Target is Germany? Use DE residential IP
de_proxy = "http://user-country-de:[email protected]:7777"

地区错配最容易被忽略:用海外出口访问只服务本地用户的站点,评分会直接掉。

2. 请求节奏要像人

import time
import random

def human_pace_requests(urls, proxy):
    results = []
    for url in urls:
        resp = requests.get(url, proxies=proxy, timeout=30)
        results.append(resp)

        # Random delay between requests
        delay = random.uniform(2, 8)
        time.sleep(delay)

    return results

3. 请求头要完整

headers = {
    "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
                  "AppleWebKit/537.36 (KHTML, like Gecko) "
                  "Chrome/126.0.0.0 Safari/537.36",
    "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
    "Accept-Language": "en-US,en;q=0.9",
    "Accept-Encoding": "gzip, deflate, br",
    "DNT": "1",
    "Connection": "keep-alive",
    "Upgrade-Insecure-Requests": "1",
}

resp = requests.get(url, proxies=proxy, headers=headers)

Accept-Language 要和出口地区一致——北美出口却发 zh-CN 优先很显眼。

4. 处理封禁要有退避

def fetch_with_retry(url, max_retries=3):
    for attempt in range(max_retries):
        # New proxy for each retry
        proxy = get_sticky_session()

        try:
            resp = requests.get(url, proxies=proxy, timeout=30)

            if resp.status_code == 403:
                print(f"IP banned, rotating... (attempt {attempt + 1})")
                time.sleep(5)
                continue

            if resp.status_code == 429:
                print(f"Rate limited, backing off...")
                time.sleep(30)
                continue

            return resp

        except requests.exceptions.ProxyError:
            print(f"Proxy failed, rotating...")
            continue

    raise Exception(f"Failed after {max_retries} retries")

403 换 IP、429 退避:429 是限流信号,换 IP 重来只会让整个池子被限。

5. 新 IP 先预热

def warm_up_proxy(proxy):
    """Visit neutral sites before target to build session trust."""
    warm_urls = [
        "https://www.google.com",
        "https://www.wikipedia.org",
    ]
    for url in warm_urls:
        try:
            requests.get(url, proxies=proxy, timeout=15)
            time.sleep(random.uniform(2, 5))
        except Exception:
            pass

和 CaptchaAI 接口衔接

IP 策略降低的是验证码出现次数,识别是另一回事。链路:抓页面 → 提取 sitekey → 提交任务 → 轮询 res.php → 同一会话提交 token。

import requests
import time
import re
import random
import string

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


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

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

    raise TimeoutError("Timeout")


def scrape_url(url, proxy_config):
    """Complete scrape with proxy + CAPTCHA solving."""
    session_id = "".join(random.choices(string.ascii_lowercase, k=8))
    proxy = {
        "http": f"http://user-session-{session_id}:{proxy_config['pass']}@{proxy_config['host']}",
        "https": f"http://user-session-{session_id}:{proxy_config['pass']}@{proxy_config['host']}",
    }

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

    match = re.search(r'data-sitekey="([^"]+)"', resp.text)
    if match:
        token = solve_recaptcha(url, match.group(1))
        resp = requests.post(
            url.replace("/form", "/submit"),
            proxies=proxy,
            data={"g-recaptcha-response": token},
        )

    return resp.text

CaptchaAI 按并发线程计价而非按次:BASIC($15/月,5 线程)、STANDARD($30/月,15 线程)、ADVANCE($90/月,50 线程),每线程识别次数不限,所以要估的是并发峰值。


一个实际的取舍场景

跨境比价采集是个典型例子:国内源多为 极验(GeeTest)滑块或图片验证码,海外源多为 reCAPTCHA 与 Cloudflare Turnstile。CaptchaAI 支持 GeeTest v3 与图片 / 九宫格,v4 只能说"即将支持"。只有登录态和表单页需要会话保持。另外 reCAPTCHA 依赖 Google 域名下的脚本,国内加载不稳定,调试时容易误判成"识别失败";采集范围请对照 网络安全法 与 robots 协议。


成本与排障速查

成本上最有效的三件事:只对验证码密集的站点用高信誉出口(可省 50–70%)、先抓完无验证码页面、淘汰差池子。

现象 原因 处理方式
每页都弹验证码 出口池质量差 换服务商或切换地区
token 被拒绝 识别与提交之间换了 IP 改用会话保持
407 错误 认证格式写错 核对用户名格式
中途会话失效 TTL 太短 申请更长的会话时长

常见问题

会话保持时长设多久合适?

覆盖完整流程再留一倍余量,表单类通常 5–10 分钟够用。TTL 过长反而容易被限流。

抓国内站点这套策略还适用吗?

思路一致,但类型不同。国内常见 极验(GeeTest)、网易易盾、腾讯防水墙;CaptchaAI 覆盖 GeeTest v3 与图片 / 九宫格。

线程数怎么估?

按峰值并发算,不按日总量:同时 10 个验证码在识别中就需要 10 个线程。BASIC($15/月)5 线程,STANDARD($30/月)15 线程。


相关阅读


先把 IP 策略调对,剩下的交给接口——获取 CaptchaAI API Key,几分钟跑通第一个识别任务。

该文章已禁用评论。