京东随机变速滑块拼图验证码识别

一、简介

京东的滑块验证码大家都很熟悉,最近京东对他进行了更新,变成了随机变速滑动的验证码。

什么是随机变速滑动验证码呢?为了好理解这个概念,先给大家讲一讲什么不变速滑动验证码。

不变速滑动验证码:鼠标拖动验证码滑块,向右拖动10像素,上图的拼图小图也向右移动10像素,滑块与拼图速度保持同步不变,我们把它叫做不变速滑动验证码。

变速滑动验证码:鼠标拖动验证码滑块,向右拖动10像素,上图的拼图小图却向右移动20像素,速度变快了;鼠标又向右拖动10像素,拼图小图却向右移动5像素,速度变慢了;滑块与拼图速度不同步,但是每次都保持同样的变化规律,我们把它叫做变速滑动验证码。

随机变速滑动验证码:鼠标拖动验证码滑块,向右拖动10像素,上图的拼图小图却向右移动20像素,速度变快了;鼠标又向右拖动10像素,拼图小图却向右移动5像素,速度变慢了;滑块与拼图速度不同步,但是每次都毫无规律可寻,我们把他叫做随机变速滑动验证码。

所以京东就变成了这种最难的随机变速滑动验证码

二、识别滑动差异点

以前的做法是下图所示,识别出拼图与缺口的位置是100px,就拖动下方滑块的100px,要么会乘以一个缩放比例系数来解决。

随机变速验证码之后,上面的方法就完全失效了,就变成了下图这种情况,虽然识别距离是100px,下方滑块拖动距离就变成了未知的x,即使是完全相同的验证码,每次的滑动距离都不同,所以是未知的x。

三、验证码识别

我们已经训练好了这款验证码的识别模型,正确率可以达到99%,下面的这个验证码的识别方法。

特别注意:以前识别这款验证码的时候,会得到识别距离【distance】,现在必须使用新参数【px_distance】


import base64
import requests
import datetime
from io import BytesIO
from PIL import Image, ImageDraw, ImageFont



#PIL图片保存为base64编码
def PIL_base64(img, coding='utf-8'):
    img_format = img.format
    if img_format == None:
        img_format = 'JPEG'

    format_str = 'JPEG'
    if 'png' == img_format.lower():
        format_str = 'PNG'
    if 'gif' == img_format.lower():
        format_str = 'gif'

    if img.mode == "P":
        img = img.convert('RGB')
    if img.mode == "RGBA":
        format_str = 'PNG'
        img_format = 'PNG'

    output_buffer = BytesIO()
    # img.save(output_buffer, format=format_str)
    img.save(output_buffer, quality=100, format=format_str)
    byte_data = output_buffer.getvalue()
    base64_str = 'data:image/' + img_format.lower() + ';base64,' + base64.b64encode(byte_data).decode(coding)
    # base64_str = base64.b64encode(byte_data).decode(coding)

    return base64_str

# 标记点
def mark(img, y):
    img = img.convert("RGB")
    draw = ImageDraw.Draw(img)
    draw.line((y, 0, y, img.size[1]), fill=(255, 0, 0), width=2)  # 线的起点和终点,线宽
    return img

# 加载图片
img1 = Image.open(r'E:\Python\lixin_project\OpenAPI接口测试\test_img\61-1.jpg')
# 图片转base64
img1_base64 = PIL_base64(img1)
img2 = Image.open(r'E:\Python\lixin_project\OpenAPI接口测试\test_img\61-2.png')
# 图片转base64
img2_base64 = PIL_base64(img2)

# 验证码识别接口
url = "http://bq1gpmr8.xiaomy.net/openapi/verify_code_identify/"
data = {
    # 用户的key
    "key": "nHaAjgg5q1znn6tRUstY",
    # 验证码类型
    "verify_idf_id": "61",
    # 点击区大图
    "img1": img1_base64,
    # 点击顺序小图
    "img2": img2_base64,
}
header = {"Content-Type": "application/json"}

t1 = datetime.datetime.now()
# 发送请求调用接口
response = requests.post(url=url, json=data, headers=header)

# 获取响应数据,识别结果
print(response.text)
print("耗时:", datetime.datetime.now() - t1)

img1.paste(img2, (0, 0))
img1 = mark(img1, response.json()['data']['px_distance'])
img1.show()

四、变速滑动对齐

