Use Cases

抓取受验证码保护的网站

爬虫遇到验证码,第一步不是加重试,而是读一遍返回的 HTML。 拦截页里的特征字符串直接告诉你这是哪一类验证码;类型定了,接入方式和成本也就定了。

安全范围: 本指南仅适用于你自有或经授权的 QA、staging 与预发布环境。内容覆盖针对你自己 CAPTCHA 集成的诊断、测试与可观测性模式 — 不涉及第三方站点或未授权流程。

先划边界:哪些数据可以采

网络安全法、数据安全法、个人信息保护法和目标站点的 robots 协议,决定了什么数据可以采、能不能留存。本文默认你对目标环境有访问权。

用 HTML 特征判定验证码类型

直接在响应正文里 grep 特征串,每个特征对应一个 method

页面特征 类型 CaptchaAI method
g-recaptcha + data-sitekey reCAPTCHA v2 method=userrecaptcha
grecaptcha.execute reCAPTCHA v3 method=userrecaptcha&version=v3
cf-turnstile + data-sitekey Cloudflare Turnstile method=turnstile
整页 Just a moment 拦截 Cloudflare Challenge method=cloudflare_challenge
<img> 验证码图 + 输入框 图片/OCR 验证码 method=base64
滑块 + gtchallenge 参数 GeeTest v3 method=geetest

两条前提:CaptchaAI 不支持 hCaptcha 和 FunCaptcha(Arkose Labs),页面出现 h-captcha 时这条链路走不通;GeeTest v4 属于即将支持,能走接口的是 v3。CaptchaFox、Friendly Captcha、Lemin 为测试版支持。

接下来只剩一道选择题:在哪一步调识别接口。判断依据是验证码出现的概率——先统计触发率,再选下面三种策略之一。

策略一:按需检测,触发时才调接口

正常发请求,命中特征串才走识别:_handle_captcha() 提取 sitekey 分发,_poll() 每 5 秒查结果。

import requests
import time
from bs4 import BeautifulSoup

API_KEY = "YOUR_API_KEY"

class ProtectedScraper:
    def __init__(self):
        self.session = requests.Session()
        self.session.headers.update({
            "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36"
        })

    def scrape(self, url):
        resp = self.session.get(url)

        # Check for CAPTCHA
        if self._has_captcha(resp.text):
            resp = self._handle_captcha(resp.text, url)

        return resp.text

    def _has_captcha(self, html):
        indicators = ["g-recaptcha", "cf-turnstile", "h-captcha", "captcha"]
        return any(ind in html.lower() for ind in indicators)

    def _handle_captcha(self, html, url):
        soup = BeautifulSoup(html, "html.parser")

        # reCAPTCHA v2
        rc = soup.find("div", class_="g-recaptcha")
        if rc:
            token = self._solve_recaptcha(rc["data-sitekey"], url)
            return self.session.post(url, data={"g-recaptcha-response": token})

        # Cloudflare Turnstile
        ts = soup.find("div", class_="cf-turnstile")
        if ts:
            token = self._solve_turnstile(ts["data-sitekey"], url)
            return self.session.post(url, data={"cf-turnstile-response": token})

        raise Exception("Unknown CAPTCHA type")

    def _solve_recaptcha(self, site_key, page_url):
        resp = requests.get("https://ocr.captchaai.com/in.php", params={
            "key": API_KEY, "method": "userrecaptcha",
            "googlekey": site_key, "pageurl": page_url
        })
        return self._poll(resp.text.split("|")[1])

    def _solve_turnstile(self, site_key, page_url):
        resp = requests.get("https://ocr.captchaai.com/in.php", params={
            "key": API_KEY, "method": "turnstile",
            "sitekey": site_key, "pageurl": page_url
        })
        return self._poll(resp.text.split("|")[1])

    def _poll(self, task_id):
        for _ in range(60):
            time.sleep(5)
            result = requests.get("https://ocr.captchaai.com/res.php", params={
                "key": API_KEY, "action": "get", "id": task_id
            })
            if result.text == "CAPCHA_NOT_READY": continue
            if result.text.startswith("OK|"): return result.text.split("|")[1]
            raise Exception(result.text)
        raise TimeoutError()

# Usage
scraper = ProtectedScraper()
html = scraper.scrape("https://example.com/data")

