本文介绍了几种常见的验证码类型以及它们的识别方法,包括图形验证码、极验滑动验证码、点触验证码和微博宫格验证码等。其中,针对图形验证码的识别方法是使用OCR技术,并且需要安装tesserocr库。我们可以先在知网注册页面下载验证码图片,并将其保存在本地,命名为code.jpg,然后使用tesserocr库进行识别测试。
一、识别图形验证码
知网注册页面的图形验证码是一种反爬措施,我们可以使用 OCR 技术来识别验证码。需要安装 tesserocr 库来实现。
在实现之前,我们先从知网注册页面下载验证码图片并保存到本地,我们可以将其命名为 code.jpg,以进行识别测试。
识别测试
将验证码图片放到程序所在的目录,用 tesserocr 库识别验证码,代码如下:
1 importtesserocr2 from PIL importImage3 image = Image.open('code.jpg')4 result =tesserocr.image_to_text(image)5 print(result)运行代码输出为空,看来是没有成功识别验证码。
使用 tesserocr 的一个简单方法直接图片文件转为字符串,代码如下所示:
1 importtesserocr
2 print(tesserocr.file_to_text('code.jpg'))
在进行图形验证码识别时,有时会出现识别错误的情况,这可能是因为图片中存在多余的线条干扰。因此,在进行识别前需要进行一些预处理操作,例如将图片转换为灰度图像以及进行二值化处理。 具体而言,对于待处理的图片,可以使用 Image 对象的 convert() 方法,并传入参数 L 来实现将图片转换为灰度图像的操作。
1 image = image.convert('L')2 image.show() #显示图片
传入 1 可将图片进行二值化处理,如下所示:
1 image = image.convert('1')2 image.show() #显示图片
还可以通过指定二值化的阈值。上面的方法是采用的默认阈值 127。这还不能直接转化原图,要将原图先转为灰度图像,然后再指定二值化阈值。代码如下所示:
1 importtesserocr
2 from PIL importImage
3
4 image = Image.open('code.jpg')
5 image = image.convert('L')
6 threshold = 121
7 table =[]
8 for i in range(256):
9 if i <threshold:
10 table.append(0)
11 else:
12 table.append(1)
13 image = image.point(table, '1')
14 #image.show()
15 image.save('code3.jpg')
16 #image = image.convert('1')
17 #image.show() # 显示图片
18 result =tesserocr.image_to_text(image)
19 print(result)
调试各种参数,识别效果仍然不理想。
极验验证码是一种需要手动拼合滑块来完成验证的安全系统,相较于图形验证码,识别难度更高。为了破解该验证码,需要使用程序自动识别和拼合滑块,步骤包括分析识别思路、识别缺口位置、生成滑块拖动和模拟实现滑块拼合通过验证等。为了完成这些步骤,需要使用Chrome浏览器和配置ChromeDriver,同时需要使用Python库Selenium。极验验证码常被用于各种网站,包括直播视频、金融服务、电子商务、游戏娱乐和政府企业等。识别难度大,首先需要点击按钮进行智能验证,如果验证不通过,则会弹出滑动窗口,拖动滑块拼合图像进行验证。之后三个加密参数会生成,通过表单提交到后台,后台还会进行一次验证。极验验证码增加了机器学习的方法来识别手动轨迹,采用多重静态和动态防御模型来防止模拟轨迹和伪造设备浏览器环境以及暴力识别攻击。破解该验证码的过程比较烦琐,需要分析加密和校验逻辑。
先找一个带有极验证的网站,这里以博客园的登录为例进行验证。极验的官方登录网站是 Geetest Account后台
该按钮是智能验证按钮。一般来说,如果是同一个会话,一段时间内第二次点击会直接通过验证。如果智能识别不通过,则会弹出滑动验证窗口,需要拖动滑块图像完成二步验证。验证成功后,验证按钮会提示验证成功。接下来就是提交表单。 经过上述分析,极验验证需要完成下面三步: (1)模拟点击验证按钮。 (2)识别滑动缺口的位置。 (3)模拟手动滑块。 第(1)步操作相对简单,可使用 Selenium 模拟点击按钮。 第(2)步操作识别缺口的位置很关键,需要用到图像的相关处理方法。首先观察缺口的样子,
描述1: 缺口周围存在明显的断裂和区别,因此可以使用边缘检测算法来定位缺口的位置。在极验验证码中,缺口在滑块未移动前不会出现。
描述2: 为了找到极验验证码的缺口位置,可以使用对比方法来处理两张图片。通过设置对比阈值,并在两张图片中查找 RGB 差异大于该阈值的像素点的位置,可以找到缺口的位置。
描述3: 在第三步中,需要注意极验验证码的机器轨迹识别。为了通过验证,必须准确模拟人类的移动轨迹,包括加速和减速过程。匀速移动和随机速度移动等方法都不能通过验证。
这里选定的连接是博客园的登录页面 https://passport.cnblogs.com/user/signin,在这里首先要初始化一些配置,如 Selenium 对象的初始化及一些参数的配置。如下所示:
1 importtime2 from io importBytesIO3 from PIL importImage4 from selenium importwebdriver5 from selenium.webdriver importActionChains6 from selenium.webdriver.common.by importBy7 from selenium.webdriver.support.ui importWebDriverWait8 from selenium.webdriver.support importexpected_conditions as EC9 10 USERNAME = 'xx'11 PASSWORD = 'xxx'12 13 classCrackGeetest():14 def __init__(self):15 self.url = 'https://passport.cnblogs.com/user/signin'16 self.browser =webdriver.Chrome()17 self.wait = WebDriverWait(self.browser, 20)18 self.username =USERNAME19 self.password =PASSWORD20 21 def __del__(self):22 self.browser.close()
定义一个方法获取登录按钮,因为用户名和密码后需要先点击登录才能调出极验按钮。代码如下所示:
1 defget_login_button(self):2 """3 获取登录按钮,调出极验验证码4 :return: 登录按钮对象5 """6 button_login = self.wait.until(EC.element_to_be_clickable((By.CLASS_NAME, 'button')))7 return button_login
模拟点击初始的验证按钮,定义一个方法来获取这个按钮,利用显示等待的方法来实现。如下所示:
1 defget_geetest_button(self):2 """3 获取初始验证按钮4 :return: 按钮对象5 """6 button = self.wait.until(EC.element_to_be_clickable((By.CLASS_NAME, 'geetest_radar_tip')))7 return button
获取一个 WebElement 对象,调用它的 click() 方法即可模拟点击,代码如下所示:
# 点击验证按钮
1 button =sele.get_geetest_button()2 button.click()
下面识别缺口的位置。需要获取前后两张比对图片,二者不一致的地方即为缺口,得到其所在位置和宽高,然后获取整个网页的截图,图片裁切出来即可,代码实现如下:
1 defget_position(self):2 """3 获取验证码位置4 :return: 验证码位置元组5 """6 img = self.wait.until(EC.presence_of_element_located((By.CLASS_NAME, 'geetest_canvas_img')))7 time.sleep(2)8 location =img.location9 size =img.size10 top, bottom, left, right = location['y'], location['y'] + size['height'], location['x'], location['x'] + size['width']11 return(top, bottom, left, right)12 13 defget_screenshot(self):14 """15 获取网页截图16 :return: 截图对象17 """18 screenshot =self.browser.get_screenshot_as_png()19 screenshot =Image.open(BytesIO(screenshot))20 returnscreenshot21 22 defget_slider(self):23 """24 获取滑块25 :return: 滑块对象26 """27 slider = self.wait.until(EC.element_to_be_clickable((By.CLASS_NAME, 'geetest_slider_button')))28 returnslider29 30 def get_geetest_image(self, name='captcha.png'):31 """32 获取验证码图片33 :return: 图片对象34 """35 top, bottom, left, right =self.get_position()36 print('验证码位置', top, bottom, left, right)37 screenshot =self.get_screenshot()38 captcha =screenshot.crop((left, top, right,bottom))39 return captcha
这里 get_position() 函数首先获取验证码图片对象的位置和宽高,随后返回其左上角和右下角的坐标。get_screenshot() 函数获取网页截图。
get_slider() 函数获取滑块对象,有的验证码是点击这个滑块后才出现带缺口的图片,有的是直接出现带缺口的图片。获取到滑块对象后调用 click() 方法触发点击,可使缺口图片呈现出来:
# 点按呼出缺口图片
1 slider =self.get_slider()2 slider.click()
get_geetest_image() 方法获取网页截图,调用 crop() 方法将图片裁切出来,返回 Image 对象。
分别将不带缺口的图片和带缺口的图片赋值给变量 image1 和 image2,接下来对比图片获取缺口。这里采用遍历图片的每个坐标点,获取两张图片对应像素的 RGB 数据。如果二者的RGB 数据差距在一定范围内,就代表两个像素相同,继续比对下一个像素点。如果差距超过一定范围,则代表像素点不同,当前位置即为缺口位置,代码实现如下:
1 defopen(self):2 """3 打开网页输入用户名密码4 :return: None5 """6 self.browser.get(self.url)7 username = self.wait.until(EC.presence_of_element_located((By.ID, 'input1')))8 password = self.wait.until(EC.presence_of_element_located((By.ID, 'input2')))9 username.send_keys(self.username)10 password.send_keys(self.password)11 12 defget_gap(self, image1, image2):13 """14 获取带缺口的偏移量15 :param image1: 不带缺口的图片16 :param image2: 带缺口的图片17 :return:18 """19 left = 6020 for i inrange(left, image1.size[0]):21 for j in range(image1.size[1]):22 if notself.is_pixel_equal(image1, image2, i, j):23 left =i24 returnleft25 returnleft26 27 defis_pixel_equal(self, image1, image2, x, y):28 """29 判断两个像素是否相同30 :param image1: 图片131 :param image2: 图片232 :param x: 位置x33 :param y: 位置y34 :return: 像素是否相同35 """36 #取两个图片的像素点37 pixel1 =image1.load()[x,y]38 pixel2 =image2.load()[x,y]39 threshold = 6040 if abs(pixel1[0] - pixel2[0]) < threshold and abs(pixel1[1] - pixel2[1]) < threshold andabs(41 pixel1[2] - pixel2[2]) <threshold:42 returnTrue43 else:44 return False
使用open()方法打开登录页面并输入用户名和密码。使用get_gap()方法获取带缺口图片和不带缺口图片的缺口位置。get_gap()方法的实现是通过遍历两张图片的像素,使用is_pixel_equal()方法比较同一位置像素是否相同,如果像素RGB绝对值都小于预设的阈值,则代表像素点相同。如果像素点不同,则代表缺口在此位置。 两张图片中明显不同的地方是待拼合的滑块和缺口。滑块的位置出现在左侧,缺口在同一水平线的右侧。因此,缺口可以从滑块右侧开始寻找。可以将遍历起始横坐标设置为60,从滑块右侧开始识别缺口位置。当识别到缺口位置后,就可以模拟拖动操作。
极验验证码的模拟拖动过程需要避免匀速运动,因为完全匀速拖动是无法通过机器学习模型的验证的。为了绕过这个问题,需要使用加速减速的方法。拖动过程需要划分为几段,每段设置一个平均速度,速度围绕平均速度小幅度随机变化,但这种方法仍然无法通过验证。因此,需要使用完全模拟加速减速的过程来完成验证。在这个过程中,滑块的运动轨迹需要先做匀加速运动,再做匀减速运动。通过应用物理学中的加速度公式,可以得到加速度、速度、位移和所需时间之间的关系式。这两个公式分别是:
x = v0 * t + 0.5 * a * t * t v = v0 + a * t
通过这些公式,可以构造轨迹移动算法,计算出加速减速的运动轨迹,并使用代码来实现。
1 defget_track(self, distance):2 """3 根据偏移量获取移动轨迹4 :param distance: 偏移量5 :return: 移动轨迹6 """7 #移动轨迹8 track =[]9 #当前位移10 current =011 #减速阈值12 mid = distance * 4 / 513 #计算间隔14 t = 0.215 #初速度16 v =017 18 while current <distance:19 if current <mid:20 #加速度为正221 a = 222 else:23 #加速度为负324 a = -325 #初速度v026 v0 =v27 #当前速度v = v0 + a * t28 v = v0 + a *t29 #移动距离 x = v0*t + 1/2 * a * t^230 move = v0 * t + 0.5 * a * t *t31 #当前位移32 current +=move33 #加入轨迹34 track.append(round(move))35 return track
这段代码实现了一个get_track()方法,其参数是移动的总距离,返回的是运动轨迹。该运动轨迹由一个列表track表示,其中的每个元素代表每次移动的距离。 在代码中,变量mid是减速阈值,即加速到什么位置开始减速。这里的mid值为4/5,即模拟前4/5的路程是加速过程,后1/5的路程是减速过程。 当前位移距离变量current初始为0,进入while循环,其条件是当前位移小于总距离。在循环中,使用分段方式定义了加速度,其中加速过程的加速度定义为2,减速过程的加速度定义为-3。然后,使用位移公式计算某个时间段内的位移,并将当前位移更新并记录到轨迹列表中。循环终止时,得到的track列表记录了每个时间间隔移动的距离,这样就得到了滑块的运动轨迹。最后,按照运动轨迹拖动滑块即可。代码如下:
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
USERNAME = 'xx'
PASSWORD = 'xxx'
BORDER = 6
class CrackGeetest():
def __init__(self):
self.url = 'https://passport.cnblogs.com/user/signin'
self.browser = webdriver.Chrome()
self.wait = WebDriverWait(self.browser, 20)
self.username = USERNAME
self.password = PASSWORD
def __del__(self):
self.browser.close()
def get_login_button(self):
"""
获取登录按钮,调出极验验证码
:return: 登录按钮对象
"""
button_login = self.wait.until(EC.element_to_be_clickable((By.ID, 'signin')))
return button_login
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)
username = self.wait.until(EC.presence_of_element_located((By.ID, 'input1')))
password = self.wait.until(EC.presence_of_element_located((By.ID, 'input2')))
username.send_keys(self.username)
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