验证码滑动轨迹浅谈

本文章只做技术探讨, 请勿用于非法用途。

引言

验证码也是我们在做爬虫工作中一个很麻烦的部分, 今天来聊一聊在验证码处理中, 如何模拟滑动轨迹。

思路

我们从两个方向入手, 首先是轨迹, 我们要设计模拟一条接近人为操作的轨迹出来, 比如肯定不能是一条直线这么简单。其次是速度, 他也不能是简单的匀速运动这么简单。下面就这两个方面, 我简单的提供一些处理思路。

轨迹

轨迹方面的操作, 我在这里介绍一下贝塞尔曲线, 详细的知识可以参考一些文章, 这里不做详细探讨。 (在 css 的动画渲染中也是常用的, 可以参考。www.w3cschool.cn/lugfe/lugfe...)

原理不是我们的重点, 这里我们使用三阶贝塞尔曲线, 它经常被用在路径规划的算法中。简单的说, 就是只需要我们提供 P0, P1, P2, P3 四个点, 它可以为我们生成一条从 P0 到 P3, 中间会向 P1 和 P2 靠近的一条光滑曲线。

曲线展示图

可以在这里自己测试玩一玩。

而对于我们的验证码来说, P0、P3 的位置是确定的, 我们只需要根据距离计算出合适的 P1、 P2 的位置就能得到这么一条平滑的轨迹路线。

时间

关于时间, 这里会介绍一些 css 动画渲染中常用的缓动函数, 他们也经常和贝塞尔曲线一起使用。

Python 复制代码
# 三次方加速函数
def ease_in_cubic(t):
    return t * t * t

# 三次减速函数
def ease_out_cubic(t):
    return 1 - pow(1 - t, 3)

# 回弹加速函数
def ease_in_back(t):
    c1 = 1.70158
    c3 = c1 + 1
    return c3 * t * t * t - c1 * t * t

# 回弹减速函数
def ease_out_back(t):
    c1 = 1.70158
    c3 = c1 + 1
    return 1 + c3 * pow(t - 1, 3) + c1 * pow(t - 1, 2)    

想了解原理的话可以参考这篇文章, 本质上是一些数学函数的应用。

总结

整体上来说, 是借鉴了一些 css 中动画渲染是用的思路, 尝试将我们的模拟曲线变得更加自然。

实践

只讲原理的话可能比较生硬, 我们基于这些来做个实践, 看一下成果。

目标网站

目标的话就选用某宝下边的验证码, 他不涉及滑块部分, 比较好演示, 且轨迹校验还是挺严格的, 有时候手动滑都不一定能过。

任意一条 1688 商品链接(例如 detail.1688.com/offer/77959...) 在干净的环境中打开, 就能看到验证码出现。

目标验证码示例

准备工作

简单的分析一下, 验证码条长度一共 300px, 也就是说我们找到滑块, 向右拖动 300px 的距离即可完成验证, 如果轨迹没问题, 就可以正常看到数据。

因为仅是轨迹处理, 我们选用 Playwright 自动化来进行演示(排除掉接口加密影响), 首先需要处理自动化检测部分, 我们使用 Playwright 打开一个默认的浏览器环境, 进行手动滑动, 发现始终无法成功, 就可以确定是有自动化环境检测的。

自动化环境检测失败

为了处理这个问题, 我们可以手动打开一个正常的 chrome 应用, 然后通过 playwright 连接到这个应用上, 具体命令如下。

Shell 复制代码
# shell 中找到自己安装的 chrome 位置
chrome --remote-debugging-port=9222 --user-data-dir="新的环境地址"

# Python 代码连接应用
with sync_playwright() as p:
    # 连接到已打开的浏览器
    browser = p.chromium.connect_over_cdp("http://localhost:9222")
    # 通常获取第一个上下文
    default_context = browser.contexts[0]
    # 获取该上下文中的第一个页面,或者新建一个
    page = default_context.pages[0] if default_context.pages else default_context.new_page()

在这个窗口上, 我们手动滑可以过的时候, 就可以开始我们的模拟调试了。另外, 通过这种方式对比两个环境变量, 可以判断出被检测的环境, 补上之后也可以使用默认的环境。

部分代码展示

按照之前提供的思路, 我们来写模拟相关的代码。

工具相关部分。

Python 复制代码
# 三阶贝塞尔曲线计算
def cubic_bezier(self, t, p0, p1, p2, p3):
    mt = 1 - t
    return mt**3 * p0 + 3 * mt**2 * t * p1 + 3 * mt * t**2 * p2 + t**3 * p3
    
