123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227 |
- import random
- import re
- import cv2
- import requests
- import cv2 as cv
- import numpy as np
- from PIL import Image
- from playwright.sync_api import sync_playwright, Page
- from util.playwright_util import is_element_present
- class CaptchaIdent:
- """
- 处理灰豚登录的滑块验证码
- """
- def __init__(self, page):
- self.page = page
- self.frame = self.page.frames[1]
- def start(self):
- for i in range(10):
- self.page.wait_for_timeout(1000)
- start_x, start_y = self.get_slide_block_img_and_start()
- self.get_slide_bg_img()
- distance1, distance2 = self.get_slide_distance(start_x, start_y)
- slide_result = self.move_to_notch(distance1, distance2)
- if not slide_result:
- self.refresh_captcha()
- else:
- return
- raise Exception("滑块验证失败")
- def get_slide_bg_img(self):
- """截取滑动验证码背景图片"""
- if self.frame is not None:
- bg_ele = self.frame.query_selector('.tc-bg-img')
- bg_style = bg_ele.evaluate(
- "element => window.getComputedStyle(element).getPropertyValue('background-image')")
- bg_img = re.search(r'url\("([^"]+)"\)', bg_style).group(1)
- r = requests.get(bg_img)
- with open("./slide_bg.png", "wb") as f:
- f.write(r.content)
- # 调整图片大小,根据style内容将宽度调整为320,高度等比例调整
- img = Image.open("./slide_bg.png")
- bg_img = Image.open("./bg.png")
- ratio = img.width / bg_img.width
- img = img.resize(size=(bg_img.width, int(img.height / ratio)))
- img.save("./slide_bg.png")
- def get_slide_block_img_and_start(self):
- """获取滑块图片以及初始x坐标"""
- print("正在获取滑块图片")
- # 首先保存整个登录背景截图
- self.page.wait_for_timeout(2000)
- slideBg = self.frame.query_selector('#slideBg')
- slideBg.screenshot(path="bg.png")
- # 获取滑动验证码所在的iframe
- captcha_frame = self.frame
- # 获取滑块图片
- # .tc-fg-item对应的有三个元素,一个是目标滑块,一个是滑轨,还有一个是滑轨上的按钮
- for i in range(3):
- slide_block_ele = captcha_frame.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_y = float(top_value)
- # 通过滑块位置,从背景图中截取滑块图片 # cropped_image = image.crop((left, top, right, bottom))
- slide_block_rect = slide_block_ele.bounding_box()
- bg = Image.open("bg.png")
- # offset = slide_block_rect["width"] // 5 # 从背景图上截取会混入滑块周围的一些像素点,所以加一个偏移值,截取到滑块内部的图片。
- offset = 5
- slide_block_img = bg.crop((slide_block_x + offset, slide_block_y + offset,
- slide_block_x + slide_block_rect["width"] - offset,
- slide_block_y + slide_block_rect["height"] - offset))
- slide_block_img.save("slide_block.png")
- return slide_block_x + offset, slide_block_y + offset
- def get_slide_distance(self, start_x, slide_bg_y):
- """获取滑动距离"""
- print("正在获取滑动距离")
- # 通过opencv比较图片,获取缺口位置
- slide_bg_img = cv.imread("./slide_bg.png")
- slide_block_img = cv.imread("./slide_block.png")
- block_height = slide_block_img.shape[0]
- # 裁剪对应坐标图片,缩减搜索范围
- slide_bg_img = slide_bg_img[int(slide_bg_y) - 5: int(slide_bg_y + block_height) + 5, :]
- slide_bg_img = self.set_contrast_brightness(slide_bg_img, 1, 100)
- cv.imwrite("./slide_bg_handled.png", slide_bg_img)
- slide_block_img = self.set_contrast_brightness(slide_block_img, 0.4, 0)
- cv.imwrite("./slide_block_handled.png", slide_block_img)
- result = cv.matchTemplate(slide_block_img, slide_bg_img, cv.TM_CCOEFF_NORMED)
- minVal, maxVal, minLoc, maxLoc = cv.minMaxLoc(result)
- th, tw = slide_block_img.shape[:2]
- tl = maxLoc # 左上角点的坐标
- br = (tl[0] + tw, tl[1] + th) # 右下角点的坐标
- cv2.rectangle(slide_bg_img, tl, br, (0, 0, 255), 2) # 绘制矩形
- cv2.rectangle(slide_bg_img, minLoc, (minLoc[0] + tw, minLoc[1] + th), (0, 255, 255), 2) # 绘制矩形
- cv2.imwrite("./match_result.png", slide_bg_img) # 保存在本地
- # 距离
- distance1 = minLoc[0] - start_x
- distance2 = maxLoc[0] - start_x
- return distance1, distance2
- def get_slide_distance2(self, start_x):
- """获取滑动距离"""
- print("正在获取滑动距离")
- # 通过opencv比较图片,获取缺口位置
- slide_bg_img = cv2.imread("./slide_bg.png")
- slide_bg_img = cv2.Canny(slide_bg_img, 100, 200)
- cv2.imwrite("./slide_bg_handle1.png", slide_bg_img)
- slide_bg_img = cv2.cvtColor(slide_bg_img, cv2.COLOR_GRAY2RGB)
- cv2.imwrite("./slide_bg_handle2.png", slide_bg_img)
- slide_block_img = cv.imread("./slide_block.png")
- slide_block_img = cv2.Canny(slide_block_img, 100, 200)
- cv.imwrite("./slide_block_handled1.png", slide_block_img)
- slide_block_img = cv2.cvtColor(slide_block_img, cv2.COLOR_GRAY2RGB)
- cv2.imwrite("./slide_block_handled2.png", slide_block_img) # 保存在本地
- result = cv.matchTemplate(slide_block_img, slide_bg_img, cv.TM_CCOEFF_NORMED)
- minVal, maxVal, minLoc, maxLoc = cv.minMaxLoc(result)
- th, tw = slide_block_img.shape[:2]
- tl = maxLoc # 左上角点的坐标
- br = (tl[0] + tw, tl[1] + th) # 右下角点的坐标
- cv2.rectangle(slide_bg_img, tl, br, (0, 0, 255), 2) # 绘制矩形
- cv2.imwrite("./match_result.png", slide_bg_img) # 保存在本地
- # 返回缺口的X坐标
- return maxLoc[0] - start_x, minLoc[0] - start_x
- @staticmethod
- def set_contrast_brightness(frame, contrast_value, brightness_value):
- if not contrast_value:
- contrast_value = 0.0
- if not brightness_value:
- brightness_value = 0
- blank = np.zeros(frame.shape, frame.dtype)
- frame = cv.addWeighted(frame, contrast_value, blank, 1 - contrast_value, brightness_value)
- return frame
- @staticmethod
- def get_tracks(distance):
- """获取移动轨迹"""
- tracks = [] # 移动轨迹
- current = 0 # 当前位移
- mid = distance * 3/4 # 减速阈值
- t = 0.5 # 计算间隔
- v = 1 # 初始速度
- while current < distance:
- if current < mid:
- a = random.randint(5, 10) # 加速度为正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, distance1, distance2):
- """移动滑轨按钮到缺口处"""
- # 获取滑动验证码所在的iframe
- captcha_iframe = self.frame
- for i in range(2):
- # 获取按钮位置,将鼠标移到上方并按下
- 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()
- distance = [distance1, distance2][i]
- if distance <= 0: # 距离不可能小于等于0
- continue
- print(f"正在进行第{i + 1}次滑动,滑动距离{distance}")
- 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)
- # 寻找按钮是否还存在,不存在的话表明已通过滑动验证码,存在的话尝试下一个距离
- if not is_element_present(self.page, '.ant-modal-body'):
- print("滑动验证通过")
- return True
- return False
- def refresh_captcha(self):
- """刷新验证码"""
- # 获取滑动验证码所在的iframe
- print("刷新验证码")
- self.frame.query_selector('.tc-action-icon').click()
- self.page.wait_for_timeout(2500)
- self.frame = self.page.frames[1]
|