之前我一直以为只有通过js逆向,每次分析获取新的滑动速度方程才行,下面我介绍一个能解决所有类似问题的通用方法。使用 DrissionPage 来解决这个问题。

需要修改 DrissionPage 的源代码,修改之前大家一定要先备份一下。

1、修改 ChromiumElement 类

大家先找到【chromium_element.py】,这个文件在【虚拟环境文件夹\Lib\site-packages\DrissionPage\_elements】目录下。

这里需要给 ChromiumElement 类新增两个函数

    # 得塔云自定义滑动
    def detayun_drag(self, jk_ele, stop_distance, offset_x=0, offset_y=0, duration=.5):
        curr_x, curr_y = self.rect.midpoint
        offset_x += curr_x
        offset_y += curr_y
        self.detayun_drag_to((offset_x, offset_y), jk_ele, stop_distance, duration)
        return self
    
    # 得塔云自定义滑动
    def detayun_drag_to(self, ele_or_loc, jk_ele, stop_distance, duration=.5):
        if isinstance(ele_or_loc, ChromiumElement):
            ele_or_loc = ele_or_loc.rect.midpoint
        elif not isinstance(ele_or_loc, (list, tuple)):
            raise ValueError(_S._lang.join(_S._lang.INCORRECT_TYPE_, 'ele_or_loc',
                                           ALLOW_TYPE=_S._lang.ELE_LOC_FORMAT, CURR_VAL=ele_or_loc))
        self.owner.actions.hold(self).detayun_move_to(ele_or_loc, duration=duration, jk_ele=jk_ele, stop_distance=stop_distance).release()
        return self

2、修改 Actions 类

大家先找到【actions.py】,这个文件在【虚拟环境文件夹\Lib\site-packages\DrissionPage\_units】目录下。