# 回弹的缓动函数(滑过再回来), 根据需求看要不要用
def ease_out_back(self, t):
    c1 = 1.70158
    c3 = c1 + 1
    return 1 + c3 * pow(t - 1, 3) + c1 * pow(t - 1, 2)

# 三次减速函数
def ease_out_cubic(t):
    return 1 - pow(1 - t, 3)

# 根据每次移动距离计算延迟等待的时间
def calculate_natural_delay(self, move_distance, progress):
    """
    自然延迟计算 - 调整最后阶段的减速
    """
    # 基础延迟
    base_delay = 14

    # 基于进度的速度变化 - 让最后阶段减速更平缓
    if progress < 0.2:  # 开始阶段 - 稍快
        speed_factor = random.uniform(0.8, 1.1)
    elif progress < 0.4:  # 加速阶段
        speed_factor = random.uniform(0.7, 1.0)
    elif progress > 0.85:  # 最后15% - 轻微减速
        speed_factor = random.uniform(1.1, 1.4)
    elif progress > 0.7:  # 最后30% - 开始减速
        speed_factor = random.uniform(1.0, 1.3)
    else:  # 中间阶段 - 稳定
        speed_factor = random.uniform(0.9, 1.1)

    # 基于距离的微调
    distance_factor = max(0.9, min(1.3, move_distance / 3.5))
    final_delay = base_delay * speed_factor * distance_factor
    return max(8, min(25, final_delay))

# 根据 移动距离 和 缓动函数 结果计算时间(根据需要选择不同的计算函数)
def calculate_delay(self, current_data, prev_data, move_distance, progress):
    """
    计算延迟时间 - 简化的智能延迟
    """
    # 理论时间间隔
    time_ratio = current_data['time_progress'] - prev_data['time_progress']
    theory_delay = max(1, time_ratio * self.total_duration)

    # 基于距离的基础延迟
    base_delay = max(3, min(20, move_distance * 0.2))

    # 基于进度的调整
    if progress < 0.3:
        progress_factor = random.uniform(0.8, 1.1)
    elif progress > 0.8:
        progress_factor = random.uniform(1.2, 1.6)
    else:
        progress_factor = random.uniform(0.9, 1.2)

    smart_delay = base_delay * progress_factor
    # 结合理论时间和智能延迟
    final_delay = theory_delay * 0.6 + smart_delay * 0.4

    return max(2, min(30, final_delay))

# 滑动过程模拟抖动
def calculate_jitter(self, t, x, y):
    """根据进度计算智能抖动"""
    # 开始阶段:较大抖动(模拟启动不稳定)
    if t < 0.2:
        jitter_x = random.uniform(-1.0, 1.0)
        jitter_y = random.uniform(-1.5, 1.5)
    # 中间阶段:较小抖动(模拟稳定移动)
    elif t < 0.8:
        jitter_x = random.uniform(-0.3, 0.3)
        jitter_y = random.uniform(-0.5, 0.5)
    # 结束阶段:轻微抖动(模拟精准定位)
    else:
        jitter_x = random.uniform(-0.1, 0.1)
        jitter_y = random.uniform(-0.2, 0.2)

    return jitter_x, jitter_y

明确一点, 就是在我们模拟的过程中, 其实是从一个点瞬移到另一个点的, 移动中间的间隔就是等到延迟时间。我们无法完全模拟出连续的滑动, 只能计算尽可能多的点来模拟效果(但不是越多越好, 过多的点会导致滑动顿挫严重)。

轨迹计算部分。

