爬虫遇到验证码,第一步不是加重试,而是读一遍返回的 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 |
滑块 + gt、challenge 参数 |
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-recaptcha 或 Just a moment;选择器写错则长度正常,只是匹配不到。
一个月的预算怎么估?
按线程数估,不按次数:测出单页平均识别耗时,算出需要的并发数,再对照套餐选线程。
国内站点的滑块验证码能走这套流程吗?
只有 GeeTest(极验)v3 可以,用 method=geetest,需要页面上的 gt 和 challenge。国内常见的网易易盾、腾讯防水墙、阿里云验证码不在支持范围内。
JavaScript 渲染的页面怎么办?
用 Selenium、Puppeteer 或 Playwright 渲染页面,从 DOM 读取 sitekey 交给 API,再把 token 写回表单提交。完整做法见 Selenium 验证码处理实战。