├── README.md └── geetestCrack.py /README.md: -------------------------------------------------------------------------------- 1 | # geetestCrack 2 | 极验验证码破解 3 | 4 | 目前来看成功率还不错 5 | -------------------------------------------------------------------------------- /geetestCrack.py: -------------------------------------------------------------------------------- 1 | # -*-coding:utf-8 -*- 2 | import base64 3 | import random 4 | import time 5 | import functools 6 | import numpy as np 7 | 8 | from tools.selenium_spider import SeleniumSpider 9 | 10 | from selenium.webdriver import ActionChains 11 | from selenium.webdriver.support import expected_conditions as EC 12 | from selenium.webdriver.support.ui import WebDriverWait 13 | from selenium.webdriver.common.by import By 14 | import PIL.Image as image 15 | from PIL import ImageChops 16 | from io import BytesIO 17 | 18 | 19 | class Crack(object): 20 | """ 21 | 解决三代极验验证码滑块 22 | """ 23 | def __init__(self): 24 | self.url = 'https://www.geetest.com' 25 | self.browser = SeleniumSpider(path="/personalwork/personal_tools_project/adbtools/chromedriver", max_window=True) 26 | self.wait = WebDriverWait(self.browser, 100) 27 | self.BORDER = 8 28 | self.table = [] 29 | 30 | for i in range(256): 31 | if i < 40: 32 | self.table.append(0) 33 | else: 34 | self.table.append(1) 35 | 36 | def open(self): 37 | """ 38 | 打开浏览器,并输入查询内容 39 | """ 40 | self.browser.get(self.url) 41 | self.browser.get(self.url + "/Sensebot/") 42 | time.sleep(1) 43 | self.browser.web_driver_wait_ruishu(10, "class", 'experience--area') 44 | self.browser.execute_js('document.getElementsByClassName("experience--area")[0].getElementsByTagName("div")' 45 | '[2].getElementsByTagName("ul")[0].getElementsByTagName("li")[1].click()') 46 | 47 | time.sleep(1) 48 | self.browser.web_driver_wait_ruishu(10, "class", 'geetest_radar_tip') 49 | 50 | self.browser.execute_js('document.getElementsByClassName("geetest_radar_tip")[0].click()') 51 | 52 | def check_status(self): 53 | """ 54 | 检测是否需要滑块验证码 55 | :return: 56 | """ 57 | self.browser.web_driver_wait_ruishu(10, "class", 'geetest_success_radar_tip_content') 58 | try: 59 | time.sleep(1) 60 | message = self.browser.find_element_by_class_name("geetest_success_radar_tip_content").text 61 | if message == "验证成功": 62 | return False 63 | else: 64 | return True 65 | except Exception as e: 66 | return True 67 | 68 | def get_images(self): 69 | """ 70 | 获取验证码图片 71 | :return: 图片的location信息 72 | """ 73 | time.sleep(1) 74 | self.browser.web_driver_wait_ruishu(10, "class", 'geetest_canvas_slice') 75 | fullgb = self.browser.execute_js('document.getElementsByClassName("geetest_canvas_bg geetest_' 76 | 'absolute")[0].toDataURL("image/png")')["value"] 77 | 78 | bg = self.browser.execute_js('document.getElementsByClassName("geetest_canvas_fullbg geetest_fade' 79 | ' geetest_absolute")[0].toDataURL("image/png")')["value"] 80 | return bg, fullgb 81 | 82 | def get_decode_image(self, filename, location_list): 83 | """ 84 | 解码base64数据 85 | """ 86 | _, img = location_list.split(",") 87 | img = base64.decodebytes(img.encode()) 88 | new_im: image.Image = image.open(BytesIO(img)) 89 | new_im.convert("RGB") 90 | new_im.save(filename) 91 | 92 | return new_im 93 | 94 | def is_pixel_equal(self, img1: image.Image, img2: image.Image, x, y): 95 | """ 96 | 判断两个像素是否相同 97 | :param image1: 图片1 98 | :param image2: 图片2 99 | :param x: 位置x 100 | :param y: 位置y 101 | :return: 像素是否相同 102 | """ 103 | 104 | # 取两个图片的像素点 105 | pix1 = img1.load()[x, y] 106 | pix2 = img2.load()[x, y] 107 | threshold = 30 108 | if (abs(pix1[0] - pix2[0] < threshold) and abs(pix1[1] - pix2[1] < threshold) and abs( 109 | pix1[2] - pix2[2] < threshold)): 110 | return True 111 | else: 112 | print("色差点", pix1, pix2) 113 | return False 114 | 115 | def compute_gap(self, img1, img2): 116 | """计算缺口偏移""" 117 | # 将图片修改为RGB模式 118 | img1 = img1.convert("RGB") 119 | img2 = img2.convert("RGB") 120 | 121 | # 计算差值 122 | diff = ImageChops.difference(img1, img2) 123 | 124 | # 灰度图 125 | diff = diff.convert("L") 126 | 127 | # 二值化 128 | diff = diff.point(self.table, '1') 129 | 130 | left = 43 131 | 132 | for w in range(left, diff.size[0]): 133 | lis = [] 134 | for h in range(diff.size[1]): 135 | if diff.load()[w, h] == 1: 136 | lis.append(w) 137 | if len(lis) > 5: 138 | return w 139 | 140 | def get_gap(self, img1, img2): 141 | """ 142 | 获取缺口偏移量 这种查找方式成功率很低 143 | :param img1: 不带缺口图片 144 | :param img2: 带缺口图片 145 | :return: 146 | """ 147 | 148 | left = 43 149 | 150 | # 优化 如果有4个像素都一样才视为是缺口边界 151 | lis = [] 152 | for x in range(left, img1.size[0]): 153 | for y in range(img1.size[1]): 154 | if not self.is_pixel_equal(img1, img2, x, y): 155 | lis.append(x) 156 | if len(lis) >= 3: 157 | left = x 158 | return left 159 | return left 160 | 161 | def ease_out_quad(self, x): 162 | return 1 - (1 - x) * (1 - x) 163 | 164 | def ease_out_quart(self, x): 165 | return 1 - pow(1 - x, 4) 166 | 167 | def ease_out_expo(self, x): 168 | if x == 1: 169 | return 1 170 | else: 171 | return 1 - pow(2, -10 * x) 172 | 173 | def get_tracks_2(self, distance, seconds, ease_func): 174 | """ 175 | 根据轨迹离散分布生成的数学生成 # 参考文档 https://www.jianshu.com/p/3f968958af5a 176 | 成功率很高 90% 往上 177 | :param distance: 缺口位置 178 | :param seconds: 时间 179 | :param ease_func: 生成函数 180 | :return: 轨迹数组 181 | """ 182 | distance += 20 183 | tracks = [0] 184 | offsets = [0] 185 | for t in np.arange(0.0, seconds, 0.1): 186 | ease = ease_func 187 | offset = round(ease(t / seconds) * distance) 188 | tracks.append(offset - offsets[-1]) 189 | offsets.append(offset) 190 | tracks.extend([-3, -2, -3, -2, -2, -2, -2, -1, -0, -1, -1, -1]) 191 | return tracks 192 | 193 | def get_track(self, distance): 194 | """ 195 | 根据物理学生成方式 极验不能用 成功率基本为0 196 | :param distance: 偏移量 197 | :return: 移动轨迹 198 | """ 199 | distance += 20 200 | # 移动轨迹 201 | track = [] 202 | # 当前位移 203 | current = 0 204 | # 减速阈值 205 | mid = distance * 3 / 5 206 | # 计算间隔 207 | t = 0.5 208 | # 初速度 209 | v = 0 210 | 211 | while current < distance: 212 | if current < mid: 213 | # 加速度为正2 214 | a = 2 215 | else: 216 | # 加速度为负3 217 | a = -3 218 | # 初速度v0 219 | v0 = v 220 | # 当前速度v = v0 + at 221 | v = v0 + a * t 222 | # 移动距离x = v0t + 1/2 * a * t^2 223 | move = v0 * t + 0.5 * a * (t ** 2) 224 | # 当前位移 225 | current += move 226 | # 加入轨迹 227 | track.append(round(move)) 228 | track.extend([-3, -3, -2, -2, -2, -2, -2, -1, -1, -1, -1]) 229 | return track 230 | 231 | def move_to_gap(self, track): 232 | """移动滑块到缺口处""" 233 | slider = self.wait.until(EC.presence_of_element_located((By.CLASS_NAME, 'geetest_slider_button'))) 234 | ActionChains(self.browser).click_and_hold(slider).perform() 235 | 236 | while track: 237 | x = track.pop(0) 238 | ActionChains(self.browser).move_by_offset(xoffset=x, yoffset=0).perform() 239 | time.sleep(0.02) 240 | 241 | ActionChains(self.browser).release().perform() 242 | 243 | def crack(self): 244 | num = 5 # 重试次数 245 | # 打开浏览器 246 | self.open() 247 | 248 | if self.check_status(): 249 | # 保存的图片名字 250 | bg_filename = 'bg.png' 251 | fullbg_filename = 'fullbg.png' 252 | 253 | # 获取图片 254 | bg_location_base64, fullbg_location_64 = self.get_images() 255 | 256 | # 根据位置对图片进行合并还原 257 | bg_img = self.get_decode_image(bg_filename, bg_location_base64) 258 | fullbg_img = self.get_decode_image(fullbg_filename, fullbg_location_64) 259 | # 获取缺口位置 260 | gap = self.compute_gap(fullbg_img, bg_img) 261 | print('缺口位置', gap) 262 | 263 | track = self.get_tracks_2(gap - self.BORDER, random.randint(2, 4), self.ease_out_quart) 264 | print("滑动轨迹", track) 265 | print("滑动距离", functools.reduce(lambda x, y: x+y, track)) 266 | self.move_to_gap(track) 267 | 268 | time.sleep(3) 269 | if not self.check_status(): 270 | print('验证成功') 271 | return True 272 | else: 273 | print('验证失败') 274 | return False 275 | 276 | else: 277 | print("验证成功") 278 | return True 279 | 280 | 281 | if __name__ == '__main__': 282 | print('开始验证') 283 | crack = Crack() 284 | count = 0 285 | for i in range(100): 286 | if crack.crack(): 287 | count += 1 288 | print(f"成功率:{count / 100 * 100}%") 289 | # crack.open() 290 | --------------------------------------------------------------------------------