Python 复制代码
def generate_multi_segment_trajectory(self, start_x, start_y, distance, num_points=100):
        """
        生成多段式轨迹,模拟人类拖动的不同阶段
        """
        end_x = start_x + distance
        end_y = start_y
        
        # 分段控制点 - 创造更自然的曲线
        segments = [
            # 第一阶段:加速段 (0-30%)
            {
                'start_t': 0.0, 'end_t': 0.3,
                'cp1': (start_x + distance * 0.15, start_y - random.randint(5, 12)),
                'cp2': (start_x + distance * 0.25, start_y - random.randint(8, 15))
            },
            # 第二阶段:匀速段 (30-70%)
            {
                'start_t': 0.3, 'end_t': 0.7,
                'cp1': (start_x + distance * 0.4, start_y + random.randint(-10, 10)),
                'cp2': (start_x + distance * 0.6, start_y + random.randint(-10, 10))
            },
            # 第三阶段:减速段 (70-90%)
            {
                'start_t': 0.7, 'end_t': 0.9,
                'cp1': (start_x + distance * 0.75, start_y + random.randint(5, 12)),
                'cp2': (start_x + distance * 0.85, start_y + random.randint(8, 15))
            },
            # 第四阶段:微调段 (90-100%)
            {
                'start_t': 0.9, 'end_t': 1.0,
                'cp1': (start_x + distance * 0.92, start_y + random.randint(-5, 5)),
                'cp2': (start_x + distance * 0.96, start_y + random.randint(-3, 3))
            }
        ]
        
        trajectory = []
        
        for i in range(num_points):
            t = i / (num_points - 1)
            
            # 确定当前属于哪个段
            current_segment = None
            for seg in segments:
                if seg['start_t'] <= t <= seg['end_t']:
                    current_segment = seg
                    break
            
            if current_segment:
                # 将t映射到当前段的范围
                seg_t = (t - current_segment['start_t']) / (current_segment['end_t'] - current_segment['start_t'])
                
                # 不同阶段使用不同的缓动函数
                if current_segment['start_t'] == 0.0:
                    eased_t = self.ease_out_cubic(seg_t * 0.8)  # 加速段
                elif current_segment['start_t'] == 0.9:
                    eased_t = 0.9 + self.ease_out_back(seg_t) * 0.1  # 微调段
                else:
                    eased_t = seg_t  # 中间段接近线性
                
                # 计算贝塞尔曲线点
                x = self.cubic_bezier(eased_t, start_x, current_segment['cp1'][0], 
                                    current_segment['cp2'][0], end_x)
                y = self.cubic_bezier(eased_t, start_y, current_segment['cp1'][1], 
                                    current_segment['cp2'][1], end_y)
                
                # 智能抖动 - 不同阶段抖动幅度不同
                jitter_x, jitter_y = self.calculate_jitter(t, x, y)
                x += jitter_x
                y += jitter_y
                
                trajectory.append((x, y))
        
        return trajectory

我这边最初是设计了四个阶段, 但实际测试中, 三个阶段的效果反而更好, 依据实际效果选择。

滑动函数部分。

Python 复制代码
# 
def optimized_drag_example():
    optimizer = HumanDragOptimizer()
    
    with sync_playwright() as p:
        browser = p.chromium.launch(
            headless=False,
            args=['--disable-blink-features=AutomationControlled']
        )
        
        # 创建上下文并隐藏自动化特征
        context = browser.new_context()
        # # 无头模式下添加
        # context = browser.new_context(
        #     viewport={'width': 1920, 'height': 1080},
        #     user_agent='Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.0.0 Safari/537.36',
        #     device_scale_factor=1,  # 真实的设备像素比
        #     has_touch=False,
        #     is_mobile=False
        # )
        context.add_init_script("""
            Object.defineProperty(navigator, 'webdriver', {
                get: () => undefined,
           });
        """)
        
        page = context.new_page()
        # 导航到目标页面
        page.goto('https://detail.1688.com/offer/735511910748.html')
        
        # 等待滑块元素
        slider = page.wait_for_selector("#nc_1_n1z", state="visible", timeout=10000)
        
        # 使用自适应重试策略执行拖动
        optimizer.adaptive_retry_strategy(page, slider, distance=300)

我这边已经通过比对环境参数, 补全了默认环境, 可以正常的使用默认环境。无头模式也一样, 可以通过补一些参数解决。

效果展示

默认环境效果展示

目前我这边的成功率, 大概是百分之九十左右, 有兴趣的话可以继续做一些调优。

无头模式效果展示

无头模式下, 可能会对环境检测更加严格些, 可以多做些测试。

总结

不是什么教程, 仅作为一种处理思路, 如果有更好的方法或是什么问题欢迎一起交流。

文中可能涉及一些数学相关的概念, 感兴趣的朋友可以自行了解, 代码相关仅作为参考就行, 按需要选用。

请洒潘江,各倾陆海云尔。

相关推荐
空空kkk5 小时前
SpringMVC——异常
java·前端·javascript
冴羽5 小时前
涨见识了,Error.cause 让 JavaScript 错误调试更轻松
前端·javascript·node.js
m***D2865 小时前
JavaScript在Node.js中的内存管理
开发语言·javascript·node.js
我叫张小白。5 小时前
JavaScript现代语法梳理:ES6+核心特性详解
开发语言·javascript·typescript·es6
啃火龙果的兔子5 小时前
react-i18next+i18next-icu使用详解
前端·javascript·react.js
1024小神5 小时前
Electron实现多tab页案例,BrowserView/iframe/webview不同方式的区别
前端·javascript·electron
U***e636 小时前
Vue自然语言
前端·javascript·vue.js
拉不动的猪6 小时前
Vue 跨组件通信底层:provide/inject 原理与实战指南
前端·javascript·面试
用户6600676685396 小时前
用 Symbol 解决多人协作中的对象属性冲突实战
前端·javascript
c***97986 小时前
Vue性能优化实战
前端·javascript·vue.js