用yolov5+playwright过滑动验证码

news2024/11/15 13:58:10

目录

梳理思路

训练模型

编写代码

总结与提高

源码下载


在上一节,我们通过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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/447382.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

C++初阶之函数重载

目录 前言 函数重载 1.函数重载的概念 2.C支持函数重载的原理--名字修饰(name Mangling) 前言 今天小编给大家带来的是C中关于函数重载的内容&#xff0c;和C语言不一样&#xff0c;函数重载是C语言特有的&#xff0c;那么该功能实现的底层原理是什么呢&#xff1f;请大家…

Idea配置maven,指定settings.xml文件不生效

一.简介 最近单位要求把项目的仓库配置从阿里云改为nexus私服&#xff0c;配置了一个settings-nexus.xml的配置文件&#xff0c;idea的maven配置指定了该settings-nexus.xml文件&#xff0c;发现走的还是阿里云的&#xff0c;新的settings-nexus.xml竟然不生效&#xff0c;依赖…

分支和循环语句(1)

文章目录 目录1. 什么是语句2. 分支语句&#xff08;选择结构&#xff09;2.1 if语句2.1.1 悬空else2.1.2 if书写形式的对比2.1.3 练习 2.2 switch语句2.2.1 在switch语句中的 break2.2.2 default子句2.2.3 练习 3. 循环语句3.1 while循环3.1.1 while语句中的break和continue 附…

记一次fastjson反序列化到内网靶标

声明&#xff1a;文中涉及到的技术和工具&#xff0c;仅供学习使用&#xff0c;禁止从事任何非法活动&#xff0c;如因此造成的直接或间接损失&#xff0c;均由使用者自行承担责任。 点点关注不迷路&#xff0c;每周不定时持续分享各种干货。 众亦信安&#xff0c;中意你啊&a…

多种方法解决This is usually caused by another repository pushing to the same ref的错误

文章目录 1. 复现错误2. 分析错误3. 解决错误4. 解决该错误的其他方法 1. 复现错误 今天使用git status查看文件状态&#xff0c;发现有一个文件未提交&#xff0c;如下代码所示&#xff1a; D:\project\test>git status On branch master Your branch is up to date with …

sftp常用命令介绍

sftp常用命令&#xff1a; 1. sftp 登录sftp服务器 sftp userip ​​​​​​ 如需要看全部命令&#xff1a;则使用help即可 2. pwd和lpwd 、 ls和lls 、cd和lcd 等 sftp登录之后默认操作是远程服务器&#xff0c;当需要操作本地时&#xff0c;就需要在前边加“l”&#…

【wpf踩坑日记】搞错了,眼睛问题(:))

背景 今天遇到一个草鸡奇葩的问题&#xff1a; ComboBox 选择时 没有触发绑定的属性的set。 其实看错了&#xff0c;Mode写出OneWay&#xff0c;应该是TowWay。 不然是会触发set的。兄弟们不用往下看了。。。。。 哎&#xff0c;有的时候就会碰到这种情况&#xff0c;我还…

Ubuntu上跑通PaddleOCR

书接上文。刚才说到我已经在NUC8里灌上了Windows Server 2019。接下来也顺利的启用了Hyper-V角色并装好了一台Ubuntu 22.04 LTS 的虚机。由于自从上回在树莓派上跑通了Paddle-Lite-Demo之后想再研究一下PaddleOCR但进展不顺&#xff0c;因此决定先不折腾了&#xff0c;还是从x6…

python常见问题总结

对于长期深耕在python爬虫的程序员来说&#xff0c;如何快速解决代码中的问题它是作为合格的程序员应该具备的基本素质。下面将我总结整理出有关python的一些常见问题记录下来方便后期查证。 Python python 没有多态&#xff0c;而是鸭子类型 多继承&#xff0c;没有接口&…

【操作系统】CPU 缓存一致性

【操作系统】CPU 缓存一致性、MESI 协议 参考资料&#xff1a; CPU缓存一致性协议(MESI) 【JUC】Java并发机制的底层实现原理 CPU 缓存一致性 文章目录 【操作系统】CPU 缓存一致性、MESI 协议CPU Cache 的数据写入写直达写回 缓存一致性问题总线嗅探MESI 协议总结 CPU Cache …

