本节我们将介绍新浪微博宫格验证码的识别。微博宫格验证码是一种新型交互式验证码,每个宫格之间会有一条
指示连线,指示了应该的滑动轨迹。我们要按照滑动轨迹依次从起始宫格滑动到终止宫格,才可以完成验证,如
下图所示。
鼠标滑动后的轨迹会以黄色的连线来标识,如下图所示。
访问新浪微博移动版登录页面,就可以看到如上验证码,链接为 https://passport.weibo.cn/signin/login
一、本节目标
我们的目标是用程序来识别并通过微博宫格验证码的验证。
二、准备工作
本次我们使用的Python库是Selenium,使用的浏览器为Chrome,请确保已经正确安装好Selenium库、Chrome浏览器,并配置好ChromeDriver。
三、识别思路
识别从探寻规律入手。规律就是,此验证码的四个宫格一定是有连线经过的,每一条连线上都会相应的指示箭头,连线的形状多样,包括C型、Z型、X型等,如下图所示。
我们发现,同一类型的连线轨迹是相同的,唯一不同的就是连线的方向,如下图所示。这两种验证码的连线轨迹是相同的。但是由于连线上面的指示箭头不同,导致滑动的宫格顺序有所不同。
如果要完全识别滑动宫格顺序,就需要具体识别出箭头的朝向。而整个验证码箭头朝向一共有8种,而且会出现在不同的位置。如果要写一个箭头方向识别算法,需要考虑不同箭头所在的位置,找
出各个位置箭头的像素点坐标,计算像素点变化规律,这个工作量就会变得比较大。这时我们可以考虑用模板匹配的方法,就是将一些识别目标提前保存并做好标记,这称作模板。这里将验证码图
片做好拖动顺序的标记当做模板。对比要新识别的目标和每一个模板,如果找到匹配的模板,则就成功识别出要新识别的目标。在图像识别中,模板匹配也是常用的方法,实现简单且易用性好。
我们必须要收集到足够多的模板,模板匹配方法的效果才会好。而对于微博宫格验证码来说,宫格只有4个,验证码的样式最多4×3×2×1=24种,则我们可以将所有模板都收集下来。
接下来我们需要考虑的就是,用何种模板来进行匹配,只匹配箭头还是匹配整个验证码全图呢?我们权衡一下这两种方式的匹配精度和工作量。
首先是精度问题。如果是匹配箭头,比对的目标只有几个像素点范围的箭头,我们需要精确知道各个箭头所在的像素点,一旦像素点有偏差,那么会直接错位,导致匹配结果大打折扣。如果
是匹配全图,我们无需关心箭头所在位置,同时还有连线帮助辅助匹配。显然,全图匹配的精度更高。
其次是工作量的问题。如果是匹配箭头,我们需要保存所有不同朝向的箭头模板,而相同位置箭头的朝向可能不一,相同朝向的箭头位置可能不一,那么我们需要算出每个箭头的位置并将其
逐个截出保存成模板,依次探寻验证码对应位置是否有匹配模板。如果是匹配全图,我们不需要关心每个箭头的位置和朝向,只需要将验证码全图保存下来即可,在匹配的时候也不需要计算箭头的
位置。显然,匹配全图的工作量更少。
综上考虑,我们选用全图匹配的方式来进行识别。找到匹配的模板之后,我们就可以得到事先为模板定义的拖动顺序,然后模拟拖动即可。
获取模板:
1 import os 2 import time 3 from io import BytesIO 4 from PIL import Image 5 from selenium import webdriver 6 from selenium.common.exceptions import TimeoutException 7 from selenium.webdriver import ActionChains 8 from selenium.webdriver.common.by import By 9 from selenium.webdriver.support.ui import WebDriverWait 10 from selenium.webdriver.support import expected_conditions as EC 11 from os import listdir 12 13 USERNAME = ‘‘ 14 PASSWORD = ‘‘ 15 16 TEMPLATES_FOLDER = ‘templates/‘ 17 18 class CrackWeiboSlide(): 19 def __init__(self): 20 self.url = ‘https://passport.weibo.cn/signin/login?entry=mweibo&r=https://m.weibo.cn/‘ 21 self.browser = webdriver.Chrome() 22 self.wait = WebDriverWait(self.browser, 20) 23 self.username = USERNAME 24 self.password = PASSWORD 25 26 def __del__(self): 27 self.browser.close() 28 29 def open(self): 30 """ 31 打开网页输入用户名密码登陆 32 :return: None 33 """ 34 self.browser.get(self.url) 35 username = self.wait.until(EC.presence_of_element_located((By.ID, ‘loginName‘))) 36 password = self.wait.until(EC.presence_of_element_located((By.ID, ‘loginPassword‘))) 37 submit = self.wait.until(EC.element_to_be_clickable((By.ID, ‘loginAction‘))) 38 username.send_keys(self.username) 39 password.send_keys(self.password) 40 submit.click() 41 42 def get_position(self): 43 """ 44 获取验证码位置 45 :return: 验证码位置元组 46 """ 47 try: 48 img = self.wait.until(EC.presence_of_element_located((By.CLASS_NAME, ‘patt-shadow‘))) 49 except TimeoutException: 50 print(‘未出现验证码‘) 51 self.opem() 52 time.sleep(2) 53 location = img.location 54 size = img.size 55 top, bottom, left, right = location[‘y‘], location[‘y‘] + size[‘height‘], location[‘x‘], location[‘x‘] + size[‘width‘] 56 return (top, bottom, left, right) 57 58 def get_screenshot(self): 59 """ 60 获取网页截图 61 :return: 截图对象 62 """ 63 screenshot = self.browser.get_screenshot_as_png() 64 screenshot = Image.open(BytesIO(screenshot)) 65 return screenshot 66 67 def get_image(self, name=‘captcha.png‘): 68 """ 69 获取验证码图片 70 :return:图片对象 71 """ 72 top, bottom, left, right = self.get_position() 73 print(‘验证码位置‘, top, bottom, left, right) 74 screenshot = self.get_screenshot() 75 captcha = screenshot.crop((left, top, right, bottom)) 76 captcha.save(name) 77 return captcha 78 79 def main(self): 80 """ 81 批量获取验证码 82 :return: 图片对象 83 """ 84 count = 0 85 while True: 86 self.open() 87 self.get_image(str(count) + ‘.png‘) 88 count += 1 89 90 if __name__ == ‘__main__‘: 91 crack = CrackWeiboSlide() 92 crack.main()
这里需要将USERNAME
和PASSWORD
修改为自己微博的用户名和密码。运行一段时间后,本地多了很多以数字命名的验证码,如下图所示。
我们将图片命名为4132.png,代表滑动顺序为4-1-3-2。按照这样的规则,我们将验证码整理为如下24张图,如下图所示。
好了,获取模板就到此结束了,接下来该模板匹配了
方法解释:
(1)调用get_image()
方法,得到验证码图片对象。然后,对验证码图片对象进行模板匹配
(2)TEMPLATES_FOLDER
就是模板所在的文件夹。这里通过listdir()
方法获取所有模板的文件名称,然后对其进行遍历,通过same_image()
方法对验证码和模板进行比对。如果匹配成功,那么就将匹配到的模板文件名转换为列表。如模板文件3124.png匹配到了,则返回结果
为[3, 1, 2, 4]。
(3)same_image()
方法接收两个参数,image
为待检测的验证码图片对象,template
是模板对象。由于二者大小是完全一致的,所以在这里我
们遍历了图片的所有像素点。比对二者同一位置的像素点,如果像素点相同,计数就加1。最后计算相同的像素点占总像素的比例。如果
该比例超过一定阈值,那就判定图片完全相同,则匹配成功。这里阈值设定为0.99,即如果二者有0.99以上的相似比,则代表匹配成功。
(4)通过上面的方法,依次匹配24个模板。如果验证码图片正常,我们总能找到一个匹配的模板,这样就可以得到宫格的滑动顺序了。
(5)接下来,根据滑动顺序拖动鼠标,连接各个宫格
这里方法接收的参数就是宫格的点按顺序,如[3,1,2,4]。首先我们利用find_elements_by_css_selector()
方法获取到4个宫格元素,它
是一个列表形式,每个元素代表一个宫格。接下来遍历宫格的点按顺序,做一系列对应操作。其中如果当前遍历的是第一个宫格,那就直
接鼠标点击并保持动作,否则移动到下一个宫格。如果当前遍历的是最后一个宫格,那就松开鼠标,如果不是最后一个宫格,则计算移动
到下一个宫格的偏移量。通过4次循环,我们便可以成功操作浏览器完成宫格验证码的拖拽填充,松开鼠标之后即可识别成功.
(6)鼠标会慢慢从起始位置移动到终止位置。最后一个宫格松开之后,验证码的识别便完成了。至此,微博宫格验证码的识别就全部完成。验
证码窗口会自动关闭。直接点击登录按钮即可登录微博。
方法代码(为什么验证不成功?请大家帮我看一下):
1 import os 2 import time 3 from io import BytesIO 4 from PIL import Image 5 from selenium import webdriver 6 from selenium.common.exceptions import TimeoutException 7 from selenium.webdriver import ActionChains 8 from selenium.webdriver.common.by import By 9 from selenium.webdriver.support.ui import WebDriverWait 10 from selenium.webdriver.support import expected_conditions as EC 11 from os import listdir 12 13 USERNAME = ‘‘ 14 PASSWORD = ‘‘ 15 16 TEMPLATES_FOLDER = ‘templates/‘ 17 18 19 class CrackWeiboSlide(): 20 def __init__(self): 21 self.url = ‘https://passport.weibo.cn/signin/login?entry=mweibo&r=https://m.weibo.cn/‘ 22 self.browser = webdriver.Chrome() 23 self.wait = WebDriverWait(self.browser, 20) 24 self.username = USERNAME 25 self.password = PASSWORD 26 27 def __del__(self): 28 self.browser.close() 29 30 def open(self): 31 """ 32 打开网页输入用户名密码并点击 33 :return: None 34 """ 35 self.browser.get(self.url) 36 username = self.wait.until(EC.presence_of_element_located((By.ID, ‘loginName‘))) 37 password = self.wait.until(EC.presence_of_element_located((By.ID, ‘loginPassword‘))) 38 submit = self.wait.until(EC.element_to_be_clickable((By.ID, ‘loginAction‘))) 39 username.send_keys(self.username) 40 password.send_keys(self.password) 41 submit.click() 42 43 def get_position(self): 44 """ 45 获取验证码位置 46 :return: 验证码位置元组 47 """ 48 try: 49 img = self.wait.until(EC.presence_of_element_located((By.CLASS_NAME, ‘patt-shadow‘))) 50 except TimeoutException: 51 print(‘未出现验证码‘) 52 self.open() 53 time.sleep(2) 54 location = img.location 55 size = img.size 56 top, bottom, left, right = location[‘y‘], location[‘y‘] + size[‘height‘], location[‘x‘], location[‘x‘] + size[ 57 ‘width‘] 58 return (top, bottom, left, right) 59 60 def get_screenshot(self): 61 """ 62 获取网页截图 63 :return: 截图对象 64 """ 65 screenshot = self.browser.get_screenshot_as_png() 66 screenshot = Image.open(BytesIO(screenshot)) 67 return screenshot 68 69 def get_image(self, name=‘captcha.png‘): 70 """ 71 获取验证码图片 72 :return: 图片对象 73 """ 74 top, bottom, left, right = self.get_position() 75 print(‘验证码位置‘, top, bottom, left, right) 76 screenshot = self.get_screenshot() 77 captcha = screenshot.crop((left, top, right, bottom)) 78 captcha.save(name) 79 return captcha 80 81 def is_pixel_equal(self, image1, image2, x, y): 82 """ 83 判断两个像素是否相同 84 :param image1: 图片1 85 :param image2: 图片2 86 :param x: 位置x 87 :param y: 位置y 88 :return: 像素是否相同 89 """ 90 # 取两个图片的像素点 91 pixel1 = image1.load()[x, y] 92 pixel2 = image2.load()[x, y] 93 threshold = 20 94 if abs(pixel1[0] - pixel2[0]) < threshold and abs(pixel1[1] - pixel2[1]) < threshold and abs( 95 pixel1[2] - pixel2[2]) < threshold: 96 return True 97 else: 98 return False 99 100 def same_image(self, image, template): 101 """ 102 识别相似验证码 103 :param image: 待识别验证码 104 :param template: 模板 105 :return: 106 """ 107 # 相似度阈值 108 threshold = 0.99 109 count = 0 110 for x in range(image.width): 111 for y in range(image.height): 112 # 判断像素是否相同 113 if self.is_pixel_equal(image, template, x, y): 114 count += 1 115 result = float(count) / (image.width * image.height) 116 if result > threshold: 117 print(‘成功匹配‘) 118 return True 119 return False 120 121 def detect_image(self, image): 122 """ 123 匹配图片 124 :param image: 图片 125 :return: 拖动顺序 126 """ 127 for template_name in listdir(TEMPLATES_FOLDER): 128 print(‘正在匹配‘, template_name) 129 template = Image.open(TEMPLATES_FOLDER + template_name) 130 if self.same_image(image, template): 131 # 返回顺序 132 numbers = [int(number) for number in list(template_name.split(‘.‘)[0])] 133 print(‘拖动顺序‘, numbers) 134 return numbers 135 136 def move(self, numbers): 137 """ 138 根据顺序拖动 139 :param numbers: 140 :return: 141 """ 142 # 获得四个按点 143 circles = self.browser.find_elements_by_css_selector(‘.patt-wrap .patt-circ‘) 144 dx = dy = 0 145 for index in range(4): 146 circle = circles[numbers[index] - 1] 147 # 如果是第一次循环 148 if index == 0: 149 # 点击第一个按点 150 ActionChains(self.browser).move_to_element_with_offset(circle, circle.size[‘width‘] / 2, circle.size[‘height‘] / 2).click_and_hold().perform() 151 else: 152 # 小幅移动次数 153 times = 30 154 # 拖动 155 for i in range(times): 156 ActionChains(self.browser).move_by_offset(dx / times, dy / times).perform() 157 time.sleep(1 / times) 158 # 如果是最后一次循环 159 if index == 3: 160 # 松开鼠标 161 ActionChains(self.browser).release().perform() 162 else: 163 # 计算下一次偏移 164 dx = circles[numbers[index + 1] - 1].location[‘x‘] - circle.location[‘x‘] 165 dy = circles[numbers[index + 1] - 1].location[‘y‘] - circle.location[‘y‘] 166 167 def crack(self): 168 """ 169 破解入口 170 :return: 171 """ 172 self.open() 173 # 获取验证码图片 174 image = self.get_image(‘captcha.png‘) 175 numbers = self.detect_image(image) 176 self.move(numbers) 177 time.sleep(10) 178 print(‘识别结束‘) 179 180 181 if __name__ == ‘__main__‘: 182 crack = CrackWeiboSlide() 183 crack.crack()
错误提示:
最后,本节代码来自:https://github.com/Python3WebSpider/CrackWeiboSlide