最容易漏掉的是全程复用同一个 requests.Session():cookie 留在会话里,提交 token 的 POST 才算同一人。

策略二:对必现验证码的页面提前取 token

带查询条件的搜索页往往每次都弹验证码,再探测一遍是多余往返:直接用已知 sitekey 换 token,带表单提交。

def scrape_known_captcha_page(url, site_key):
    # Solve before even loading the page
    token = solve_recaptcha(site_key, url)

    # Submit directly with token
    resp = requests.post(url, data={
        "g-recaptcha-response": token,
        "query": "search term"
    })
    return resp.text

前提是 sitekey 稳定:站点改版后仍用缓存值会连续失败,建议每天探测一次核对。

策略三:整站被 Cloudflare 拦在门外

连 HTML 都取不到、只返回整页拦截,说明拦住你的是站点级验证流程。提交 cloudflare_challenge,返回 qa_session_cookie 和配套 UA。

def get_cloudflare_clearance(url, proxy):
    resp = requests.get("https://ocr.captchaai.com/in.php", params={
        "key": API_KEY,
        "method": "cloudflare_challenge",
        "pageurl": url,
        "proxy": proxy,
        "proxytype": "HTTP"
    })
    task_id = resp.text.split("|")[1]

    for _ in range(60):
        time.sleep(5)
        result = requests.get("https://ocr.captchaai.com/res.php", params={
            "key": API_KEY, "action": "get", "id": task_id
        })
        if result.text == "CAPCHA_NOT_READY": continue
        if "qa_session_cookie" in result.text:
            # Parse qa_session_cookie and user_agent from response
            return result.text
    raise TimeoutError()

硬性要求:后续请求必须使用与识别时相同的出口 IP 和 UA,变一项即失效——只适合出口固定的自有服务器基础设施。

翻页采集:节奏控制与失败隔离

规模化后的问题是节奏:单页失败只跳过记日志,不中断整轮;每页随机等待 2–5 秒。

def scrape_multiple_pages(base_url, site_key, pages):
    scraper = ProtectedScraper()
    results = []

    for page in pages:
        url = f"{base_url}?page={page}"
        try:
            html = scraper.scrape(url)
            soup = BeautifulSoup(html, "html.parser")
            items = soup.find_all("div", class_="item")
            results.extend([item.text.strip() for item in items])
            print(f"Page {page}: {len(items)} items")
        except Exception as e:
            print(f"Page {page} failed: {e}")

        time.sleep(random.uniform(2, 5))

    return results

并发前先算成本:CaptchaAI 按线程计费,不按次数。BASIC $15/月含 5 个线程,STANDARD $30/月含 15 个,ADVANCE $90/月含 50 个。协程数超过线程数不会更快,多出来的只在排队。

高频故障与排查方向

问题 处理方式
每一页都弹验证码 请求过密,降低频率
token 提交后被拒绝 token 有时效,120 秒内用掉
凭据有效但仍被拦 出口 IP 或 UA 与识别时不一致
识别成功但页面不对 有重定向,或表单缺隐藏字段
一直返回 CAPCHA_NOT_READY 正常等待,超 60 次判超时重提

还有一个只在国内会踩的坑:reCAPTCHA 前端脚本托管在 Google 域名下,内地访问不稳定,本地复现时可能加载不出组件而误判成被拦截。走 API 不依赖前端脚本。

常见问题

怎么确认是被验证码拦了,而不是选择器写错?

看响应体长度和内容:被拦截时页面异常短,且含 g-recaptchaJust a moment;选择器写错则长度正常,只是匹配不到。

一个月的预算怎么估?

按线程数估,不按次数:测出单页平均识别耗时,算出需要的并发数,再对照套餐选线程。

国内站点的滑块验证码能走这套流程吗?

只有 GeeTest(极验)v3 可以,用 method=geetest,需要页面上的 gtchallenge。国内常见的网易易盾、腾讯防水墙、阿里云验证码不在支持范围内。

JavaScript 渲染的页面怎么办?

用 Selenium、Puppeteer 或 Playwright 渲染页面,从 DOM 读取 sitekey 交给 API,再把 token 写回表单提交。完整做法见 Selenium 验证码处理实战

相关阅读

该文章已禁用评论。