一、图形验证码的识别
- 先将验证码的图片保存到本。
- 打开开发者工具,找到验证码元素。验证码元素是一张图片,src 属性是 CheckCode.aspx。打开链接 http://my.cnki.net/elibregister/CheckCode.aspx,保存并命名为 code.jpg。
- 识别测试
- 新建一个项目,将验证码图片放到项目根目录下,用 tesserocr 库识别验证码,示例:
import tesserocr from PIL import Image image = Image.open('code.jpg') result = tesserocr.image_to_text(image) print(result)
这里新建了一个 Image 对象,调用 tesserocr 的 image_to_ text()方法。传入该 Image 对象 即可完成识别。
-
tesserocr 还有一个更加简单的方法,这个方法可直接将图片文件转为字符串,代码:
import tesserocr print(tesserocr.file_to_text('image.png'))
此方法的识别效果不如上一种方法好。
- 新建一个项目,将验证码图片放到项目根目录下,用 tesserocr 库识别验证码,示例:
-
验证码处理
-
换一个验证码带有多线条,命名为 code2.jpg。重新识别和实际结果有偏差。
-
对于有线条干扰情况,还需要做额外处理,如转灰度、二值化等操作。 可以利用 Image 对象的 convert()方法参数传人 L,即可将图片转化为灰度图像,示例:
image = image.convert('L') image.show()
传入1 即可将图片进行二值化处理:
image = image.convert('1') image.show()
还可以指定二值化的阈值,上面的方法采用的是默认阈(yù)值。以上方法采用默认阔值 127。不能直接转化原图,要将原图先转为灰度图像,再指定二值化阔值,代码:
import tesserocr from PIL import Image image = Image.open('code.jpg') image = image.convert('L') threshold = 80 table = [] for i in range(256): if i < threshold: table.append(0) else: table.append(1) image = image.point(table,'1') image.show()
变量 threshold 代表二值化阈值,阈值设置为 80。原来验证码中的线条已经去除,整个验证码将会变得黑向分明。这时再重新识别验证码,代码:
import tesserocr from PIL import Image image = Image.open('code2.jpg') image = image.convert('L') threshold = 127 table = [] for i in range(256): if i < threshold: table.append(0) else: table.append(1) image = image.point(table,'1') result = tesserocr.image_to_text(image) print(result)
针对一些有干扰的图片,做一些灰度和二值化处理,会提高图片识别的正确率。
- 阈值:继续遍历图片。如果图片灰度值大于阈值说明是背景图片,将之颜色设置为白色,否则是文字,颜色设置为黑色
-
大概逻辑:
-
(计次循环首(图片.取宽度(),x)
计次循环首(图片.取高度(),y)
‘遍历每一个像素点
‘取出遍历到的当前像素点颜色
颜色值=到字节集(图片.取某点颜色值(x-1,y-1))
‘计算灰度值
灰度值=(颜色值[1]+颜色值[2]+颜色值[3])/3
‘将灰度值放回图片
连续赋值(灰度值,颜色值[1],颜色值[2],颜色值[3])
图片.写某点颜色值(x-1,y-1,灰度值)
计次循环尾()
计次循环尾()
图片.取图片数据()
-
-
二、极验滑动验证码的识别
- 识别思路
- 首先找到一个带有极验验证的网站,如极验官方后台,链接:https://account.geetest.com/login。
-
可以使用Selenium来完全模拟人的行为的方式来完成验证。(一般,验证需要三步:)
- 模拟点击验证按钮: 可以直接用Selenium 模拟点击按钮
- 识别滑动缺口的位置:
- 模拟拖动滑块
- 识别滑动缺口的位置的操作比较关键,需要用到图像相关处理方法
- 首先观察图像:缺口的四周边缘有明显的断裂边缘,边缘和边缘周围有明显的区别。可以实现一个边缘检测算法来找出缺口的位置。对于极验验证码来说,可以利用和原图对比检测的方式来识别缺口的位置,因为在没有滑动滑块之前, 缺口并没有呈现。
-
可以同时获取两张图片。设定一个对比阈值,然后遍历两张图片,找出相同位置像素 RGB 差距超过此阈值的像素点,那么此像素点的位置就是缺口的位置。
- 模拟拖动滑块中需要走注意:极验验证码增加了机器轨迹识别,匀速移动、 随机速度移动等方法都不能通过验证,只有完全模拟人的移动轨迹才可以通过验证。 人的移动轨迹一般是先加速后减速,需要模拟这个过程才能成功。
- 识别滑动缺口的位置的操作比较关键,需要用到图像相关处理方法
- 初始化
- 选定链接:https://account.geetest.com/login,极验的管理后台登录页面。首先初始化一些配置,如 Selenium 对象的初始化及一些参数的配置,示例:
from selenium import webdriver from selenium.webdriver.support.wait import WebDriverWait EMAIL = '[email protected]' PASSWORD = '123456' class CrackGeetest(): def __init__(self): self.url = 'https://account.geetest.com/login' self.browser = webdriver.Chrome() self.wait = WebDriverWait(self.browser,20) self.email = EMAIL self.password = PASSWORD
其中, EMAIL 和 PASSWORD 就是登录极验需要的用户名和密码,如果没有需先注册。
- 选定链接:https://account.geetest.com/login,极验的管理后台登录页面。首先初始化一些配置,如 Selenium 对象的初始化及一些参数的配置,示例:
-
模拟点击
-
实现第一步的操作,也就是模拟点击初始的验证按钮
-
定义一个方法来获取这个按钮,利用显式等待的方法来实现,示例:
def get_geetest_button(self): "获取初始验证按钮:return:按钮对象" button = self.wait.unil(EC.element_to_be_clickable((By.CLASS_NAME,'geetest_radar_tip'))) return button
获取一个WebElement对象,调用它的click()方法即可模拟点击,示例:
#点击验证按钮 button = get_geetest_button() button.click()
完成第一步模拟点击验证按钮
-
-
-
识别缺口
-
识别缺口的位置。
-
首先获取前后两张比对图片,二者不一致的地方即为缺口 。获取不带缺口图片,利用 Selenium选取图片元素,得到其所在位置和宽高,然后获取整个网页的截图,图片裁切出来即可,代码:
def get_position(self): """获取验证码位置:return:验证码位置元组""" img = self.wait.until(EC.presence_of_element_located((By.CLASS_NAME,'geetest_canvas_img'))) time.sleep(2) location =img.location size = img.size top, bottom, left, right = location['y'],location['y']+size['weight'],location['x'],location['x']+size['width'] return(top, bottom, left, right) def get_geetest_image(self,name='captcha.png'): """获取验证码图片:return:图片对象""" top,bottom,left,right = self.get_position() print('验证码位置', top, bottom, left, right) screenshot = self.get_screenshot() captcha = screenshot.crop((left, top, right, bottom)) return captcha
这里 get_position()函数首先获取图片对象,获取它的位置和宽高,随后返回其左上角和右下角的坐标 get_geetest_image()方法获取网页截图,调用 crop()方法将图片裁切出来,返回的是 Image 对象
- 接下来需要获取第二张图片,也就是带缺口的图片。要使得图片出现缺口,只需要点击下方的滑块即可。这个动作触发之后,图片中的缺口就会显现,代码:
def get_slider(self): """获取滑块:return:滑块对象""" slider = self.wait.until(EC.element_to_be_clickable((By.CLASS_NAME,'greetest_slider_button'))) return slider
利用 get_slider()方法获取滑块对象,调用 click()方法即可触发点击,缺口图片即可呈现,代码:
#点按呼出缺口 slider = self.get_slider() slider.click()
调用 get_geetest_image()方法将第二张图片获取下来即可。
- 现在已经得到两张图片对象,分别赋值给变量 image1 和 image2。再对比图片获取缺口。这里遍历图片的每个坐标点,获取两张图片对应像素点的 RGB 数据。如果二者 RGB 数据差距距在一定范围内,那就代表两个像素相同,继续比对下一个像素点 如果差距超过一定范围,则代表像素点不同,当前位置即为缺口位置,代码:
def is_pixel_equal(self, image1, image2, x, y): """判断两个像素是否相同 :param image1:图片1 :param image1:图片1 :param x:位置x :param y:位置y :return:像素是否相同""" #取两个图片的像素点 pixel1 = image1.load()[x, y] pixel2 = image2.load()[x, y] threshold = 60 if abs(pixel1[0] - pixel2[0]) < threshold and abs(pixel1[1] - pixel2[1]) < threshold and abs(pixel1[2] -pixel2[2])<threshold: return True else: return False def get_gap(self, image1, image2): """获取缺口偏移量 :param image1:不带缺口图片 :param image2:带缺口图片 :return:""" left = 60 for i in range(left,image1.size[0]): for j in range(image1.size[1]): if not self.is_pixel_equal(image1, image2, i, j): left = i return left return left
get_gap()方法即获取缺口位置的方法。此方法的参数是两张图片, 一张为带缺口图片,另一张为不带缺口图片。遍历两张图片的每个像素,利用 is_pixel_equal()方法判断两张图片同一位置的 像素是杏相同。比较两张图 RGB 的绝对值是否均小于定义的阈值 threshold。如果绝对值均在阔值之 内,则代表像素点相同,继续遍历。否则代表不相同的像素点,即缺口的位置。
- 滑块的位置会出现在左边位置,缺口会出现在与滑块同一水平线的位置,所以缺口一般会在滑块的右侧。如果要寻找缺口,直接从滑块右侧寻找即可。直接设置遍历的起始横坐标为 60,也就是从滑块的右侧开始识别,这样识别出的结果就是缺口的位置。
-
-
- 模拟拖动
- 完全模拟加速减速的过程也就是人拖动滑块的操作通过验证。前段滑块做匀加速运动,后段滑块做匀减速运动,利用加速度公式即可完成验证。
- 滑块滑动的加速度用 a 表示, 当前速度用 v 表示,初速用 v0 表示,位移用 x 表示,所需时间用t 表示,满足关系:
- x = v0*t + 0.5*a*t*t
- v = v0 + a*t
- 利用这两个公式可以构造轨迹移动算法,计算出先加速后减速的运动轨迹,代码:
def get_track(self, distance): """根据偏移量获取移动轨迹 :param distance:偏移量 :return: 移动轨迹""" #移动轨迹 track = [] #当前位移 current = 0 #减速阈值 mid = distance * 4 / 5 #计算间隔 t = 0.2 #初速度 v = 0 while current < distance: if current < mid: a = 2 else: a = -3 #初速度 V0 v0 = v v = v0 + a*t move = v0 *t +1/2 *a *t *t current += move #加入轨迹 track.append(round(move)) return track
定义 get_ rack()方法,传人的参数为移动的总距离,返回的是运动轨迹。运动轨迹用 track 表示,是一个列表,列表的每个元素代表每次移动多少距离。
-
定义变量 mid,即减速的阔值,也就是加速到什么位置开始减速。这里 mid 值为 4/5,即模拟前 4/5 路程是加速过程,后 1/5 路程是减速过程。
- 接着定义当前位移的距离变量 current,初始为 0,然后进入 while 循环,循环的条件是当前位移小于总距离。在循环里我们分段定义了加速度,其中加速过程的加速度定义为 2,减速过程的加速度定义为 -3。之后套用位移公式计算出某个时间段内的位移,将当前位移更新并记录到轨迹里即可。
- 直到运动轨迹达到总距离时,循环终止。最后得到的 track 记录了每个时间间隔移动了多少位移, 这样滑块的运动轨迹就得到了。
- 最后按照该运动轨迹拖动滑块即可,方法实现代码:
def move_to_gap(self, slider,tracks): """拖动滑块到缺口处 :params slider:滑块 :params tracks:轨迹 :return:""" ActionChains(self,browser).click_and_hold(slider).perform() for x in tracks: ActionChains(self,browser).move_by_offset(xoffset=x,yoffset=0).perform() time.sleep(0.5) ActionChains(self.browser).release().perform()
传人的参数为滑块对象和运动轨迹。首先调用 ActionChains 的 click_and_ hold()方法按住拖动底部滑块,遍历运动轨迹获取每小段位移距离,调用 move_by_offset()方法移动此位移,最后调用 release()方法松开鼠标即可。
-
- 滑块滑动的加速度用 a 表示, 当前速度用 v 表示,初速用 v0 表示,位移用 x 表示,所需时间用t 表示,满足关系:
- 完全模拟加速减速的过程也就是人拖动滑块的操作通过验证。前段滑块做匀加速运动,后段滑块做匀减速运动,利用加速度公式即可完成验证。
-
完整代码(需适当修改参数):
import time from io import BytesIO from PIL import Image from selenium import webdriver from selenium.webdriver import ActionChains from selenium.webdriver.common.by import By from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support import expected_conditions as EC EMAIL = '[email protected]' PASSWORD = '' BORDER = 6 INIT_LEFT = 60 class CrackGeetest(): def __init__(self): self.url = 'https://account.geetest.com/login' self.browser = webdriver.Chrome() self.wait = WebDriverWait(self.browser, 20) self.email = EMAIL self.password = PASSWORD def __del__(self): self.browser.close() def get_geetest_button(self): """ 获取初始验证按钮 :return: """ button = self.wait.until(EC.element_to_be_clickable((By.CLASS_NAME, 'geetest_radar_tip'))) return button def get_position(self): """ 获取验证码位置 :return: 验证码位置元组 """ img = self.wait.until(EC.presence_of_element_located((By.CLASS_NAME, 'geetest_canvas_img'))) time.sleep(2) location = img.location size = img.size top, bottom, left, right = location['y'], location['y'] + size['height'], location['x'], location['x'] + size[ 'width'] return (top, bottom, left, right) def get_screenshot(self): """ 获取网页截图 :return: 截图对象 """ screenshot = self.browser.get_screenshot_as_png() screenshot = Image.open(BytesIO(screenshot)) return screenshot def get_slider(self): """ 获取滑块 :return: 滑块对象 """ slider = self.wait.until(EC.element_to_be_clickable((By.CLASS_NAME, 'geetest_slider_button'))) return slider def get_geetest_image(self, name='captcha.png'): """ 获取验证码图片 :return: 图片对象 """ top, bottom, left, right = self.get_position() print('验证码位置', top, bottom, left, right) screenshot = self.get_screenshot() captcha = screenshot.crop((left, top, right, bottom)) captcha.save(name) return captcha def open(self): """ 打开网页输入用户名密码 :return: None """ self.browser.get(self.url) email = self.wait.until(EC.presence_of_element_located((By.ID, 'email'))) password = self.wait.until(EC.presence_of_element_located((By.ID, 'password'))) email.send_keys(self.email) password.send_keys(self.password) def get_gap(self, image1, image2): """ 获取缺口偏移量 :param image1: 不带缺口图片 :param image2: 带缺口图片 :return: """ left = 60 for i in range(left, image1.size[0]): for j in range(image1.size[1]): if not self.is_pixel_equal(image1, image2, i, j): left = i return left return left def is_pixel_equal(self, image1, image2, x, y): """ 判断两个像素是否相同 :param image1: 图片1 :param image2: 图片2 :param x: 位置x :param y: 位置y :return: 像素是否相同 """ # 取两个图片的像素点 pixel1 = image1.load()[x, y] pixel2 = image2.load()[x, y] threshold = 60 if abs(pixel1[0] - pixel2[0]) < threshold and abs(pixel1[1] - pixel2[1]) < threshold and abs( pixel1[2] - pixel2[2]) < threshold: return True else: return False def get_track(self, distance): """ 根据偏移量获取移动轨迹 :param distance: 偏移量 :return: 移动轨迹 """ # 移动轨迹 track = [] # 当前位移 current = 0 # 减速阈值 mid = distance * 4 / 5 # 计算间隔 t = 0.2 # 初速度 v = 0 while current < distance: if current < mid: # 加速度为正2 a = 2 else: # 加速度为负3 a = -3 # 初速度v0 v0 = v # 当前速度v = v0 + at v = v0 + a * t # 移动距离x = v0t + 1/2 * a * t^2 move = v0 * t + 1 / 2 * a * t * t # 当前位移 current += move # 加入轨迹 track.append(round(move)) return track def move_to_gap(self, slider, track): """ 拖动滑块到缺口处 :param slider: 滑块 :param track: 轨迹 :return: """ ActionChains(self.browser).click_and_hold(slider).perform() for x in track: ActionChains(self.browser).move_by_offset(xoffset=x, yoffset=0).perform() time.sleep(0.5) ActionChains(self.browser).release().perform() def login(self): """ 登录 :return: None """ submit = self.wait.until(EC.element_to_be_clickable((By.CLASS_NAME, 'login-btn'))) submit.click() time.sleep(10) print('登录成功') def crack(self): # 输入用户名密码 self.open() # 点击验证按钮 button = self.get_geetest_button() button.click() # 获取验证码图片 image1 = self.get_geetest_image('captcha1.png') # 点按呼出缺口 slider = self.get_slider() slider.click() # 获取带缺口的验证码图片 image2 = self.get_geetest_image('captcha2.png') # 获取缺口位置 gap = self.get_gap(image1, image2) print('缺口位置', gap) # 减去缺口位移 gap -= BORDER # 获取移动轨迹 track = self.get_track(gap) print('滑动轨迹', track) # 拖动滑块 self.move_to_gap(slider, track) success = self.wait.until( EC.text_to_be_present_in_element((By.CLASS_NAME, 'geetest_success_radar_tip_content'), '验证成功')) print(success) # 失败后重试 if not success: self.crack() else: self.login() if __name__ == '__main__': crack = CrackGeetest() crack.crack()
三、点触验证码的识别
- 识别思路