第17课:网页爬虫|验证码对抗【当反爬亮出“最后一道防线”】

文章目录
学习目标
学完这一课,你将能够:
- 理解验证码的反爬原理——知道网站为什么要用验证码、它检测的是什么维度的“人机差异”
- 区分主流验证码类型——图形验证码、滑动验证码、点选验证码、无感验证码(reCAPTCHA v3/Turnstile),知道每种的大致成本和破解难度
- 掌握两种低成本绕过路线——本地OCR(
ddddocr+pytesseract)和第三方打码平台集成(超级鹰等) - 编写完整的识别-注入代码——将验证码图片从网页中提取、发送识别、结果回填到表单的完整闭环
- 建立重试与容错机制——识别失败时自动刷新验证码、更换代理、限制重试次数,防止程序假死
- 清晰认知合规边界——理解绕过技术措施的法律红线,知晓哪些验证码绝对不能碰,哪些场景风险极高
- 掌握商业级方案选型——知道大型项目的验证码处理架构(队列、负载均衡、多平台灾备)和成本预估
验证码是对抗反爬的最后一道关卡。这一课我们将学会用最低成本的方式,穿透这道防线。
本课特别提醒:验证码绕过存在明确的法律风险(详见第九章)。本文所有技术仅供学习研究,请勿用于侵犯他人合法权益的场合。你应当只在以下范围内使用本课技能:① 爬取自己拥有完全控制权的网站或服务器;② 获得对方书面授权的正规商业合作;③ 有明确法律依据的公开数据采集。
一、通俗原理:验证码是“人机识别的最后一道闸门”
1.1 为什么会有验证码?
你还记得第15课讲的反爬漏斗吗?验证码通常位于漏斗的最深层——前面几层(UA校验、Referer校验、频率限制、IP封禁)都没能拦住你的时候,网站才会祭出最终的“杀手锏”。
验证码的核心理念很简单:设计一个对人类来说相对容易、但对机器来说极其困难的任务。比如让人类识别扭曲的字母、找出图片中的红绿灯、把滑块拖到缺口处。
但是随着AI技术的进步,这个“对人类容易、对机器困难”的假设正在崩塌。事实上验证码的攻防已经进入了一个持续的拉锯战状态。
1.2 验证码的三大技术本质
从技术实现的角度,验证码本质上是在三个维度上构建“人机差异”:
| 维度 | 检测内容 | 真人特征 | 机器劣势 |
|---|---|---|---|
| 视觉识别 | 能否理解被扭曲/遮挡/干扰的视觉信息 | 人脑高度发达的模式识别能力 | 传统OCR对干扰线、粘连字符鲁棒性差 |
| 行为模式 | 操作是否有自然的时间分布和运动轨迹 | 随机停顿、中间抖动、非匀速移动 | 程序往往是匀速直线、无停顿、无误差 |
| 环境指纹 | 浏览器/设备是否具备真实人类的完整特征 | Canvas指纹、WebGL指纹、字体列表等 | 无头浏览器或自动化工具的特征暴露 |
1.3 主流验证码类型一览
我们按“机器破解成本”从低到高排序:
| 类型 | 代表作品 | 破解成本 | 绕过难度 |
|---|---|---|---|
| 图形验证码(扭曲字符) | 传统4-6位数字/字母 | 极低(ddddocr 5行代码) |
⭐ |
| 数学/问答验证码 | “3+4=?” | 极低(正则匹配) | ⭐ |
| 基础滑动拼图 | 拖动拼图至缺口 | 中等(模板匹配+轨迹模拟) | ⭐⭐ |
| 点选验证码 | “点击所有红绿灯” | 较高(物体检测模型) | ⭐⭐⭐ |
| 高级滑动验证码 | 极验3/4、腾讯防水墙 | 高(CNN+行为模拟) | ⭐⭐⭐⭐ |
| 无感验证码 | Google reCAPTCHA v3、Cloudflare Turnstile | 极高(多重指纹分析) | ⭐⭐⭐⭐⭐ |
重要结论:如果网站用的是高级滑动验证码(极验、腾讯)或无感验证码(reCAPTCHA v3、Cloudflare),本课的低成本方案基本无效,需要更专业的手段(逆向、高级模型训练、商业服务),这些内容超出本课范围。
1.4 验证码识别本质上解决一个什么问题?
对爬虫来说,验证码带来的挑战不是“能不能识别”,而是“识别成本”的问题。核心难点在于:
- 验证码图片的位置:你需要先定位网页中验证码图片的URL或元素,然后下载它(下载时可能涉及前置Cookie、动态时间戳等,直接访问图片URL可能返回空白图)
- 识别精度与时效平衡:识别过程不能太长(验证码会过期)
- 结果回填与验证:识别出的文字/坐标需要插入正确的输入框或触发正确的点击事件,有时候还需要连带提交验证码ID
本课将从最简单的图形验证码入手,手把手教你搭建一个完整的识别闭环。
二、方案一:本地OCR识别(低成本、零网络依赖)
2.1 工具选型对比
| 工具 | 优势 | 劣势 | 适用场景 |
|---|---|---|---|
| ddddocr | 专为验证码训练,对扭曲、干扰线识别率高,纯Python(无需系统级安装),基础版仅20MB | 不支持中文(有扩展版),模型固定无法调优 | 新手首选,绝大多数英文数字验证码 |
| Tesseract | 开源免费,支持100+语言 | 需要系统级安装,中文识别率一般,需大量预处理 | 印刷体验证码 |
| EasyOCR | 开箱即用,支持80+语言,深度学习模型 | 模型体积大(~2GB),推理速度慢 | 多语言验证码、快速原型 |
| PaddleOCR | 中文优化,超高精度 | 依赖PaddlePaddle生态,部署复杂 | 中文验证码、高精度需求 |
对于90%以上的英文数字混合验证码(4-6位字符、单纯扭曲、彩色背景、有干扰线),ddddocr 是最佳选择。
2.2 ddddocr 安装与基础使用
一条命令直接搞定安装:
pip install ddddocr
验证安装并测试最简单的识别:
import ddddocr
ocr = ddddocr.DdddOcr()
# 读取验证码图片(二进制模式)
with open('captcha.png', 'rb') as f:
img_bytes = f.read()
result = ocr.classification(img_bytes)
print(f"识别结果: {result}") # 输出类似:"8h7k"
就这么简单,classification 方法直接输出识别出的字符串。
ddddocr 支持三种常见验证码类型:
| 方法 | 用途 | 返回格式 |
|---|---|---|
classification(img_bytes) |
图片文字识别 | 字符串,如 "8h7k" |
slide_match(bg_bytes, slider_bytes) |
滑块验证码缺口定位 | {"target": [x, y], ...} |
click_match(img_bytes, target) |
点选验证码物体定位 | [[x1,y1], [x2,y2], ...] |
在常见的扭曲、含干扰线条件下,ddddocr 的基础模式准确率可稳定超过85%。如果你直接调用 classification 不够精准,可以启用检测模式来大幅提升抗干扰能力:
# 启用检测模式,语义自动帮你提高抗背景干扰的准确率
ocr = ddddocr.DdddOcr(det=True)
result = ocr.classification(img_bytes, det=True)
print(f"增强识别结果: {result}")
实测数据显示,在包含5条干扰线的验证码中,启用检测模式后准确率从72%提升至89%。
2.3 滑块验证码的缺口定位(ddddocr 内置)
对于简单的滑块验证码,ddddocr 的 slide_match 可以直接返回缺口位置:
def get_slider_position(bg_img_path, slider_img_path):
ocr = ddddocr.DdddOcr()
with open(bg_img_path, 'rb') as f:
bg_img = f.read()
with open(slider_img_path, 'rb') as f:
slider_img = f.read()
result = ocr.slide_match(bg_img, slider_img)
# 返回缺口左上角的x坐标(有时返回完整target坐标)
target_x = result.get('target', [0])[0]
return target_x
# 示例
x = get_slider_position('background.png', 'slider.png')
print(f"缺口X坐标: {x}")
这一步返回的是缺口的左上角x坐标,可以配合 Selenium 计算出实际需要移动的距离。
特别提醒:2025年后的主流滑动验证码通常叠加了轨迹特征校验和行为上下文校验,单纯靠 slide_match 获取位置之后用鼠标拖到底是不够的,必须模拟真人滑动轨迹(参见第六章“哪些验证码无法低成本绕过”一节)。
2.4 图像预处理增强识别率(OpenCV辅助)
当 ddddocr 直接识别率不够高时,可以用 OpenCV 给验证码图片先进行“降噪”处理:
import cv2
import ddddocr
def preprocess_and_recognize(image_path):
# 读取图片并转为灰度图
img = cv2.imread(image_path, cv2.IMREAD_GRAYSCALE)
# 二值化:把图片转换成纯黑白的字符和背景
_, binary = cv2.threshold(img, 127, 255, cv2.THRESH_BINARY_INV)
# 降噪:去除孤立的噪声点
kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (2, 2))
denoised = cv2.morphologyEx(binary, cv2.MORPH_CLOSE, kernel)
# 将处理后的图像保存为临时文件(ddddocr读取字节流)
temp_path = 'processed.png'
cv2.imwrite(temp_path, denoised)
# 用ddddocr识别
ocr = ddddocr.DdddOcr()
with open(temp_path, 'rb') as f:
result = ocr.classification(f.read())
return result
code = preprocess_and_recognize('noisy_captcha.png')
print(f"预处理后识别结果: {code}")
2.5 实战:从网页中提取验证码图片(Selenium版)
将 Selenium 截图 + ddddocr 识别串联成完整流程:
import time
import base64
import ddddocr
from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
def solve_captcha_from_page(driver, captcha_img_selector, input_selector, submit_selector, retry_times=3):
"""
从页面中提取验证码 -> 识别 -> 填入 -> 提交
captcha_img_selector: 验证码图片的CSS选择器
input_selector: 验证码输入框的CSS选择器
submit_selector: 提交按钮的CSS选择器
"""
ocr = ddddocr.DdddOcr()
for attempt in range(retry_times):
try:
# 等待验证码图片加载完成
img_element = WebDriverWait(driver, 10).until(
EC.presence_of_element_located((By.CSS_SELECTOR, captcha_img_selector))
)
# 方案1: 截取img元素(推荐,最精准)
# 如果想要更高精准度,也可以直接保存图片URL,但selenium截图更稳定
img_path = f'captcha_{attempt}.png'
img_element.screenshot(img_path) # 截取元素截图
# 方案2: 如果图片是动态src,可以使用requests直接取,但要保持同一个sessionid
# 我们用更稳妥的截屏方式
# 识别验证码
with open(img_path, 'rb') as f:
result = ocr.classification(f.read())
print(f"第{attempt+1}次识别结果: {result}")
# 填入验证码输入框
input_box = driver.find_element(By.CSS_SELECTOR, input_selector)
input_box.clear()
input_box.send_keys(result)
# 点击提交按钮
driver.find_element(By.CSS_SELECTOR, submit_selector).click()
# 等待页面响应,检查是否验证码错误(例如没有错误信息,或者页面跳转)
time.sleep(2)
# 注意:这里需要根据具体网站判断验证是否成功,如果页面中还有“验证码错误”提示,则继续循环
if "验证码错误" not in driver.page_source:
print("验证码识别成功,登录完成")
return True
else:
print("验证码错误,刷新重试")
# 刷新验证码图片(如有刷新按钮,或者重新请求图片URL)
refresh_btn = driver.find_elements(By.CSS_SELECTOR, "img[onclick*='refresh']")
if refresh_btn:
refresh_btn[0].click()
time.sleep(1)
continue
except Exception as e:
print(f"验证码处理失败: {e}")
continue
return False
# 使用示例(具体选择器请根据实际网站调整)
# driver = webdriver.Chrome()
# driver.get("https://example.com/login")
# solve_captcha_from_page(driver, "#captcha_img", "#captcha_input", "#submit_btn")
这个脚本已经实现了“截图→识别→填回→提交→错误重试”的完整业务闭环。要注意的是,img_element.screenshot() 是将指定 <img> 元素区域截图保存为 captcha_0.png,之后 ddddocr 直接从本地截图中读取字节流并识别——以图片二进制流形式传递给打码平台是最稳妥的方式,也是打码平台最推荐的格式。
三、方案二:第三方打码平台(低成本、高成功率)
3.1 什么时候应该用打码平台?
- 本地OCR无法识别(中文字符、极复杂背景、粘连严重)
- 滑块验证码、点选验证码
- 需要高成功率(商业项目)
- 识别速度要求高于开发时间(打码平台通常比本地模型更快训练部署)
3.2 打码平台的选型与成本
| 平台 | 图形验证码价格 | 滑块/点选价格 | 特点 |
|---|---|---|---|
| 超级鹰 | ≈0.5分/次 | ≈2分/次 | 国内主流、接口简单、API稳定覆盖 |
| 云打码 | ≈1分/次 | 定制 | 老牌平台 |
| 打码兔 | ≈1分/次 | 定制 | |
| CapMonster | ≈0.3-1美元/千次 | 按次计费 | 国际平台,AI识别 |
常见费用参考:
- 4位纯数字验证码约 0.3-0.8分人民币/次
- 4-6位英文数字混合约 0.5-1分/次
- 滑块/点选验证码约 1-3分/次
- 价格会随市场波动,请以各平台最新公告为准
参考对比:如果每月识别一万次,图形验证码的支出大约在 100 元(约0.01元/次)。对于中小项目成本可控。
3.3 超级鹰API接入(完整代码)
本节以国内最常用的 超级鹰(ChaoJiYing) 打码平台为例。
"""
超级鹰验证码识别模块
文档:http://www.chaojiying.com/api-14.html
"""
import requests
import hashlib
import base64
import time
class ChaoJiYingClient:
def __init__(self, username, password, soft_id):
self.username = username
self.password = hashlib.md5(password.encode()).hexdigest()
self.soft_id = soft_id
self.base_url = "http://upload.chaojiying.net/Upload/Processing.php"
def recognize(self, img_path, codetype=1902, timeout=30):
"""
识别验证码
:param img_path: 验证码图片路径
:param codetype: 识别类型 1902=4位纯数字 1004=4位英文数字混合 等
:param timeout: 超时秒数
:return: 识别结果字符串
"""
with open(img_path, 'rb') as f:
img_data = f.read()
files = {'userfile': ('captcha.png', img_data)}
data = {
'user': self.username,
'pass2': self.password,
'softid': self.soft_id,
'codetype': codetype,
}
response = requests.post(self.base_url, data=data, files=files, timeout=timeout)
result = response.json()
if result.get('err_no') == 0:
return result.get('pic_str')
else:
raise Exception(f"识别失败: {result.get('err_no')} - {result.get('err_str')}")
def report_error(self, pic_id):
"""识别错误时上报,平台返还该次扣分"""
url = "http://upload.chaojiying.net/Upload/ReportError.php"
data = {
'user': self.username,
'pass2': self.password,
'id': pic_id,
}
response = requests.post(url, data=data)
return response.json()
# 使用示例
if __name__ == "__main__":
# 请从超级鹰官网获取你的注册参数
client = ChaoJiYingClient(
username='your_username',
password='your_password',
soft_id='123456' # 软件ID
)
# 识别图形验证码
start = time.time()
result = client.recognize('captcha.png', codetype=1004)
print(f"识别结果: {result}, 耗时: {time.time()-start:.2f}秒")
# 识别滑块验证码缺口位置(需配合本地处理)
# 滑块类型 codetype 取决于平台具体支持情况(通常是9101等)
3.4 接入打码平台的关键注意事项
根据业界经验,以下细节经常导致识别成功率低:
- 图片文件名必须带后缀(
.png/.jpg),否则部分平台解析失败 codetype绝对不能填错:1004=4位英文数字混合,9004=中文,1902=4位纯数字;填错直接导致识别率骤降- 密码需要 MD5 加密:
pass2 = hashlib.md5(password.encode()).hexdigest(),不是明文传原始密码 - 验证码图片必须先下载再传文件内容,直接传URL到打码平台容易被拒
- 识别成功后,有些网站校验时还需要提交
pic_id(超级鹰返回的本次识别ID),要与验证码字符串一起提交给服务器,否则后端可能识别失败 - 识别失败要上报:如果识别结果提交后服务器返回“验证码错误”,主动调用
report_error(pic_id),平台会返还该次扣分;长期不报告会导致账号信誉下降、成本增加
3.5 将打码平台集成到爬虫(完整闭环)
import time
import requests
from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
from chaojiying import ChaoJiYingClient # 假设上面代码已保存为chaojiying.py
def login_with_captcha(username, password, login_url, captcha_img_xpath, captcha_input_xpath):
driver = webdriver.Chrome()
driver.get(login_url)
# 初始化打码平台客户端
cjy = ChaoJiYingClient(
username='YOUR_ACCOUNT',
password='YOUR_PASSWORD',
soft_id='YOUR_SOFT_ID'
)
for attempt in range(3):
try:
# 获取验证码图片元素
img_element = WebDriverWait(driver, 10).until(
EC.presence_of_element_located((By.XPATH, captcha_img_xpath))
)
img_path = f"captcha_{attempt}.png"
img_element.screenshot(img_path)
# 调用打码平台识别
captcha_text = cjy.recognize(img_path, codetype=1004)
print(f"第{attempt+1}次 识别结果: {captcha_text}")
# 填写表单
driver.find_element(By.NAME, 'username').send_keys(username)
driver.find_element(By.NAME, 'password').send_keys(password)
driver.find_element(By.XPATH, captcha_input_xpath).send_keys(captcha_text)
# 提交登录
driver.find_element(By.XPATH, "//button[@type='submit']").click()
time.sleep(2)
# 验证登录是否成功(可用url或页面元素判断)
if "登录成功" in driver.page_source or "dashboard" in driver.current_url:
print("✅ 登录成功!")
return driver
else:
print("❌ 验证码错误,重试...")
# 刷新验证码(通常点击验证码图片刷新)
img_element.click()
time.sleep(1)
continue
except Exception as e:
print(f"第{attempt+1}次尝试失败: {e}")
continue
print("登录失败,已达到最大重试次数")
driver.quit()
return None
# 使用
driver = login_with_captcha(
username="myuser",
password="mypass",
login_url="https://example.com/login",
captcha_img_xpath="//img[@id='captcha_img']",
captcha_input_xpath="//input[@name='captcha']"
)
核心问题:你一定碰不到的隐藏坑:很多网站为了防止自动化,验证码图片的URL本身携带动态时间戳(/captcha?t=XXXXXX),如果用 requests.get(img_url) 直接请求可能拿不到真实图片 —— 但上面的代码用的是 img_element.screenshot(),从未分离地跟在当前的浏览器session后,不会遇到这类问题。因此教程中全部使用“截屏img元素”的方式,而非通过URL额外请求,这是新教程必须强调的最佳实践。
四、方案对比与选型决策
| 对比维度 | 本地OCR (ddddocr) | 打码平台 |
|---|---|---|
| 图形验证码成功率 | 80%-95%(取决于难度) | 95%-99% |
| 滑块验证码支持 | ✅ 可定位缺口(简单场景) | ✅ 多数平台支持 |
| 成本 | 免费 | 0.5-3分/次 |
| 网络依赖 | 离线可用 | 需要联网 |
| 部署难度 | 简单(pip install) | 简单(API调用) |
| 汉字验证码能力 | ❌ 识别弱 | ✅ 支持 |
| 点选验证码支持 | ❌ 不支持 | ✅ 部分支持 |
决策矩阵
┌─────────────┐
│ 是什么类型 │
└──────┬──────┘
↓
┌────────────────────────────────┐
│ │
┌───▼────┐ ┌─────▼─────┐
│图形验证码│ │滑块/点选码│
└────┬────┘ └─────┬─────┘
↓ ↓
┌─────────────────┐ ┌─────────────────┐
│ 准确率要求? │ │ 强制生产环境? │
└────────┬────────┘ └────────┬────────┘
↓ ↓
┌────────┴────────┐ ┌───────┴────────┐
↓ ↓ ↓ ↓
┌────┐ ┌────────────┐ ┌────┐ ┌────────────┐
│低 │ │高(>95%) │ │否 │ │是 │
└──┬─┘ └───────┬────┘ └──┬─┘ └───────┬────┘
↓ ↓ ↓ ↓
┌─────┐ ┌──────────┐ ┌─────────┐ ┌──────────┐
│ddddocr│ │打码平台 │ │评估成本 │ │打码平台 │
└─────┘ └──────────┘ │+尝试组合│ └──────────┘
└─────────┘
企业级方案:打码平台负载均衡架构
如果每天需要处理上万次验证码,需要引入以下设计:
- 多打码平台并行:将请求分发到超级鹰、云打码、CapMonster 等多个平台,实现平台级容灾
- 自动降级路由:主平台识别失败(或超时),自动切换到备用平台
- 识别结果缓存:相同图片哈希值 5 分钟内置信较高的重复结果命中后,直接返回,节省成本
- 识别失败自动重试队列:失败的验证码图片放入重试队列,延迟 3 秒、6 秒、12 秒重试,最多 3 次
核心伪代码:
captcha_platforms = [
ChaoJiYingClient(...), # 主平台
YunDaMaClient(...), # 备用1
CapMonsterClient(...) # 备用2
]
def solve_with_fallback(img_bytes, fallback_platforms):
for idx, platform in enumerate(fallback_platforms):
try:
result = platform.recognize(img_bytes)
if result:
return result
except Exception as e:
log(f"{platform.__class__.__name__} 识别失败: {e}")
continue
raise Exception("All captcha platforms failed")
五、验证码处理的常见踩坑与解决方案
坑1:ddddocr 识别结果总差一位
原因:图片中的验证码包含大小写字母,且模型默认输出混合大小写,但服务器要求全大写下发了不一样的预期。
解决:用小写转换统一格式(.upper() / .lower()):
result = ocr.classification(img_bytes)
result_upper = result.upper() # 强制转为大写
坑2:打码平台返回 ERROR 但本地图片正常
根本原因(API接入绝大部分都坑在这里):漏了前置请求(没种sessionid)直接去拿验证码图片,图片是动态生成的、服务器对不合法的访客返回 403 占位图。
- 没有用
session携带 Cookie - 图片 URL 含动态时间戳没有实时刷新
解决:
# 必须复用 session 去请求图片
session = requests.Session()
session.get(login_url) # 先访登录页,种下 sessionid
img_resp = session.get(captcha_img_url) # 保持同一个会话去请求
# 再用 img_resp.content 传给打码平台
坑3:图片 filename 没有后缀
打码平台解析时认为文件格式无法确定,返回格式错误 9002。
不要直接传二进制数据用一个奇葩字符串作为文件名,一定要带 .png 或 .jpg:
# ❌ 错误
files = {'userfile': ('data', img_bytes)}
# ✅ 正确
files = {'userfile': ('captcha.png', img_bytes)}
坑4:滑块验证码滑动后总提示“网络不给力”或“验证码错误”
2025 年以后的滑块验证码不只是校验是否滑到缺口(基础位置校验),同时也在分析三个维度的行为特征:
- 轨迹特征:加速度、停顿、抖动
- 上下文行为:点滑块前的鼠标移动、点击位置偏差
- 设备指纹一致性
解决方案:缺口的精准识别 + 模拟真人轨迹(带加减速、随机抖动、随机停顿)必须一起上。通用策略:
import random
def generate_slider_track(distance):
"""
模拟真人滑块的拖动轨迹(匀加速+匀减速模型)
"""
track = []
current = 0
mid = distance * 4 / 5 # 前4/5加速,后1/5减速
t = 0.2 # 单位时间间隔
v = 0
while current < distance:
if current < mid:
a = random.uniform(2, 4) # 加速度 2-4 模拟手指启动
else:
a = -random.uniform(3, 6) # 减速度 3-6
v0 = v
v = v0 + a * t
move = v0 * t + 0.5 * a * t * t
if move < 0:
break
move = round(min(move, distance - current), 2)
current += move
track.append(move)
# 补足剩余像素(防止未能准确到达距离)
if current < distance:
track.append(distance - current)
return track
# 使用轨迹序列执行滑动(配合 Selenium ActionChains)
# for step in track:
# action_chains.drag_and_drop_by_offset(slider, step, 0).perform()
轨迹的真实感测试:在代码中打印 track 数组,观察数据分布(先密集后稀疏,夹杂随机微调),大量固定步长轨迹会被服务器识别为机器行为。
坑5:验证码无限刷新,爬虫突然进入死循环
场景:登录页面验证码识别第一次失败后刷新,刷新了 10 次都失败,程序一直卡住。
解决:限制重试次数(retry 3次)+ 设置最大重试间隔,重试超过停止并抛出异常:
def safe_captcha_solve(max_retries=3):
for attempt in range(max_retries):
try:
result = recognise_func()
# 假设识别成功,填入并提交
fill_and_submit(result)
# 登录成功后跳出循环
if login_success:
return
except Exception:
# 刷新验证码
refresh_captcha()
time.sleep(1 + attempt * 0.5) # 重试等待时间递增
raise Exception("Max retries exceeded for captcha solving")
坑6:打码平台识别结果识别正确,网站却提示“验证码错误”,但人眼手动输入正确
原因:网站执行了“会话绑定”,验证码图片在生成时,服务器后端往内存里的 Session 中存下了本次图片对应的 ID(captcha_id)。你提交验证码时,必须一并将这个 captcha_id 提交上去,后端对不上的直接判定失败。
解决方案:抓包登录请求,发 POST 时除了验证码答案之外,带上一个隐藏字段(例如 captcha_id、captcha_key),该字段必须从上一页面通过 BeautifulSoup 提取的动态值同步传入。
六、哪些验证码无法低成本绕过?
6.1 商业级(不可绕过)验证码列表
| 验证码类型 | 代表服务 | 为什么难 | 低成本方法有效吗 |
|---|---|---|---|
| 极验3/4 (Geetest) | 淘宝、微博、B站 | 行为轨迹+深度指纹分析 | ❌ 需CNN+轨迹模拟+逆向 |
| Google reCAPTCHA v3 | 全球数百万网站 | 完全无感,不显示验证框,后台持续评分 | ❌ |
| 阿里云验证码2.0 | 高德、菜鸟裹裹 | AI驱动视觉+行为双重模型 | ❌ |
| Cloudflare Turnstile | Cloudflare掩护的网站 | 切换为无感验证,纯环境指纹对抗 | ❌ |
| 腾讯防水墙 | 微信、QQ空间 | 三因子风控模型 | ❌ |
| 网易易盾 | 各大游戏官网 | 机器视觉模型+设备指纹 | ❌ |
6.2 具体成本差异对比
- ddddocr 成本:免费,安装 10 秒,推理 <0.1 秒
- 打码平台单次成本:0.5~3 分人民币,适用图形验证码和部分滑块
- 商业高精度模型(针对极验等) :需要大量的训练数据和工程师成本,普通项目完全烧不起
6.3 面对商业级验证码的调研思路
如果您遇到了商业级验证码,不要在本课的低成本方案上浪费时间。其绕过涉及三个方向:
- 逆向其JS加密逻辑:找出签名生成函数,用Python重写后直接调用接口(需要前端逆向能力)
- CNN + 轨迹模拟 + 设备指纹伪装:需要真金白银投入模型训练
- 采用专业打码平台的人工/半人工识别服务:价格是普通图形验证码的10倍以上(按次收费)
关于极验、reCAPTCHA 的绕过方法较为敏感且技术细节远超本课范围,感兴趣可在学完本专栏并具备 JS 逆向能力后再做探索。
七、合规边界与法律风险(非常重要)
7.1 法律核心红线:避开或破坏技术措施即违法
《反不正当竞争法》第12条明确规定,不得利用技术手段妨碍其他经营者合法提供网络产品或服务。在爬虫情景下,这意味着通过破解技术手段绕过网站反爬机制(如验证码、IP限制)获取数据,或对目标系统造成实质性干扰(如高频请求导致服务崩溃),均已构成违法行为。
7.2 入罪的边界——从行政处罚到刑事犯罪
若你绕过的验证码属于“平台的核心技术防护措施”,或你的爬取行为导致了服务器过载、数据泄露或商业模式被破坏,则除民事责任外还可能触及:
- 《刑法》第285条:非法获取计算机信息系统数据罪
- 《网络安全法》第44条:不得非法获取个人信息
行业曾有实际案例:某人绕过某平台登录验证码,批量爬取后台数据用于自己的商业分析,最终以非法获取计算机信息系统数据罪被判刑。虽然具体刑期高于此,但足以警醒:即便你只是“爬公开数据的非登录验证码”,只要你绕着走,就属于对抗技术措施,风险的尺度由被爬网站的损失与态度共同决定。
7.3 只有以下场景才是相对安全的
验证码技术措施只应在以下三种非常有限的场景中使用:
- 爬取自己完全拥有的网站或服务器(没有对任何第三方产生侵入)
- 获得对方书面授权的公开数据抓取,并且对方明确同意允许技术对抗
- 学术研究和内部测试(但不得公开传播绕过方案)
7.4 合规的替代方案
与其绞尽脑汁绕过,不如主动融入平台规则:
- 寻找官方API:先花时间研究平台的开发者文档(90%的平台有官方数据出口)。第12课的内容已经把如何找异步接口讲透。
- 数据购买:对关键商业数据走付费授权渠道——这是任何公司都愿意接受的办法。
- 只读非敏感数据 + 降低频率:不涉及用户隐私和核心业务数据,通过降速把验证码触发概率降到零。
如果需要绕过的是需登录的网站并绕过了图形验证码进行暴力撞库,会立刻触犯《网络安全法》第四十四条——这是强监管场景。
八、总结
本课核心知识清单
| 知识点 | 掌握程度 |
|---|---|
| 验证码类型与原理 | 能区分图形/滑块/点选的验证码,知道各自的技术本质 |
ddddocr 库使用 |
能独立处理4-6位字母数字混合验证码,定位简单滑块缺口 |
| 图像预处理(OpenCV) | 能使用灰度化、二值化等提升低质量图片识别率 |
| 打码平台 API 接入 | 能完成超级鹰 SDK 集成,正确处理 codetype、Session复用、pic_id补充上报 |
| 验证码自动重试闭环 | 能从页面提取验证码→识别→提交→错误重试免人工干预 |
| 合规边界 | 清楚绕过“技术措施”的法律风险,能为自己的爬虫项目做书面合规评估 |
| 哪些验证码不能碰 | 能一眼识别极验、reCAPTCHA、腾讯等商业级验证码,判断是否需要投入高级人力 |
选型决策总结
- 首选「无码」方案:低频率 + 绕开触发阈值,不触发才是最高效
- 图形验证码:本地
ddddocr免费(85-95%准确率)> 确认准确率难满足时再上打码平台(2分/次,98%准确率) - 简单滑块验证码:
ddddocr缺口定位 + 手动构造轨迹(匀加速+抖动+停顿)【关键的成败点在于不能匀速滑动】 - 点选/高级滑块/无感验证码:放弃解决,考虑是否一定要爬这个网站,或转向商业级高成本服务
九、课后作业
作业1:ddddocr 本地识别(必做)
从网上下载 10 张不同类型的图形验证码(4-6位英文数字混合,有轻微干扰线及无干扰线的各半),使用 ddddocr 识别,统计准确率。
提交要求:
- 附上图片样例和识别结果
- 如果识别失败,尝试预处理(二值化、降噪),记录提升效果
作业2:滑块验证码缺口定位(必做)
找一个提供滑块验证码的测试网站(如 https://www.geetest.com/demo 演示版),用 Selenium 截图滑块和背景图,再用 ddddocr.slide_match() 获取缺口X坐标,计算出滑块需要移动的距离,并打印此距离。
作业3:超级鹰API集成(必做)
注册超级鹰账户并完成充值(最小金额体验),将本课中的 ChaoJiYingClient 类跑通,识别至少 5 张网页中自动出现的图形验证码。
作业4:登录验证码处理闭环(必做)
找一个公开的、带验证码的测试登录网站(开发自建测试站点或者如常见开源论坛演示站),用 Selenium 实现:
- 打开登录页面
- 定位验证码图片元素,截图传给
ddddocr(或打码平台) - 识别验证码并填入
- 提交登录表单
- 循环重试最多 3 次
作业5:识别行为合规报告(必做)
选择一个你正在使用的爬虫项目(真实或假设),评估它所面对的验证码类型、触发频率以及在“商业级验证码”中的分类。写出 300 字的技术与法律合规角度分析,明确是否建议采用绕过方案。
结束语:验证码是反爬的终极防线,但绝非不可逾越。本课从成本最低的 ddddocr 方式切入,教你用极低的成本让绝大多数图形验证码不再成为障碍。但也希望你能深刻记住:绕过验证码的成本不只在技术上,更在法律和道德上。当你面对一条验证码时,技术问题解决了50%,剩下的50%是“该不该做”。
下一课,我们将学习爬虫框架 Scrapy——从“编写脚本”到“搭建生产级系统”,让你拥有大规模、分布式爬取的能力。
第18课见。
🔗《20节课精通网页爬虫》系列课程导航
🌟 感谢您耐心阅读到这里!
💡 如果本文对您有所启发欢迎:
👍 点赞📌 收藏 📤 分享给更多需要的伙伴。
🗣️ 期待在评论区看到您的想法, 共同进步。
🔔 关注我,持续获取更多干货内容~
🤗 我们下篇文章见~
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐

所有评论(0)