爬虫中级——滑块验证码的应对方法

爬虫中级------滑块验证码 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. 访问挑战页,默认显示第 1 页数字(无需验证)。
  2. 点击"下一页"或直接访问 page=2,页面弹出滑块验证区域。
  3. 验证码区域自动加载背景图和滑块图片。
  4. 用户拖动滑块,松开后自动提交验证。
  5. 若验证通过,返回该页数字;否则刷新验证码重试。

3.2 关键接口捕获

通过 F12 → Network,我们捕获到三个核心接口:

(1)初始化接口(/init/
  • 请求GET /captcha/api/cap4_challenge/init/?challenge_type=cap4_challenge
  • 响应 :包含 successpage_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 的模板匹配功能,在背景图中寻找拼图块的最佳匹配位置。

步骤

  1. overlaymasked 的 base64 数据解码为 OpenCV 图像(BGR 格式)。
  2. 转为灰度图(减少计算量)。
  3. 使用 cv2.matchTemplate 进行模板匹配(方法 TM_CCOEFF_NORMED)。
  4. 从匹配结果中获取最大值位置 (x, y),即缺口左上角在原始背景图中的坐标。
  5. 根据 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. 运行与调试

  1. 手动登录 https://www.spiderdemo.cn/admin_I/
  2. 按 F12 → Application → Cookies → 复制 sessionidcsrftoken
  3. 替换代码中的 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 获取实际容器宽高。
  • 多模板匹配:尝试多种匹配算法,取最优结果。
  • 增加日志记录:记录每次重试的细节,便于分析失败原因。

滑块验证码的本质是"人机交互",而我们的爬虫通过"逆向 + 图像处理"成功模拟了这一交互。 希望本文能为你攻克滑块验证码提供一条清晰的思路。如有问题,欢迎交流讨论!

相关推荐
明月_清风16 小时前
robots.txt 完全指南:从入门到精通
后端·爬虫
明月_清风16 小时前
从零写一个"像人"的爬虫:反爬攻防实战指南
后端·爬虫
嘘嘘出差2 天前
从零到精通:爬虫技术进阶路线全解析
爬虫
嘘嘘出差2 天前
Python 爬虫入门实战:爬取豆瓣电影 Top 250
开发语言·爬虫·python
嘘嘘出差2 天前
天气数据监控系统
jvm·爬虫
小花皮猪3 天前
Bright Data CLI 入门教程:命令行实现 Web Scraping 与结构化数据采集
爬虫
Maiko Star3 天前
Python项目实战——网络机器人(爬虫)
爬虫·python·机器人
STDD3 天前
n8n 实战工作流模板:GitHub→飞书通知、定时爬虫→数据库、Webhook→多渠道推送
爬虫·github·飞书
跨境数据猎手4 天前
反向海淘实战|独立站搭建+国内电商API对接
开发语言·爬虫·架构