Python爬虫 | 滑动验证码破解
极验验证码:需要手动拼合滑块来完成的验证,相对图形验证码识别难度上升了几个等级。下面用程序识别并通过极验验证码的验证,其中有分析识别思路、识别缺口位置、生成滑块拖动、模拟实现滑块拼合通过验证等步骤。需要用到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)步中要注意的是,极验验证码增加了机器轨迹识别,匀速移动、随机速度移动等方法都不能通过验证,只有完全模拟人的移动轨迹才可以通过验证。人的移动轨迹一般是先加速后减速,要对这个过程模拟才能成功。
[code]要包括这几个步骤。 第一步,初始化,在这里我们先初始化 一些selenium的 配置及一些参数的配置。 第二步,就是模拟点击了,这里主要是利用selenium模块模拟浏览器对网页进行操作。 第三步,就该识别缺口的位置了。首先获取前后两张图片,得到其所在位置和宽高,然后获取整个网页的截图,图片裁切下来即可。 最后一步,模拟拖动,经过多次试验,得出一个结论,那就是完全模拟加速减速的过程通过了验证。前段作匀加速,后段作匀减速运动,利用物理学的加速度公式即可完成验证。
位移移动
需要的基础知识
位移移动相当于匀变速直线运动,类似于小汽车从起点开始运行到终点的过程(首先为匀加速,然后再匀减速)。
其中a为加速度,且为恒量(即单位时间内的加速度是不变的),t为时间
教程一:博客园
误区一:
下面整个button区域对应着整个登录框,可以直接使用id里的标签进行定位,而不是第1个span标签
[code]button = self.wait.until(EC.presence_of_element_located((By.ID,"submitBtn")))
误区二:注意关于图片的一些知识点
[code]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):
[code]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]
完整代码
[code]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('验证成功')
- Python 爬虫实战 极验滑动验证码的识别
- 滑动验证码的破解(python+opencv+selenium)
- Python项目实战:破解滑动验证码案例练习
- 2019经典版:Python爬虫验证码破解实战(内有详细代码)
- python B站 滑动验证码破解(极验)
- python破解bilibili滑动验证码登录功能
- Python 爬虫入门(四)—— 验证码下篇(破解简单的验证码)
- 爬虫之破解滑动验证码
- python爬虫学习:验证码之滑动验证码
- 爬虫项目:破解极验滑动验证码
- 爬虫项目:破解极验滑动验证码
- 爬虫项目:破解极验滑动验证码
- python爬虫之验证码篇3-滑动验证码识别技术
- Python——破解极验滑动验证码
- python爬虫-滑动验证码
- Python爬虫入门教程 57-100 python爬虫高级技术之验证码篇3-滑动验证码识别技术
- Python3网络爬虫实战-43、极验滑动验证码的识别
- Python3网络爬虫开发实战之极验滑动验证码的识别
- Python3网络爬虫实战-45、微博宫格验证码的识别