爬虫中级------滑块验证码 JS 逆向与自动化采集实战
从接口逆向到图像识别,构建永不放弃的滑块验证码爬虫
1. 引言
在爬虫进阶之路上,滑块验证码 是继文字验证码之后的又一道"拦路虎"。它通过要求用户拖动拼图块至缺口位置,来区分人类和机器。对于爬虫而言,最核心的任务不再是 OCR 识别字符,而是精确计算滑块的位移距离。
本文将带你从 JS 逆向 的角度,深入分析滑块验证码的交互流程、接口设计,并结合 OpenCV 图像匹配 实现全自动位移计算。最终,我们将打造一套"永不放弃"的爬虫系统,稳定采集 100 页数字并求和。
技术栈:Python + requests + OpenCV + NumPy + PIL
2. 挑战目标
- 目标网站 :
https://www.spiderdemo.cn/captcha/cap4_challenge/?challenge_type=cap4_challenge - 任务:采集 100 页数字,每页 10 个随机整数,计算总和并提交答案。
- 验证机制:第二页开始,翻页需通过滑块拼图验证。
- 与第一代区别:验证方式从"输入字符"变为"拖动滑块",无需 OCR,需图像处理。
3. 第一步:JS 逆向与接口分析
3.1 打开开发者工具,观察交互流程
- 访问挑战页,默认显示第 1 页数字(无需验证)。
- 点击"下一页"或直接访问
page=2,页面弹出滑块验证区域。 - 验证码区域自动加载背景图和滑块图片。
- 用户拖动滑块,松开后自动提交验证。
- 若验证通过,返回该页数字;否则刷新验证码重试。
3.2 关键接口捕获
通过 F12 → Network,我们捕获到三个核心接口:
(1)初始化接口(/init/)
- 请求 :
GET /captcha/api/cap4_challenge/init/?challenge_type=cap4_challenge - 响应 :包含
success、page_data(第一页数字)、has_passed_before等。 - 作用:建立会话,并直接返回第一页数据,避免首次验证。
(2)滑块验证码图片接口(/captcha_image/)
-
请求 :
GET /captcha/api/cap4_challenge/captcha_image/?t=时间戳 -
响应示例 :
json{ "masked": "base64编码的PNG(拼图块,带透明背景)", "overlay": "base64编码的WebP(带缺口的背景图)", "image_info": { "bg_width": 400, "bg_height": 240, "mask_width": 50, "mask_height": 50 }, "slider_y": 80, "captcha_id": "abc123..." } -
关键字段 :
overlay:背景图,缺口位置即为目标。masked:可拖动的拼图块。image_info:原始图片尺寸。slider_y:缺口在垂直方向的偏移(用于对齐)。captcha_id:本次验证的唯一标识,提交时必须附带。
(3)提交验证并获取数据(/page/)
-
请求 :
POST /captcha/api/cap4_challenge/page/ -
请求体 :
json{ "distance": 120, // 滑块水平位移(像素) "page_num": 2, "challenge_type": "cap4_challenge", "captcha_id": "abc123...", "container_width": 400, // 当前容器宽度 "container_height": 240 // 当前容器高度 } -
响应 :成功时返回
{"success": true, "page_data": [8629, 7103, ...]}。
3.3 逆向分析前端逻辑
查看 cap4_challenge.js 源码,发现前端将滑块拖动的距离直接赋给 window.sliderDistance,并在 verifyCaptchaAndGetData 中将其作为 distance 提交。前端并未进行缺口位置计算 ,而是依赖用户手动拖动。因此,我们无需逆向加密算法,只需准确模拟用户拖动的距离即可。
重点在于理解 distance 的物理意义:
- 它是滑块左边缘相对于初始位置的像素位移。
- 有效范围受容器尺寸和图片缩放影响。
- 前端在
setupSliderDynamic中根据容器实际尺寸计算缩放,并调整滑块显示大小。
通过分析,我们得出 distance = x * scale,其中 x 是缺口在原始背景图中的水平坐标,scale = container_width / bg_width。因此,我们只需通过图像匹配得到 x,再乘以 scale 即可。
4. 图像匹配:计算缺口位置
4.1 准备 OpenCV
我们使用 OpenCV 的模板匹配功能,在背景图中寻找拼图块的最佳匹配位置。
步骤:
- 将
overlay和masked的 base64 数据解码为 OpenCV 图像(BGR 格式)。 - 转为灰度图(减少计算量)。
- 使用
cv2.matchTemplate进行模板匹配(方法TM_CCOEFF_NORMED)。 - 从匹配结果中获取最大值位置
(x, y),即缺口左上角在原始背景图中的坐标。 - 根据
image_info和容器宽度计算最终distance。
4.2 缩放比例处理
前端代码中,container_width 默认由 CSS 决定(通常为 400px),图片原始宽度 bg_width 已知(来自 image_info)。
缩放比例 scale = container_width / bg_width。
缺口在容器中的位置 distance = x * scale,同时要确保 distance 不超过最大可拖动距离 (bg_width - mask_width) * scale。
注意 :如果页面响应式导致 container_width 变化,可在爬虫中通过获取页面元素宽度动态调整,但大多数情况下固定为 400 即可。
5. Python 实现:全自动滑块验证爬虫
5.1 环境准备
bash
pip install requests pillow opencv-python numpy
注意:若系统已有依赖冲突,建议使用虚拟环境。本爬虫依赖 OpenCV 4.x,兼容 numpy 1.x。
5.2 核心代码结构
我们将沿用前代爬虫的 健壮框架:
requests.Session保持 Cookie。- 手动提供登录态 Cookie。
- 调用
/init/初始化会话。 - 每一页采用"无限重试"策略,失败时指数退避。
- 图像匹配失败或验证失败时自动重试。
关键函数清单
| 函数 | 功能 |
|---|---|
init_challenge() |
初始化,返回第一页数据(若有) |
get_captcha() |
获取滑块图片和 captcha_id |
calculate_distance() |
模板匹配,返回 distance |
fetch_page_forever() |
无限重试,直至成功获取该页数据 |
main() |
主循环,累加 100 页和 |
5.3 完整代码
python
import requests
import time
import cv2
import numpy as np
import base64
import random
from PIL import Image
# ======================== 配置 ========================
BASE_URL = "https://www.spiderdemo.cn"
INIT_URL = BASE_URL + "/captcha/api/cap4_challenge/init/"
CAPTCHA_IMAGE_URL = BASE_URL + "/captcha/api/cap4_challenge/captcha_image/"
DATA_URL = BASE_URL + "/captcha/api/cap4_challenge/page/"
CHALLENGE_TYPE = "cap4_challenge"
INITIAL_DELAY = 2
MAX_DELAY = 15
PAGE_INTERVAL = 1.5
# ======================== 会话与 Cookie ========================
session = requests.Session()
session.headers.update({
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Safari/537.36',
'Referer': BASE_URL + '/',
})
# ========== 请替换为你的 Cookie 字符串(从浏览器复制) ==========
COOKIE_STR = "sessionid=你的sessionid; csrftoken=你的csrftoken"
for item in COOKIE_STR.split('; '):
if '=' in item:
k, v = item.split('=', 1)
session.cookies.set(k, v)
# ======================== 初始化 ========================
def init_challenge():
print("🔧 初始化挑战...")
resp = session.get(INIT_URL, params={'challenge_type': CHALLENGE_TYPE})
if resp.status_code == 200:
data = resp.json()
if data.get('success'):
print("✅ 初始化成功")
return data.get('page_data')
else:
print(f"⚠️ 初始化失败: {data.get('error')}")
return None
else:
print(f"❌ 初始化 HTTP 错误: {resp.status_code}")
return None
# ======================== 获取滑块验证码 ========================
def get_captcha():
"""返回 (captcha_id, bg_img, piece_img, image_info)"""
resp = session.get(CAPTCHA_IMAGE_URL, params={'t': int(time.time()*1000)})
if resp.status_code != 200:
raise Exception(f"获取验证码失败,状态码 {resp.status_code}")
data = resp.json()
captcha_id = data['captcha_id']
image_info = data['image_info']
# base64 解码
bg_b64 = data['overlay'].split(',')[1] if ',' in data['overlay'] else data['overlay']
piece_b64 = data['masked'].split(',')[1] if ',' in data['masked'] else data['masked']
bg_bytes = base64.b64decode(bg_b64)
piece_bytes = base64.b64decode(piece_b64)
# 转为 OpenCV 图像
bg_img = cv2.imdecode(np.frombuffer(bg_bytes, np.uint8), cv2.IMREAD_COLOR)
piece_img = cv2.imdecode(np.frombuffer(piece_bytes, np.uint8), cv2.IMREAD_COLOR)
return captcha_id, bg_img, piece_img, image_info
# ======================== 计算滑动距离 ========================
def calculate_distance(bg, piece, image_info, container_width=400):
bg_gray = cv2.cvtColor(bg, cv2.COLOR_BGR2GRAY)
piece_gray = cv2.cvtColor(piece, cv2.COLOR_BGR2GRAY)
# 模板匹配
result = cv2.matchTemplate(bg_gray, piece_gray, cv2.TM_CCOEFF_NORMED)
_, max_val, _, max_loc = cv2.minMaxLoc(result)
x, y = max_loc
# 缩放
bg_width = image_info['bg_width']
scale = container_width / bg_width
distance = int(round(x * scale))
# 限制范围
max_dist = int(round((bg_width - image_info['mask_width']) * scale))
distance = min(max_dist, max(0, distance))
return distance
# ======================== 无限重试获取页面数据 ========================
def fetch_page_forever(page_num):
# 先尝试直接请求(可能不需要验证码)
resp = session.post(DATA_URL, json={
'page_num': page_num,
'challenge_type': CHALLENGE_TYPE
})
if resp.status_code == 200:
data = resp.json()
if data.get('success'):
return data['page_data']
elif '滑块' in data.get('error', '') or '验证' in data.get('error', ''):
# 需要验证,进入下方流程
pass
else:
# 其他错误,等待后重试
time.sleep(INITIAL_DELAY)
return fetch_page_forever(page_num)
delay = INITIAL_DELAY
while True:
try:
captcha_id, bg, piece, info = get_captcha()
distance = calculate_distance(bg, piece, info)
distance += random.randint(-2, 2) # 模拟人类微调
print(f" 计算滑动距离: {distance}px")
payload = {
'distance': distance,
'page_num': page_num,
'challenge_type': CHALLENGE_TYPE,
'captcha_id': captcha_id,
'container_width': 400,
'container_height': 240
}
resp = session.post(DATA_URL, json=payload)
if resp.status_code == 200:
data = resp.json()
if data.get('success'):
return data['page_data']
else:
error = data.get('error', '')
print(f" 提交失败: {error}")
if '滑块' in error or '验证' in error:
time.sleep(delay)
delay = min(delay * 2, MAX_DELAY)
continue
else:
time.sleep(delay)
delay = min(delay * 2, MAX_DELAY)
continue
elif resp.status_code == 405:
print(" 收到405,重新初始化...")
init_challenge()
time.sleep(2)
continue
else:
print(f" HTTP 错误: {resp.status_code}")
time.sleep(delay)
delay = min(delay * 2, MAX_DELAY)
continue
except Exception as e:
print(f" 异常: {e}")
time.sleep(delay)
delay = min(delay * 2, MAX_DELAY)
continue
# ======================== 主流程 ========================
def main():
init_data = init_challenge()
total_sum = 0
start_page = 2 if init_data else 1
if init_data:
total_sum += sum(init_data)
print(f"第一页数字: {init_data}, 本页和: {sum(init_data)}")
print("\n开始采集 100 页...")
for page in range(start_page, 101):
print(f"\n--- 正在采集第 {page} 页 ---")
numbers = fetch_page_forever(page)
page_sum = sum(numbers)
total_sum += page_sum
print(f" 数字: {numbers}")
print(f" 本页和: {page_sum}, 累计总和: {total_sum}")
if page < 100:
print(f" 等待 {PAGE_INTERVAL} 秒...")
time.sleep(PAGE_INTERVAL)
print("\n" + "="*50)
print(f"所有数字总和: {total_sum}")
print("="*50)
if __name__ == "__main__":
main()
6. 运行与调试
6.1 获取 Cookie
- 手动登录
https://www.spiderdemo.cn/admin_I/。 - 按 F12 → Application → Cookies → 复制
sessionid和csrftoken。 - 替换代码中的
COOKIE_STR。
6.2 执行脚本
bash
python slider_captcha.py
程序将自动完成 100 页采集,过程中会输出每页的进度和最终总和。

6.3 常见问题与解决
| 问题 | 原因 | 解决方案 |
|---|---|---|
| 401/登录错误 | Cookie 过期或无效 | 重新登录,更新 Cookie |
| 405 Method Not Allowed | 未初始化或会话失效 | 程序会自动重新初始化,若无,请检查 INIT_URL 是否正确 |
| 验证码总是失败 | 图像匹配不准确 | 调整 container_width,或改用其他匹配方法(如 TM_CCORR_NORMED) |
| OpenCV 依赖错误 | numpy 版本冲突 | 使用虚拟环境或降级 OpenCV 到 4.x |
7. 总结与展望
7.1 核心收获
- JS 逆向 :通过分析前端代码,我们明确了
distance的来源和计算方法,无需破解加密,只需模拟真实操作。 - 图像匹配:OpenCV 模板匹配有效解决了缺口定位问题,配合缩放计算,得到精准位移。
- 健壮性设计:无限重试 + 指数退避 + 自动初始化,确保了长时间运行的成功率。
7.2 可优化方向
- 动态容器尺寸:若页面自适应,可通过 Selenium 或解析 CSS 获取实际容器宽高。
- 多模板匹配:尝试多种匹配算法,取最优结果。
- 增加日志记录:记录每次重试的细节,便于分析失败原因。
滑块验证码的本质是"人机交互",而我们的爬虫通过"逆向 + 图像处理"成功模拟了这一交互。 希望本文能为你攻克滑块验证码提供一条清晰的思路。如有问题,欢迎交流讨论!