springboot:缓存不止redis,学会使用本地缓存ehcache

0. 引言 随着redis的普及&#xff0c;更多的同学对redis分布式缓存更加熟悉&#xff0c;但在一些实际场景中&#xff0c;其实并不需要用到redis&#xff0c;使用更加简单的本地缓存即可实现我们的缓存需求。 今天&#xff0c;我们一起来看看本地缓存组件ehcache 1. ehcache简…

python+vue 市政工程资源互助平台的设计与实现

该系统将由用户系统&#xff0c;管理员系统两部分组成。用户有个人和vip两种类型。 用户模块包括登录注册功能&#xff0c;登录字段包括用户名&#xff0c;密码&#xff0c;用户身份以及验证码。注册包括用户名&#xff0c;密码&#xff0c;邮箱&#xff0c;电话号码&#xff0…

vite+vue+element-plus完成一个admin管理后台

整体项目的 访问链接&#xff1a;https://bigmiss.top/demo/index.html 用到的技术整理 名称版本安装命令说明vite4.0.0npm init vitelatest构建Vue项目vue3.2.45npm install vuenext渐进式框架&#xff08;在vite已安装&#xff09;element-plus3.7.5npm install element-plu…

C#调用C++封装的SDK库(dll动态库)——下

C#调用C封装的SDK库(dll动态库)——下 一、说明 上一篇我们相当于封装的是C语言风格的动态dll库&#xff0c;供C#来调用的。 C#调用C封装的SDK库(dll动态库)——上 如果我们要封装的是下面的类呢&#xff1f;我们该怎么办&#xff1f;大家先思考下。 class Calculation { p…

Tomcat常用操作

Tomcat时间长不用&#xff0c;居然不会用了&#xff0c;这里用的Tomcat9.0.74&#xff0c;对应的jdk版本是jdk8与jdk11。 先看他的把Tomcat安装上去&#xff1a;Tomcat9的安装 运行与关闭Tomcat winr输入cmd。在运行窗口输入startup.bat&#xff0c;回车&#xff0c;启动Tom…

Java基础:对象的克隆(复制)

假如想复制一个简单变量。很简单&#xff1a; int apples 5; int pears apples; 不仅int类型&#xff0c;其它七种原始数据类型(boolean,char,byte,short,float,double.long)同样适用于该类情况。 但是如果你复制的是一个对象&#xff0c;情况就复杂了。 假设说我是一个b…

webpack----开发服务器

文章目录 devServer抽取csscss的兼容性压缩cssjs语法检查js的兼容性 devServer 每次编辑源码后&#xff0c;都要webpack重新打包&#xff0c;才能看到效果&#xff0c;麻烦&#xff01;使用webpack-dev-server 自动打包编译源码配置 // webpack.config.js ... mode: "de…

shell中的for循环和if判断

一.编写脚本for1.sh,使用for循环创建20账户&#xff0c;账户名前缀由用户从键盘输入&#xff0c;账户初始密码由用户输入&#xff0c;例如: test1、test2、test3、.....、 test10 1.创建脚本for1.sh [rootserver ~]# vim for1.sh 2.编写脚本for1.sh 3.执行脚本for1.sh [roo…

linux命令----- mkdir与rmdir

创建与删除目录 一 mkdir1.mkdir 目录名2.mkdir -p 目录一/目录二 二 rmdir1.rmdir 目录名2.删除非空目录时失败3. rmkdir -p 目录1/目录2 一 mkdir mkdir是make directories的缩写&#xff0c;主要用于linux中创建目录 创建的目录不能和同级目录中已经存在的目录重名可以mkd…

【产品经理】系统上线自查清单

产品上线之前的准备工作&#xff0c;看起来简单&#xff0c;实际做起来是非常繁杂的&#xff0c;如果没有尽早考虑和准备&#xff0c;可能会手忙脚乱甚至导致产品延迟上线。 产品上线前的准备工作听起来简单&#xff0c;但实际做起来非常繁杂。除了要考虑用户需求、商业需求外&…