这里需要给 Actions 类新增两个函数

    # 得塔云自定义滑动
    def detayun_move_to(self, ele_or_loc, jk_ele, stop_distance, offset_x=None, offset_y=None, duration=.5):
        is_loc = False
        mid_point = offset_x == offset_y is None
        if offset_x is None:
            offset_x = 0
        if offset_y is None:
            offset_y = 0
        if isinstance(ele_or_loc, (tuple, list)):
            is_loc = True
            lx = ele_or_loc[0] + offset_x
            ly = ele_or_loc[1] + offset_y
        elif isinstance(ele_or_loc, str) or ele_or_loc._type == 'ChromiumElement':
            ele_or_loc = self.owner(ele_or_loc)
            self.owner.scroll.to_see(ele_or_loc)
            x, y = ele_or_loc.rect.midpoint if mid_point else ele_or_loc.rect.location
            lx = x + offset_x
            ly = y + offset_y
        else:
            raise ValueError(_S._lang.join(_S._lang.INCORRECT_TYPE_, 'ele_or_loc',
                                           ALLOW_TYPE=_S._lang.ELE_LOC_FORMAT, CURR_VAL=ele_or_loc))

        if not location_in_viewport(self.owner, lx, ly):
            # 把坐标滚动到页面中间
            clientWidth = self.owner._run_js('return document.body.clientWidth;')
            clientHeight = self.owner._run_js('return document.body.clientHeight;')
            self.owner.scroll.to_location(lx - clientWidth // 2, ly - clientHeight // 2)

        # 这样设计为了应付那些不随滚动条滚动的元素
        if is_loc:
            cx, cy = location_to_client(self.owner, lx, ly)
        else:
            x, y = ele_or_loc.rect.viewport_midpoint if mid_point else ele_or_loc.rect.viewport_location
            cx = x + offset_x
            cy = y + offset_y

        ox = cx - self.curr_x
        oy = cy - self.curr_y
        self.detayun_move(jk_ele, stop_distance, ox, oy, duration)
        return self

    # 得塔云自定义滑动
    def detayun_move(self, jk_ele, stop_distance, offset_x=0, offset_y=0, duration=.5):
        import re
        duration = .02 if duration < .02 else duration
        # num = int(duration * 50)
        num = int(offset_x)

        points = [(self.curr_x + i * (offset_x / num),
                   self.curr_y + i * (offset_y / num)) for i in range(1, num)]
        points.append((self.curr_x + offset_x, self.curr_y + offset_y))
        # 记录开始位置
        x0 = points[0][0]
        # 记录偏差
        last_d_x = 100000
        
        for x, y in points:
            t = perf_counter()
            self.curr_x = x
            self.curr_y = y
            self._dr.run('Input.dispatchMouseEvent', type='mouseMoved', button=self._holding,
                         x=self.curr_x, y=self.curr_y, modifiers=self.modifier)
            ss = .02 - perf_counter() + t
            if ss > 0:
                sleep(ss)

            # 输出监控对象的left属性
            style = jk_ele.attr('style')
            if 'left' in style:
                # 在属性中提取left的像素值
                left_px = re.findall('left:(.+?)px;', style)[0].strip()
                # 把字符串转换成数值
                left_px = float(left_px)
            elif 'transform' in style:
                # 在属性中提取left的像素值
                left_px = re.findall('transform: translate3d[(](.+?)px, 0px, 0px[)];', style)[0].strip()
                # 把字符串转换成数值
                left_px = float(left_px)

            # 判断是否滑到停止位置
            if left_px >= stop_distance:
                if abs(left_px - stop_distance) < last_d_x:
                    return self
                elif abs(left_px - stop_distance) > last_d_x:
                    self._dr.run('Input.dispatchMouseEvent', type='mouseMoved', button=self._holding,
                                 x=self.curr_x - 1, y=self.curr_y, modifiers=self.modifier)
                    return self
                else:
                    return self
            
            # 记录本次偏差
            last_d_x = abs(left_px - stop_distance)
            
        return self

五、DrissionPage的实战代码

import re
import time
import base64
import requests
from io import BytesIO
from PIL import Image, ImageDraw, ImageFont
from DrissionPage import ChromiumPage
from DrissionPage import ChromiumOptions

CHROME_PATH = r'C:\Users\Administrator\AppData\Local\Google\Chrome\Application\chrome.exe'
# 初始化配置
co = ChromiumOptions().set_browser_path(CHROME_PATH)
page = ChromiumPage(co)

# 用户的key
key = 'nHaAjgg5q1znn6tRUstY'
# 识别接口部分
def shibie(img1_base64, img2_base64):
    # 验证码识别接口
    url = "http://bq1gpmr8.xiaomy.net/openapi/verify_code_identify/"
    data = {
        # 用户的key
        "key": key,
        # 验证码类型
        "verify_idf_id": "61",
        # 背景大图
        "img1": img1_base64,
        # 滑动小图
        "img2": img2_base64,
    }
    header = {"Content-Type": "application/json"}

    # 发送请求调用接口
    response = requests.post(url=url, json=data, headers=header)
    # 判断是否正确请求
    if response.json()['code'] == 200:
        print('识别内容:', response.json())
        return response.json()['data']['px_distance']
    else:
        print('参数错误,请前往得塔云了解详情:http://bq1gpmr8.xiaomy.net/tool/verifyCodeHomePage2/?_=1714093687434')
        print('错误参数:', response.json())
        return None

for i in range(500):
    # 访问目标网站
    page.get('https://cfe.m.jd.com/privatedomain/risk_handler/03101900/?returnurl=https%3A%2F%2F2.zoppoz.workers.dev%3A443%2Fhttps%2Fitem.jd.com%2F100148991501.html&evtype=2&rpid=rp-188538371-10152-1742333003232')
    time.sleep(2)

    # 点击快速验证按钮
    name_input = page.ele('xpath://div[@class="verifyBtn"]')
    name_input.click()  # 执行点击操作
    time.sleep(1)
    print('点击了获取验证码')

    # 获取验证码
    # 定位目标图片元素(确保图片已加载)
    b_img_tag = page.ele('xpath://img[@id="main_img"]')
    img1_base64 = b_img_tag.attr('src')
    print('获取img1成功')

    s_img_tag = page.ele('xpath://img[@id="slot_img"]')
    img2_base64 = s_img_tag.attr('src')
    print('获取img2成功')

    # 识别图片,获得滑动距离
    true_sliding_distance = shibie(img1_base64, img2_base64)
    print('识别结果,真实滑动距离', true_sliding_distance)

    # 获取滑块元素
    element = page.ele('xpath://img[@class="move-img"]')
    # 获取监控元素
    jk_ele = page.ele('xpath://img[@id="slot_img"]')
    # 最大滑动距离
    max_distance = 242
    # 滑动滑块
    element.detayun_drag(jk_ele, stop_distance=true_sliding_distance, offset_x=max_distance, duration=1)

    time.sleep(5)

想了解更多验证码识别,请访问:得塔云

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

detayun

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值