极验验证码:需要手动拼合滑块来完成的验证,相对图形验证码识别难度上升了几个等级。下面用程序识别并通过极验验证码的验证,其中有分析识别思路、识别缺口位置、生成滑块拖动、模拟实现滑块拼合通过验证等步骤。需要用到Chrome 浏览器,并配置 ChromeDriver ,要用到的 Python 库是 Selenium。
1、 对极验验证码了解
极验验证码官网:http://www.geetest.com/。一个专注提供验证安全的系统,主要验证方式是拖动滑块拼命图像。若图像完全拼合,则验证成功,即表单提交,否则需要重新验证。
现在极验验证码使用的企业很多,每天有超过几亿次的响应。极验验证码广泛用于直播视频、金融服务、电子商务、游戏娱乐、政府企业等各大类网站。
2、 极验验证码特点
识别难度大。首先要点击按钮进行智能验证,如果验证不通过,则会弹出滑动窗口,拖动滑块拼合图像进行验证。之后三个加密参数会生成,通过表单提交到后台,后台还会进行一次验证。
极验验证码增加了机器学习的方法来识别手动轨迹。其官方网站的安全防护有下面几点说明:
三角防护之防模拟:恶意程序模仿人类行为轨迹对验证码进行识别。利用机器学习和神经网络,构建线上线下的多重静态、动态防御模型,识别模拟轨迹,界定人机边界。
三角防护之防伪造:恶意程序通过伪造设备浏览器环境对验证码进行识别。利用设备基因技术,深度分析浏览器的实际性能来辨识伪造信息,同时根据依靠事件不断更新黑名单来提高防依靠能力。
三角防护之防暴力:恶意程序在短时间内进行密集的攻击,对验证码进行暴力识别。为了防暴力,极验验证码有多种形态,每种形态都用神经网络生成海量图库储备,每一张图片都是独一无二的,且图库不断更新,极大程度提高防暴力识别成本。
3、 识别思路
对于极验验证码,直接模拟表单提交,加密参数构造较困难,需要分析其加密和校验逻辑,相对烦琐。所以直接采用模拟浏览器动作的方式来完成验证。使用 Python 的 Selenium 库模拟人的行为方式来完成验证,成本要相对去识别加密算法少很多。如图所示。
!极验验证码实例](https://img2018.cnblogs.com/blog/1501522/201905/1501522-20190520165616663-50859391.png)
先找一个带有极验证的网站,这里以博客园的登录为例进行验证。极验的官方登录网站是 https://account.geetest.com/login,也可以在其官网上做测试。在博客园的登录页面点击登录后会出现一个极验验证按钮。如图2-2所示。
该按钮是智能验证按钮。一般来说,如果是同一个会话,一段时间内第二次点击会直接通过验证。如果智能识别不通过,则会弹出滑动验证窗口,需要拖动滑块图像完成二步验证。验证成功后,验证按钮会提示验证成功。接下来就是提交表单。
经过上述分析,极验验证需要完成下面三步:
(1)模拟点击验证按钮。
(2)识别滑动缺口的位置。
(3)模拟手动滑块。
第(1)步操作相对简单,可使用 Selenium 模拟点击按钮。
第(2)步操作识别缺口的位置很关键,需要用到图像的相关处理方法。首先观察缺口的样子,如图所示。
缺口的四周边缘有明显的断裂边缘,边缘与边缘周围有明显的区别。可用边缘检测算法来找出缺口的位置。对于极验验证码,可以利用和原图对比检测方式识别缺口的位置,通常在没有拖动滑块前,缺口没有呈现。
可以同时获取两张图片,设定一个对比阈值,然后遍历两张图片,找出相同位置像素 RGB 差距超过此阈值的像素点,此像素点的位置就是缺口的位置。
第(3)步中要注意的是,极验验证码增加了机器轨迹识别,匀速移动、随机速度移动等方法都不能通过验证,只有完全模拟人的移动轨迹才可以通过验证。人的移动轨迹一般是先加速后减速,要对这个过程模拟才能成功。
要包括这几个步骤。
第一步,初始化,在这里我们先初始化 一些selenium的 配置及一些参数的配置。
第二步,就是模拟点击了,这里主要是利用selenium模块模拟浏览器对网页进行操作。
第三步,就该识别缺口的位置了。首先获取前后两张图片,得到其所在位置和宽高,然后获取整个网页的截图,图片裁切下来即可。
最后一步,模拟拖动,经过多次试验,得出一个结论,那就是完全模拟加速减速的过程通过了验证。前段作匀加速,后段作匀减速运动,利用物理学的加速度公式即可完成验证。
位移移动
需要的基础知识
位移移动相当于匀变速直线运动,类似于小汽车从起点开始运行到终点的过程(首先为匀加速,然后再匀减速)。
其中a为加速度,且为恒量(即单位时间内的加速度是不变的),t为时间
教程一:博客园
误区一:
下面整个button区域对应着整个登录框,可以直接使用id里的标签进行定位,而不是第1个span标签
button = self.wait.until(EC.presence_of_element_located((By.ID,"submitBtn")))
误区二:注意关于图片的一些知识点
img1 = Image.open("E:/python27/pic/1.jpg")
img1.size
r, g, b = img1.split()
打开图片:Image.open(fp, mode='r')
保存图片:Image.save(fp, format=None, **params):
展示图片:Image.show(title=None, command=None):
location = img.location
# location属性可以返回该图片对象(既这张图片)在浏览器中的位置,以字典的形式返回,{‘x’:30,‘y’:30} 这里我们图片的位置是(30,30)。坐标轴是以屏幕左上角为原点,x轴向右递增,y轴像下递增。(相对整个html的坐标)
size = img.size
# 通过size获取属性大小,size属性同样返回一个字典,{‘height’:30,‘width’:30 } 即图片对象的高度,宽度。
four_corner =(location['x'],
location['y'],
location['x']+size['width'],
location['y']+size['height'])
# 通过location和size获取上下左右,注意是小写的x和y
captcha = screenshot.crop((top, bottom, left, right))
# Image.crop() 从图像中提取出某个矩形大小的图像。它接收一个四元素的元组作为参数,各元素为(left, upper, right, lower),坐标系统的原点(0, 0)是左上角。# 因为上面截图,截取到的是整个页面,现在只把验证码图片取到
image.size属性是一个列表,下标0是横坐标(宽,图片的最右边),下标1是纵坐标
image.size[0]
image.size[1]
完整代码
from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.common.by import By
from selenium.webdriver import ActionChains
from selenium import webdriver
# import PIL.Image as image
from PIL import Image
from io import BytesIO
import time
EMAIL =
PASSWORD =
BODER = 6
INIT_LEFT = 60
class CrackGeetest():
def __init__(self):
self.url = 'http://www.sf-express.com/cn/sc/dynamic_function/waybill'
self.browser = webdriver.Chrome('D:\\chromedriver.exe')
self.wait = WebDriverWait(self.browser, 100)
self.email = EMAIL
self.keyword = PASSWORD
self.BORDER = 6
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_geetest_button(self):
"""
点击按钮,弹出没有缺口的图片
:return: 返回按钮对象
"""
button = self.wait.until(EC.presence_of_element_located((By.CLASS_NAME, 'geetest_radar_tip')))
return button
def __del__(self):
self.browser.close()
def get_position(self):
"""
获取验证码位置
:return:验证码位置元组
"""
img = self.wait.until(EC.presence_of_element_located((By.CLASS_NAME, 'geetest_canvas_img')))
time.sleep(2)
# location属性可以返回该图片对象(既这张图片)在浏览器中的位置,以字典的形式返回,{‘x’:30,‘y’:30} 这里我们图片的位置是(30,30)。坐标轴是以屏幕左上角为原点,x轴向右递增,y轴像下递增。(相对整个html的坐标)
location = img.location
# size属性同样返回一个字典,{‘height’:30,‘width’:30 } 即图片对象的高度,宽度。
size = img.size # 通过sizes属性获取大小
top,bottom,left,right = location['y'],location['y']+size['height'],location['x'],location['x']+size['width'] # 通过location和size获取上下左右
return (top,bottom,left,right)
def get_screenshot(self):
"""
获取网页截图
:return: 截图对象
"""
pic.png = self.browser.get_screenshot_as_png()
screenshot = Image.open(BytesIO(pic.png)) # 转换为字节对象
'''
img1 = Image.open("E:/python27/pic/1.jpg")
img1.size
r, g, b = img1.split()
打开图片:Image.open(fp, mode='r')
保存图片:Image.save(fp, format=None, **params):
展示图片:Image.show(title=None, command=None):
'''
def get_geetest_image(self, name='captcha.jpg'):
"""
获取验证码图片
:return: 图片对象
"""
top, bottom, left, right = self.get_position() # 定位到这个位置之后,再截图
print('验证码位置',top, bottom, left, right)
screenshot = self.get_screenshot() # 获取到截图
# Image.crop() 从图像中提取出某个矩形大小的图像。它接收一个四元素的元组作为参数,各元素为(left, upper, right, lower),坐标系统的原点(0, 0)是左上角。
captcha = screenshot.crop((top, bottom, left, right)) # 因为上面截图,截取到的是整个页面,现在只把验证码图片取到
captcha.save(name) # 把图片存储起来,name是传进来的变量
return captcha
def get_slider(self):
"""
获取滑块
:return: 滑块对象
"""
slider = self.wait.until(EC.element_to_be_clickable((By.CLASS_NAME, 'geetest_slider_button')))
return slider
def is_pixel_equal(self, img1, img2, x, y):
"""
判断两个像素是否相同
:param image1: 图片1
:param image2: 图片2
:param x: 位置x
:param y: 位置y
:return: 像素是否相同
"""
# 取两个图片的像素点 (img.load()[x, y]获取某一点的像素值)
pixel1 = img1.load()[x, y] # 上面的 i 和 j
pixel2 = img2.load()[x, y]
threshold = 60 # 控制误差
if (abs(pixel1[0] - pixel2[0] < threshold) and abs(pixel1[1] - pixel2[1] < threshold) # 0 1 2 是RGB的三种颜色
and abs(pixel1[2] - pixel2[2] < threshold)):
return True
else: # 有一个超过阈值,则认为2个像素点不一致
return False
def get_gap(self, img1, img2):
"""
获取缺口偏移量
:param img1: 不带缺口图片
:param img2: 带缺口图片
:return:
"""
left = 60 # 从图片的60处开始往右,这样就可以把被拖动滑块跳过去,因为被拖动滑块处像素也不一样
# 相当于从60开始,一列列做对比
for i in range(left, img1.size[0]): # i 相当于横坐标 size属性是一个列表,下标0是横坐标(宽,图片的最右边),下标1是纵坐标
for j in range(img1.size[1]): # j 相当于纵坐标 从0开始
if not self.is_pixel_equal(img1, img2, i, j):
left = i # 如果相同位置的像素不一致,则替换为i
return left
return left
def get_track(self, distance):
"""
根据偏移量获取移动轨迹
:param distance: 偏移量
:return: 移动轨迹
"""
# 移动轨迹
track = []
# 当前位移
current = 0
# 减速阈值
mid = distance * 4 / 5
# 计算间隔
t = 0.2
# 初速度
v = 0
while current < distance: # 所以 track是不会大于总长度的
if current < mid:
# 加速度为正2
a = 2
else:
# 加速度为负3
a = -3
# 初速度v0
v0 = v
# 移动距离x = v0t + 1/2 * a * t^2,现做了加速运动
move = v0 * t + 1 / 2 * a * t * t
# 当前速度v = v0 + at 速度已经达到v,该速度作为下次的初速度
v = v0 + a * t
# 当前位移
current += move
# 加入轨迹
track.append(round(move)) # track 就是最终鼠标在 X 轴移动的轨迹
return track
def move_to_gap(self, slider, track):
"""
拖动滑块到缺口处
:param slider: 滑块
:param track: 轨迹
:return:
"""
ActionChains(self.browser).click_and_hold(slider).perform() # 利用动作链,获取slider,perform是
for x in track:
ActionChains(self.browser).move_by_offset(xoffset=x, yoffset=0).perform() # xoffset横坐标,yoffset纵坐标。使得鼠标向前推进
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()
time.sleep(0.5)
# 获取验证码图片
image1 = self.get_geetest_image('captcha1.png')
#点按呼出滑块
slider = self.get_slider()
slider.click() # 一般的验证码,只有点完slide按钮之后,才会出现缺口
# 获取带缺口的验证码图片
image2 = self.get_geetest_image('captcha2.png')
# 获取缺口位置
gap = self.get_gap(image1, image2)
print('缺口位置', gap)
# 减去缺口的位移(因为,滑块移动img1到img2的时候,允许有误差范围。在范围内,即使有几个像素差,也是可以接受的
gap -= BODER
# 获取移动轨迹
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_sucess_radar_tip_constant'),'验证成功') # 证明极验成功
)
print(success)
# 失败后重试
if not success:
self.crack()
else:
self.login()
if __name__ == '__main__':
print('开始验证')
crack = CrackGeetest()
crack.crack()
print('验证成功')