一、简介
京东的滑块验证码大家都很熟悉,最近京东对他进行了更新,变成了随机变速滑动的验证码。
什么是随机变速滑动验证码呢?为了好理解这个概念,先给大家讲一讲什么不变速滑动验证码。
不变速滑动验证码:鼠标拖动验证码滑块,向右拖动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)
想了解更多验证码识别,请访问:得塔云