目录
梳理思路
训练模型
编写代码
总结与提高
源码下载
在上一节,我们通过opencv-python+playwright成功过掉了QQ空间的滑动验证码。在本节,我们将使用yolov5+playwright来实现相同效果。
注:因为yolov5的配置教程网上已经很多了,笔者这里就不再赘述。
梳理思路
1. 使用playwright打开浏览器,访问qq空间登录页面。
2. 点击密码登录。
3. 输入账号密码并点击登录。
4. 出现滑动验证码图片后,我们就可以获取到验证码背景图以及滑块图片。验证码背景图片通过元素style中的url链接就可以获取到,由于下载保存的是原图,所以我们要将宽度调整为280px,280这个值同样也可以在style中看到。
注:从style中也可以看到height值为200px,但其实这个包含了下方滑轨的高度,因此图片的真实高度要小于200px。所以我们在调整原图大小时,高度不要设为200px,而是通过以下公式进行等比缩放。
调整后的高度 = 原图高/(原图宽/280)
5. 我们同样可以找到滑块图片的链接,但打开后却是这样的。
由于不知道滑块在这张大图上的位置,所以无法有效截取。另一种方案是直接通过屏幕截取获得滑块图片。首先,对全屏幕进行截图,然后用playwright获取到滑块元素,获取到该元素的位置和大小后,就可以截取了。滑块的起始位置就是style中的left值。
6. 验证码背景图有了,滑块有了,滑块初始位置也有了,接下来就是判断背景图的缺口位置,再求出滑动距离。我们可以通过yolov5识别缺口位置获取缺口的x坐标。拿到缺口x坐标后减去滑块的x坐标就可以求出滑动距离了。
7. 位置拿到之后,就是用鼠标控制滑轨上的按钮并进行滑动操作。滑动操作要真实,不能匀速,滑动时鼠标肯定也会有上下抖动,总之要尽量模拟人的滑动操作。
8. 滑动成功的话下方的滑轨元素就会消失,我们可以通过这点来判断是否通过了滑动验证码。如果没有通过(小概率),则刷新验证码并再次执行步骤4,5,6,7,8。
注:跟上一小节相比,区别仅仅在第6和第8点,用opencv-python会获取到两个可能的缺口位置,而且可能要滑动两次。用yolov5的话,缺口识别概率大大提高,所以我们只需要滑动一次。
训练模型
1. 需要一些图片用作yolov5的训练集,我们可以通过以下程序下载一定数量的QQ空间滑动验证码背景图片(在本教程中,笔者一共使用了151张图片)。
# get_bg.py
"""获取点选验证码背景,用于yolov5训练"""
from playwright.sync_api import sync_playwright
from PIL import Image
import requests
import re
import os
class QQZonSlide:
def __init__(self):
self.login_url = "https://i.qq.com/"
self.username = "你的账号"
self.password = "你的密码"
self.page = None
def start(self):
with sync_playwright() as p:
self.init_page(p)
self.login()
if not os.path.exists("./images"):
os.mkdir("./images")
for i in range(0, 151):
self.get_slide_bg_img(i)
self.refresh_captcha()
def init_page(self, p):
"""初始化浏览器,获取page对象"""
browser = p.chromium.launch(headless=False)
self.page = browser.new_page()
def login(self):
"""通过账号密码登录"""
print("开始登录")
# 访问页面
self.page.goto(self.login_url)
# 定位到登录框元素并点击密码登录
login_frame = self.page.frame_locator("#login_frame")
login_frame.get_by_role("link", name="密码登录").click()
# 清空账号框然后输入账号
login_frame.locator("#u").clear()
login_frame.locator("#u").fill(self.username)
# 清空密码框然后输入密码
login_frame.locator("#p").clear()
login_frame.locator("#p").fill(self.password)
# 点击登录按钮
self.page.wait_for_timeout(1000)
login_frame.locator("#login_button").click()
def get_slide_bg_img(self, index):
"""截取滑动验证码背景图片"""
self.page.wait_for_timeout(2000)
print(f"正在获取第{index}张滑动验证码背景图片")
# 获取滑动验证码所在的iframe
captcha_iframe = self.page.frame_locator("#login_frame").frame_locator("#tcaptcha_iframe_dy")
# 获取滑动验证码的背景图
slide_bg_style = captcha_iframe.locator("#slideBg").get_attribute("style")
slide_bg_url = re.search(r'url\("(.+)"\)', slide_bg_style).groups()[0]
r = requests.get(slide_bg_url)
with open(f"./images/{index}.png", "wb") as f:
f.write(r.content)
# 调整图片大小,根据style内容将宽度调整为280,高度等比例调整
img = Image.open(f"./images/{index}.png")
ratio = img.width / 280
img = img.resize(size=(280, int(img.height/ratio)))
img.save(f"./images/{index}.png")
def refresh_captcha(self):
"""刷新验证码"""
print("刷新验证码")
# 找到刷新按钮并点击
captcha_iframe = self.page.frame_locator("#login_frame").frame_locator("#tcaptcha_iframe_dy")
try:
captcha_iframe.locator("//div[@class='tcaptcha-embed']/div[4]").click(timeout=1000)
except:
captcha_iframe.locator("//div[@class='tcaptcha-embed']/div[5]").click(timeout=1000)
self.page.wait_for_timeout(1000)
if __name__ == "__main__":
slide = QQZonSlide()
slide.start()
2. 使用labelimg进行标注,笔者把标签名称都设置成了“0”。
3. 整理数据,新建images和labels文件夹并将图片和标注结果放在对应文件夹中。
├─images # 图片
│ ├─train # 训练集图片
│ └─val # 验证集图片
└─labels # 标签(txt)
├─train # 训练集标签
└─val # 验证集标签
4. 在yolov5/data路径下新建slide.yaml文件,输入以下内容。
# Train/val/test sets as 1) dir: path/to/imgs, 2) file: path/to/imgs.txt, or 3) list: [path/to/imgs1, path/to/imgs2, ..]
path: C:/Users/louis/Desktop/QQZone/data # dataset root dir
train: images/train # train images (relative to 'path') 118287 images
val: images/val # val images (relative to 'path') 5000 images
test:
# Classes
names: ['0']
5. 下载训练权重文件,在本教程中我们使用yolov5s.pt。
6. 使用以下命令训练数据(先cd到yolov5文件夹下),训练结果还是可以的。
python train.py --weights yolov5s.pt --batch-size=3 --data=./data/slide.yaml
我们将生成的best.pt模型文件从yolov5/runs/train/exp/weights路径移动到yolov5文件夹下(方便后面输入命令)。
7. 使用以下命令验证下images/val文件夹中的图片(先cd到yolov5文件夹下)。
python detect.py --weights best.pt --source ../data/images/val --conf-thres 0.7 --save-txt
验证集所有图片的缺口位置都被正确识别出来了。
编写代码
根据以上思路,我们可以编写出如下代码。
# main.py
from playwright.sync_api import sync_playwright
from PIL import Image
import requests
import random
import shutil
import re
import os
class QQZonSlide:
def __init__(self):
self.login_url = "https://i.qq.com/"
self.username = "你的账号"
self.password = "你的密码"
self.page = None
def start(self):
with sync_playwright() as p:
self.init_page(p)
self.login()
while True:
self.get_slide_bg_img()
start_x = self.get_slide_block_img_and_start_x()
distance = self.get_slide_distance(start_x)
slide_result = self.move_to_notch(distance)
if not slide_result:
self.refresh_captcha()
else:
break
def init_page(self, p):
"""初始化浏览器,获取page对象"""
browser = p.chromium.launch(headless=False)
self.page = browser.new_page()
def login(self):
"""通过账号密码登录"""
print("开始登录")
# 访问页面
self.page.goto(self.login_url)
# 定位到登录框元素并点击密码登录
login_frame = self.page.frame_locator("#login_frame")
login_frame.get_by_role("link", name="密码登录").click()
# 清空账号框然后输入账号
login_frame.locator("#u").clear()
login_frame.locator("#u").fill(self.username)
# 清空密码框然后输入密码
login_frame.locator("#p").clear()
login_frame.locator("#p").fill(self.password)
# 点击登录按钮
self.page.wait_for_timeout(1000)
login_frame.locator("#login_button").click()
def get_slide_bg_img(self):
"""截取滑动验证码背景图片"""
self.page.wait_for_timeout(2000)
print("正在获取滑动验证码背景图片")
# 获取滑动验证码所在的iframe
captcha_iframe = self.page.frame_locator("#login_frame").frame_locator("#tcaptcha_iframe_dy")
# 获取滑动验证码的背景图
slide_bg_style = captcha_iframe.locator("#slideBg").get_attribute("style")
slide_bg_url = re.search(r'url\("(.+)"\)', slide_bg_style).groups()[0]
r = requests.get(slide_bg_url)
with open("./slide_bg.png", "wb") as f:
f.write(r.content)
# 调整图片大小,根据style内容将宽度调整为280,高度等比例调整
img = Image.open("./slide_bg.png")
ratio = img.width / 280
img = img.resize(size=(280, int(img.height/ratio)))
img.save("./slide_bg.png")
def get_slide_block_img_and_start_x(self):
"""获取滑块图片以及初始x坐标"""
print("正在获取滑块图片")
# 首先保存整个登录背景截图
self.page.screenshot(path="bg.png")
# 获取滑动验证码所在的iframe
captcha_iframe = self.page.frame_locator("#login_frame").frame_locator("#tcaptcha_iframe_dy")
# 获取滑块图片
# .tc-fg-item对应的有三个元素,一个是目标滑块,一个是滑轨,还有一个是滑轨上的按钮
for i in range(3):
slide_block_ele = captcha_iframe.locator(".tc-fg-item").nth(i)
slide_block_style = slide_block_ele.get_attribute("style")
# 滑轨按钮元素的style值中不包含url字符串
if "url" not in slide_block_style:
continue
# 从元素的style值中分析得出只有目标滑块的top值小于150
top_value = re.search(r'top: (.+)px;', slide_block_style).groups()[0]
if float(top_value) > 150:
continue
# 获取x坐标
slide_block_x = float(re.search(r'left: (.+)px; top: ', slide_block_style).groups()[0])
# 通过滑块位置,从背景图中截取滑块图片
slide_block_rect = slide_block_ele.bounding_box()
bg = Image.open("./bg.png")
offset = slide_block_rect["width"] // 4 # 从背景图上截取会混入滑块周围的一些像素点,所以加一个偏移值,截取到滑块内部的图片。
slide_block_img = bg.crop((slide_block_rect["x"] + offset, slide_block_rect["y"] + offset,
slide_block_rect["x"] + slide_block_rect["width"] - offset,
slide_block_rect["y"] + slide_block_rect["height"] - offset))
slide_block_img.save("slide_block.png")
return slide_block_x + slide_block_rect["width"] // 4
def get_slide_distance(self, start_x):
"""获取滑动距离"""
print("正在获取滑动距离")
# 用yolov5获取到缺口位置,位置坐标保存在yolov5/runs/detect/exp/labels文件夹下
if os.path.exists("./yolov5/runs/detect"):
shutil.rmtree("./yolov5/runs/detect")
os.system("python ./yolov5/detect.py --weights ./yolov5/best.pt --source ./slide_bg.png --conf-thres 0.7 --save-txt")
# 将归一化的坐标还原成真实坐标,得出缺口的左侧的x坐标
# yolov5有可能会定位多个框(跟训练精度有关),那txt中也就会有多行结果,我们只选择第一行
# 由于阈值设置成0.7,可能会导致没有结果,所以不会生成slide_bg.txt。在这种情况下,我们就随便填一个距离
try:
with open("./yolov5/runs/detect/exp/labels/slide_bg.txt", "r", encoding="utf-8") as f:
result = f.readlines()[0]
except FileNotFoundError:
distance = 50
else:
notch_center_x = float(result.split(" ")[1]) * 280
notch_width = float(result.split(" ")[3]) * 280
notch_left_x = round(notch_center_x - notch_width/2, 2)
# 减去滑块的x坐标,求出距离
distance = notch_left_x - start_x
print(f"距离为{distance}")
return distance
@staticmethod
def get_tracks(distance):
"""获取移动轨迹"""
tracks = [] # 移动轨迹
current = 0 # 当前位移
mid = distance * 4 / 5 # 减速阈值
t = 0.2 # 计算间隔
v = 0 # 初始速度
while current < distance:
if current < mid:
a = random.randint(3, 5) # 加速度为正5
else:
a = random.randint(-5, -3) # 加速度为负3
v0 = v # 初速度 v0
v = v0 + a * t # 当前速度
move = v0 * t + 1 / 2 * a * t * t # 移动距离
current += move
tracks.append(round(current))
return tracks
def move_to_notch(self, distance):
"""移动滑轨按钮到缺口处"""
# 获取滑动验证码所在的iframe
captcha_iframe = self.page.frame_locator("#login_frame").frame_locator("#tcaptcha_iframe_dy")
# 获取按钮位置,将鼠标移到上方并按下
slider_btn_rect = captcha_iframe.get_by_alt_text("slider").bounding_box()
self.page.mouse.move(slider_btn_rect['x'], slider_btn_rect['y'])
self.page.mouse.down()
print(f"正在滑动")
tracks = self.get_tracks(distance)
for x in tracks:
self.page.mouse.move(slider_btn_rect['x']+x, random.randint(-5, 5)+slider_btn_rect['y'])
self.page.mouse.move(slider_btn_rect['x'] + tracks[-1] + 5, random.randint(-5, 5) + slider_btn_rect['y'])
self.page.mouse.move(slider_btn_rect['x'] + tracks[-1] - 5, random.randint(-5, 5) + slider_btn_rect['y'])
self.page.mouse.up()
# 滑动结束后等待一段时间
self.page.wait_for_timeout(2000)
# 寻找按钮是否还存在,不存在的话表明已通过滑动验证码,存在的话尝试下一个距离
try:
captcha_iframe.get_by_alt_text("slider").wait_for(timeout=2000)
except Exception as e:
print("已通过滑动验证码")
return True
else:
print(f"滑动失败")
return False
def refresh_captcha(self):
"""刷新验证码"""
print("刷新验证码")
# 找到刷新按钮并点击
captcha_iframe = self.page.frame_locator("#login_frame").frame_locator("#tcaptcha_iframe_dy")
captcha_iframe.locator("#e_reload").click()
self.page.wait_for_timeout(2000)
if __name__ == "__main__":
slide = QQZonSlide()
slide.start()
运行视频如下:
用yolov5+playwright过滑动验证码
在一次滑动成功后,QQ空间并没有直接显示验证通过,而是出现了新的滑动验证码,第二次滑动成功后才跳转到短信验证码界面。所以两次滑动都是没问题的,得到的距离值也没有问题。
总结与提高
用yolov5识别缺口的准确率很高,但是前提是训练集中的图片质量要高,种类要多,本节训练的模型不足点就在图片的种类不多。笔者此次下载的图片主要是这三种:
而QQ空间可能会出现这种的,这就有可能产生识别误差:
另外,用于训练的图片数量也不多,这也是可以提高的一个地方。但是总的来看,此次滑动验证码的通过的效果还是令人满意的。
源码下载
笔者把yolov5的源码已经放入,使用项目前先记得安装yolov5/requirements.txt中的库。
链接:https://pan.baidu.com/s/1ZgXLmG3Cs_xZ5MVJmml2VQ
提取码:wbp1