captcha_ident.py 9.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227
  1. import random
  2. import re
  3. import cv2
  4. import requests
  5. import cv2 as cv
  6. import numpy as np
  7. from PIL import Image
  8. from playwright.sync_api import sync_playwright, Page
  9. from util.playwright_util import is_element_present
  10. class CaptchaIdent:
  11. """
  12. 处理灰豚登录的滑块验证码
  13. """
  14. def __init__(self, page):
  15. self.page = page
  16. self.frame = self.page.frames[1]
  17. def start(self):
  18. for i in range(10):
  19. self.page.wait_for_timeout(1000)
  20. start_x, start_y = self.get_slide_block_img_and_start()
  21. self.get_slide_bg_img()
  22. distance1, distance2 = self.get_slide_distance(start_x, start_y)
  23. slide_result = self.move_to_notch(distance1, distance2)
  24. if not slide_result:
  25. self.refresh_captcha()
  26. else:
  27. return
  28. raise Exception("滑块验证失败")
  29. def get_slide_bg_img(self):
  30. """截取滑动验证码背景图片"""
  31. if self.frame is not None:
  32. bg_ele = self.frame.query_selector('.tc-bg-img')
  33. bg_style = bg_ele.evaluate(
  34. "element => window.getComputedStyle(element).getPropertyValue('background-image')")
  35. bg_img = re.search(r'url\("([^"]+)"\)', bg_style).group(1)
  36. r = requests.get(bg_img)
  37. with open("./slide_bg.png", "wb") as f:
  38. f.write(r.content)
  39. # 调整图片大小,根据style内容将宽度调整为320,高度等比例调整
  40. img = Image.open("./slide_bg.png")
  41. bg_img = Image.open("./bg.png")
  42. ratio = img.width / bg_img.width
  43. img = img.resize(size=(bg_img.width, int(img.height / ratio)))
  44. img.save("./slide_bg.png")
  45. def get_slide_block_img_and_start(self):
  46. """获取滑块图片以及初始x坐标"""
  47. print("正在获取滑块图片")
  48. # 首先保存整个登录背景截图
  49. self.page.wait_for_timeout(2000)
  50. slideBg = self.frame.query_selector('#slideBg')
  51. slideBg.screenshot(path="bg.png")
  52. # 获取滑动验证码所在的iframe
  53. captcha_frame = self.frame
  54. # 获取滑块图片
  55. # .tc-fg-item对应的有三个元素,一个是目标滑块,一个是滑轨,还有一个是滑轨上的按钮
  56. for i in range(3):
  57. slide_block_ele = captcha_frame.locator(".tc-fg-item").nth(i)
  58. slide_block_style = slide_block_ele.get_attribute("style")
  59. # 滑轨按钮元素的style值中不包含url字符串
  60. if "url" not in slide_block_style:
  61. continue
  62. # 从元素的style值中分析得出只有目标滑块的top值小于150
  63. top_value = re.search(r'top: (.+)px;', slide_block_style).groups()[0]
  64. if float(top_value) > 150:
  65. continue
  66. # 获取x坐标
  67. slide_block_x = float(re.search(r'left: (.+)px; top: ', slide_block_style).groups()[0])
  68. slide_block_y = float(top_value)
  69. # 通过滑块位置,从背景图中截取滑块图片 # cropped_image = image.crop((left, top, right, bottom))
  70. slide_block_rect = slide_block_ele.bounding_box()
  71. bg = Image.open("bg.png")
  72. # offset = slide_block_rect["width"] // 5 # 从背景图上截取会混入滑块周围的一些像素点,所以加一个偏移值,截取到滑块内部的图片。
  73. offset = 5
  74. slide_block_img = bg.crop((slide_block_x + offset, slide_block_y + offset,
  75. slide_block_x + slide_block_rect["width"] - offset,
  76. slide_block_y + slide_block_rect["height"] - offset))
  77. slide_block_img.save("slide_block.png")
  78. return slide_block_x + offset, slide_block_y + offset
  79. def get_slide_distance(self, start_x, slide_bg_y):
  80. """获取滑动距离"""
  81. print("正在获取滑动距离")
  82. # 通过opencv比较图片,获取缺口位置
  83. slide_bg_img = cv.imread("./slide_bg.png")
  84. slide_block_img = cv.imread("./slide_block.png")
  85. block_height = slide_block_img.shape[0]
  86. # 裁剪对应坐标图片,缩减搜索范围
  87. slide_bg_img = slide_bg_img[int(slide_bg_y) - 5: int(slide_bg_y + block_height) + 5, :]
  88. slide_bg_img = self.set_contrast_brightness(slide_bg_img, 1, 100)
  89. cv.imwrite("./slide_bg_handled.png", slide_bg_img)
  90. slide_block_img = self.set_contrast_brightness(slide_block_img, 0.4, 0)
  91. cv.imwrite("./slide_block_handled.png", slide_block_img)
  92. result = cv.matchTemplate(slide_block_img, slide_bg_img, cv.TM_CCOEFF_NORMED)
  93. minVal, maxVal, minLoc, maxLoc = cv.minMaxLoc(result)
  94. th, tw = slide_block_img.shape[:2]
  95. tl = maxLoc # 左上角点的坐标
  96. br = (tl[0] + tw, tl[1] + th) # 右下角点的坐标
  97. cv2.rectangle(slide_bg_img, tl, br, (0, 0, 255), 2) # 绘制矩形
  98. cv2.rectangle(slide_bg_img, minLoc, (minLoc[0] + tw, minLoc[1] + th), (0, 255, 255), 2) # 绘制矩形
  99. cv2.imwrite("./match_result.png", slide_bg_img) # 保存在本地
  100. # 距离
  101. distance1 = minLoc[0] - start_x
  102. distance2 = maxLoc[0] - start_x
  103. return distance1, distance2
  104. def get_slide_distance2(self, start_x):
  105. """获取滑动距离"""
  106. print("正在获取滑动距离")
  107. # 通过opencv比较图片,获取缺口位置
  108. slide_bg_img = cv2.imread("./slide_bg.png")
  109. slide_bg_img = cv2.Canny(slide_bg_img, 100, 200)
  110. cv2.imwrite("./slide_bg_handle1.png", slide_bg_img)
  111. slide_bg_img = cv2.cvtColor(slide_bg_img, cv2.COLOR_GRAY2RGB)
  112. cv2.imwrite("./slide_bg_handle2.png", slide_bg_img)
  113. slide_block_img = cv.imread("./slide_block.png")
  114. slide_block_img = cv2.Canny(slide_block_img, 100, 200)
  115. cv.imwrite("./slide_block_handled1.png", slide_block_img)
  116. slide_block_img = cv2.cvtColor(slide_block_img, cv2.COLOR_GRAY2RGB)
  117. cv2.imwrite("./slide_block_handled2.png", slide_block_img) # 保存在本地
  118. result = cv.matchTemplate(slide_block_img, slide_bg_img, cv.TM_CCOEFF_NORMED)
  119. minVal, maxVal, minLoc, maxLoc = cv.minMaxLoc(result)
  120. th, tw = slide_block_img.shape[:2]
  121. tl = maxLoc # 左上角点的坐标
  122. br = (tl[0] + tw, tl[1] + th) # 右下角点的坐标
  123. cv2.rectangle(slide_bg_img, tl, br, (0, 0, 255), 2) # 绘制矩形
  124. cv2.imwrite("./match_result.png", slide_bg_img) # 保存在本地
  125. # 返回缺口的X坐标
  126. return maxLoc[0] - start_x, minLoc[0] - start_x
  127. @staticmethod
  128. def set_contrast_brightness(frame, contrast_value, brightness_value):
  129. if not contrast_value:
  130. contrast_value = 0.0
  131. if not brightness_value:
  132. brightness_value = 0
  133. blank = np.zeros(frame.shape, frame.dtype)
  134. frame = cv.addWeighted(frame, contrast_value, blank, 1 - contrast_value, brightness_value)
  135. return frame
  136. @staticmethod
  137. def get_tracks(distance):
  138. """获取移动轨迹"""
  139. tracks = [] # 移动轨迹
  140. current = 0 # 当前位移
  141. mid = distance * 3/4 # 减速阈值
  142. t = 0.5 # 计算间隔
  143. v = 1 # 初始速度
  144. while current < distance:
  145. if current < mid:
  146. a = random.randint(5, 10) # 加速度为正5
  147. else:
  148. a = random.randint(-5, -3) # 加速度为负3
  149. v0 = v # 初速度 v0
  150. v = v0 + a * t # 当前速度
  151. move = v0 * t + 1 / 2 * a * t * t # 移动距离
  152. current += move
  153. tracks.append(round(current))
  154. return tracks
  155. def move_to_notch(self, distance1, distance2):
  156. """移动滑轨按钮到缺口处"""
  157. # 获取滑动验证码所在的iframe
  158. captcha_iframe = self.frame
  159. for i in range(2):
  160. # 获取按钮位置,将鼠标移到上方并按下
  161. slider_btn_rect = captcha_iframe.get_by_alt_text("slider").bounding_box()
  162. self.page.mouse.move(slider_btn_rect['x'], slider_btn_rect['y'])
  163. self.page.mouse.down()
  164. distance = [distance1, distance2][i]
  165. if distance <= 0: # 距离不可能小于等于0
  166. continue
  167. print(f"正在进行第{i + 1}次滑动,滑动距离{distance}")
  168. tracks = self.get_tracks(distance)
  169. for x in tracks:
  170. self.page.mouse.move(slider_btn_rect['x'] + x, random.randint(-5, 5) + slider_btn_rect['y'])
  171. self.page.mouse.move(slider_btn_rect['x'] + tracks[-1] + 5, random.randint(-5, 5) + slider_btn_rect['y'])
  172. self.page.mouse.move(slider_btn_rect['x'] + tracks[-1] - 5, random.randint(-5, 5) + slider_btn_rect['y'])
  173. self.page.mouse.up()
  174. # 滑动结束后等待一段时间
  175. self.page.wait_for_timeout(2000)
  176. # 寻找按钮是否还存在,不存在的话表明已通过滑动验证码,存在的话尝试下一个距离
  177. if not is_element_present(self.page, '.ant-modal-body'):
  178. print("滑动验证通过")
  179. return True
  180. return False
  181. def refresh_captcha(self):
  182. """刷新验证码"""
  183. # 获取滑动验证码所在的iframe
  184. print("刷新验证码")
  185. self.frame.query_selector('.tc-action-icon').click()
  186. self.page.wait_for_timeout(2500)
  187. self.frame = self.page.frames[1]