登录
首页精彩阅读Python高效实现滑块验证码自动操纵
Python高效实现滑块验证码自动操纵
2022-02-17
收藏
Python高效实现滑块验证码自动操纵

CDA数据分析师

出品作者:CDALevel Ⅰ

持证人岗位:数据分析师

行业:大数据

众所周知,规范性的网络爬虫可以帮助Decision-maker在低成本下获得想要的信息,不仅如此,做科研、写论文、包括现在的大学生都可以利用该技术获得相应的数据。在数据为王的时代,网络爬虫是每个coder都需要必备的技能。然而,随着爬虫事业的发展,网站也开始部署了一些反爬虫措施,最典型的莫过于12306等网站。其中,验证码是常用的表单登录反爬虫措施。coder在编写程序爬虫的时候或多或少都会遇到验证码的人机判别。当然,验证码不是一成不变的,随着互联网技术的发展,验证码的样式也变得越来越多。比如包括中文汉字、中文符号、英文字符的图形验证码、移动滑块验证码、滑动宫格验证码、计算题验证码、点触验证码等等。

本文选择目前市场上较为流行且难度较大的移动滑块验证码,并在介绍其样式的基础上进一步选择更为高级的无原图有空隙的移动滑块验证码,利用Python语言结合超级鹰平台高效实现滑块路径长度的计算,并以小不点搜索网址(https://www.xiaoso.net/)作为案例作为代码实现,使读者更为直观的体验。

本文采用Anaconda进行Python编译,主要涉及的Python模块:

  • PIL
  • chaojiying
  • time
  • selenium

本章分为三部分讲解:

  1. 移动滑块验证码样式背景
  2. 解决原理与创新点
  3. 案例与代码

一、移动滑块验证码样式背景

移动滑块验证码目的是让使用者将指定的部分拖动到缺口部分,完成图形的拼合。目前市场上主要有两种样式,一种是无空隙的,换句话说就是拖动的滑块一开始是处于左侧边缘的,如下图所示,此类我称之为无空隙移动滑块。

Python高效实现滑块验证码自动操纵

另一种则是有空隙的,也即拖动的滑块一开始不在左侧边缘,而是与左侧边缘存在一定距离的,最常见的就是如下图所示的企鹅拼图。

Python高效实现滑块验证码自动操纵

除此样式的不同之外,更深层次的区别有无原图与有原图。熟悉网络爬虫技术的coder都知道,在网页源代码中是存在一个frame框架来装验证码整个验证程序,而在解决怎么自动破解这个移动滑块验证码的时候,我们需要下载验证码原图,通过一系列技术发现拖动滑块与缺口的距离(第二部分会详解)。而这张原图就是区别所在,下面为两个定义的解释:

无原图验证码:只有带滑块的图,没有不带滑块的原图

有原图验证码:有不带滑块的图

上面的注释可能有些云里雾里,那么以无空隙移动滑块验证码举例,无原图验证码和有原图验证码的图片如下:

Python高效实现滑块验证码自动操纵

二、解决原理与本文创新点

在每个网站进行表单登录,如间隔较长时间进行微信登录就有移动滑块验证码的出现,也有在对多页进行信息爬取的时为防止是机器操纵而弹出的移动滑块验证码。而这些都需要我们在代码中判别是否需要进行验证码拖动。以微信登录为例子,由于验证码在第一次登录肯定是会弹出来的,因此,我们只需在代码中一步一步来即可,不用加判断条件。

而后便是移动滑块验证码自身的解决问题。换位思考一下,如果不用机器用的是人工,那么我们会根据肉眼的信息自然而然的判断将滑块拖动到缺口位置,但是机器是没有肉眼和判断力。因此,作为coder,应该给予机器判别能力,能做的即是告诉机器滑块的位置以及滑块移动的距离。

在第一部分讲解了移动滑块验证码有两种类别,有空隙和无空隙的。先介绍无空隙情况下如何实现滑块移动距离的计算。

市面上常用的方式是有边缘检测算法,该算法是基于在移动滑块验证码中,缺口的位置四周边缘有明显的断裂痕迹。边缘检测算法有Canny算子、Sobel算子等进行计算,大致过程就是将原始的移动滑块验证码图片进行图像灰度化、高斯平滑继而识别到边缘,整个过程可以借助Python中的opencv库进行。识别到边缘后,对缺口和滑块的轮廓面积做一个限定条件,接着计算滑块和缺口的质心,进而计算所需位移。不同算子的边缘检测算法准确度是不一样的,普遍都较低且位移误差大。耗时方面,由于是对图像进行识别,可想而知其速度是不尽人意的。对于无空隙的移动滑块验证码的情况下,coder仅仅把缺口的质心位置识别出来即可。而对于有空隙的情况下,就需要计算滑块和缺口的质心,即设定滑块和缺口的轮廓条件,继而计算滑块的移动距离。

市面上除了边缘检测算法外,还有一种准确度较高的,也是普遍再用的方法。这种方法是将带缺口的移动滑块验证码和不到缺口的移动滑块验证码进行RGB像素做差。设定一个阈值并遍历两张图片中的RGB像素,如果相同地方上的差值小于这个阈值那么认为图像基本是 一致的,反之则是不一致,可以认为是缺口。找到这个缺口后,对于无空隙的验证码图片来说将缺口的质心横坐标计算出来即是滑动的距离。而对于有空隙的验证码图片来说,就需要有原图移动滑块验证码图片和不带缺口的移动滑块验证码图片,这样滑块和缺口的位置才能通过RGB像素对比得到出来。然而,在大多网页上是不存在有原图移动滑块验证码图片的,需要通过一系列设置才能够得到,这将在第三部分案例中详细讲到。这个方法可以明显感觉到准确度是比较大的,但是要求也是比较苛刻,需要有原图移动滑块验证码图片。

下面从公式角度理解滑块移动距离的计算,首先是无空隙的移动滑块验证码,如下图

Python高效实现滑块验证码自动操纵

可以看到,当coder求出滑块的质心坐标$C{s}$和缺口的质心坐标$C{g}$后,直接用横坐标$C{g1}-C{s1}$相减即可。要计算的滑块移动距离$d$即为$d=C{g1}-C{s1}$,上面讲解过其实无空隙的移动滑块验证码在编程中可以偷懒,因为半个滑块的长度有时在某些网页中是被允许接受的,也即直接计算缺口的质心$C_{s}$作为移动距离$d$也是可以的。

接着讲解有空隙的移动滑块验证码的情况,如下图:

Python高效实现滑块验证码自动操纵

有空隙的情况就可能复杂点,coder直接利用$d=C{g1}-C{s1}$公式得到滑块的计算距离是最快的。而如果想要像无空隙移动滑块验证码一样偷懒的话是不行的,如上图直接计算质心$C{g}$的横坐标外,还需要减上空隙的距离、滑块的一半长度,即$d = C{g1}-d{g}-d{m}$。

  • 创新点
  • 因此,本文在滑块的移动距离计算方面创新性的提出一种高效且准确度极高的一种方法,借助验证码识别平台---超级鹰。它的技术就是使用目前AI领域最为流行的深度学习进行验证码识别。超级鹰提供多种编程语言的调用API,当然就包括本文需要的Python编程语言。下载地址为https://www.chaojiying.com/download/Chaojiying_Python.rar。除此之外,使用者需要在该平台上注册账号。注册账号之后方能调用API。超级鹰可以对多种类别的验证码进行识别,如数字图像验证码、中文汉字图像验证码、纯英文验证码、不定长汉字英文数字等,本文需要用到的移动滑块验证码同样也有。在编程程序中,我们只需在相应的位置上写上对应的验证码类型即可。
  • 利用该方法,可以对移动滑块验证码进行一刀切解决,不用考虑是否有无原图、有原图这种情况。如果是采用有空隙的移动滑块验证码图片,那么就会返回滑块和缺口的坐标$C{s}$和$C{g}$,coder继而在利用横坐标相减即可。
  • 这种方式不仅在耗时上得到成倍的缩减,并且准确度也是极高的。

三、案例与代码

在了解了无原图、有原图、无空隙、有空隙相关特征的滑块验证码后,本文选择无原图有空隙的滑块验证码作为案例,选择小不点搜索网站(https://www.xiaoso.net/)为案例网站,进去以后如下图所示:

Python高效实现滑块验证码自动操纵

该网站是一个类似于百度、搜狗的这种搜索引擎。它的特点在于,他主要是视频结果呈现,给出一句关键词后,会给出在知名视频平台(B站、youku、qq等)上有关该关键词的视频。因此,小不点对于经常找相关视频材料的小伙伴们也是一个大的福利,可以节省下不少的时间。

进去以后,可以看到右上角有个登录页面,点进去后呈现如下页面

Python高效实现滑块验证码自动操纵

由于我们是第一次进入,也即是新用户,因此,直接进行新用户注册即可,无需扫码,填写手机号等信息。注册之后,读者们可以尝试登录,在输入完用户名和密码后会弹出一个验证码,该验证码就是本文重点讲的无原图有空隙移动滑块验证码。我们需要用Python编程实现自动登录(自动表单登录)

Python高效实现滑块验证码自动操纵

Tips:有读者会问实现这个小不点自动登录有什么用呢?本文可以给出答案:这个小不点网站仅在本文做一个例子体现,让读者可以清洗明了如何使用Python实现自动登录。在往后实现QQ邮箱自动登录、爬虫时遇到反爬虫就可以举一反三。

接下来将讲解如何在Python中实现小不点网站的自动登录。

首先,我们需要明确下思路,我们做这个的自动登录的流程图应如下所示:

Python高效实现滑块验证码自动操纵
  • 登录页面解析

其实整个登录页面是很比较简洁的,我们让机器实现自动输入英文用户名密码点击提交即可。整个过程只有三步,那如何让机器找到填写与点击的位置呢,我们就需要找到网页的源代码,在源代码中找到三个元素对应的相关代码即可。

如何打开网页源代码?在所在页面按F12即可,然后同时按住ctrl+shift+C即可快捷的进入到鼠标拖动模式,该模式下利用鼠标点击即可到达相应位置的源代码,比如点击"英文用户名",效果如图下所示

Python高效实现滑块验证码自动操纵

可以看到"英文用户名"元素的类名为input,是一个输入文本类型,那么如何取出其代码呢?其实这里所说的代码只是笼统的概念,本质上是让机器识别出元素的位置。那么识别元素的位置在网页中有许多方式,例如利用id去导航,class类去导航、css等。本文则主要选择xpath去导航,xpath可以理解为机器(Python中的selenium工具)使用路径表达式在 XML 文档中进行导航。除此之外,本文还少量用到name属性,如上面那张图,name=“username”在该网页中是有且只有一个的,那么就可以直接同name进行导航。

具体怎么得到这些元素的相关属性实现导航呢,很简单,在右侧对应代码中鼠标右键点击,找到copy,选择"copy xpath"即可。

同理,英文用户名密码提交均可以使用上述方法。此外,点击提交后会弹出移动滑块验证码。这个验证码在实现滑块的拖动与点击依旧是上述的xpath元素定位方法。但稍有不同的是,需要先切换到验证码所在frame中。frame为框,可以理解为在一个网页中嵌套的页面,frame不是独立的页面而是原本网页中一个产物。因此,在编程的时候需要注意,要从原本的frame中切换到验证码所在的frame,不然会报错。

  • Python操纵
  • 在利用Python实现小不点搜索网表单自动登录时,需要用到一个非常经典的网站自动化测试库---selenium。这个库支持各种浏览器如Google、Firebox等主流页面。不仅如此,还支持Windows、Linus等系统。
  • 安装完selenium以后,就需要学习如何使用并结合"登录页面解析"部分实现元素定位。相关代码如下:
from selenium import webdriver
from selenium.webdriver.common.action_chains import ActionChains import time
from selenium.webdriver.support.ui import WebDriverWait
from chaojiying import Chaojiying_Client
    # 启动浏览器
    def Launch_browser(self): self.driver = webdriver.Chrome() self.wait = WebDriverWait(self.driver, 10, 0.5) self.driver.get(self.url)
        account = self.account
        pwd = self.pwd self.driver.find_element_by_name('username').send_keys(account) self.driver.find_element_by_name('password').send_keys(pwd) self.driver.find_element_by_xpath(self.enter).click()
        # 等待className为geetest_slider_button的元素在元素表中出现
        time.sleep(2)

这段代码是使用类定义编写的,首先引入相关库。

webdriver.Chrome()这句函数是调出Google浏览器,比如在anaconda中的spyder编译器,会弹出一个Google浏览器页面。这相当于完成机器自动打开浏览器这一操作。接下来就是让机器跳转到小不点搜索网站,用的是self.driver.get(self.url)这一函数。self.url这里指的是小不点搜索网站的网址。

跳转后就会到熟悉的登录界面,在界面中如何导航到英文用户名和密码对应的位置并输入进去呢?代码中

account = self.account ##输入小不点注册的账号 pwd = self.pwd #输入小不点注册的密码 self.driver.find_element_by_name('username').send_keys(account) # 找到账号位置并给予信息 self.driver.find_element_by_name('password').send_keys(pwd) #找到密码位置并给予信息 self.driver.find_element_by_xpath(self.enter).click() #模拟点击提交按钮

便是导航到元素对应位置与点击提交的操纵,可以看到本文使用name元素定位的,这也应征了上一部分所说,'username'和'password'在该页面是独立存在。最后的代码time.sleep(2)是防止网速过慢导致滑块验证码出现延迟而跟不上机器识别的节奏导致的问题出现。

接下来就是移动滑块验证码的破解,破解当然需要超级鹰,但是超级鹰服务的引入下一部分在说明。而破解的前提是要获取移动滑块验证码的图片,这里本文将利用selenium库中的截屏功能,将移动滑块验证码的图片整一张保存至本地中。代码如下

# 移动滑块验证码截图 def get_picture(self): self.driver.switch_to_frame(self.frame) #一定要转到验证码的框,才能定位!!! self.slider = self.driver.find_element_by_xpath(self.slider_xpath) ## 先将滑块隐藏,获取原图,在截图,在复原 self.driver.execute_script("document.getElementsByClassName('{}')[0].style['display'] = 'none'".format(self.gap_xpath)) self.driver.find_element_by_xpath('//*[@id="slideBgWrap"]').screenshot(self.file_path) self.driver.find_element_by_xpath(self.img_xpath).screenshot(self.file_path2) self.driver.execute_script("document.getElementsByClassName('{}')[0].style['display'] = 'block'".format(self.gap_xpath))

首先依旧采用类与对象的写法,第一行代码是将frame转到移动滑块验证码所在的框中,第二行则是通过xpath路径导航到滑块的地方。接着则是验证码截图部分重要的地方,前面讲了该案例是解决无原图有空隙的移动滑块验证码。如果整个过程交给了超级鹰那么无原图有空隙这两个问题变迎刃而解。但是,如果采用市面上常用的方法呢,那么无原图怎么解决呢?第3行到最后一行代码就是答案。同样是利用selenium库的功能,首先通过多次试验,作者发现在网页源代码上做特定的修改就会使验证码图像变回原来的图像,不会带缺口的。修改方式就是在
document.getElementsByClassName('{}')[0]
处赋予没有缺口属性.style['display'] = 'none'。赋予之后就会呈现原图,接着利用xpath导航至验证码原图的位置并screenshot截图一下即可完成移动滑块验证码图片的本地保存。最后复原验证码图片,在修改处改为.style['display'] = 'block',即可重新出现缺口。下载下来的图片如下图所示(tips:由于每一次登录的验证码图片都不一致,这里展现实验时的一次。)

Python高效实现滑块验证码自动操纵
  • 超级鹰服务引入

得到了验移动滑块验证码图片之后,我们需要计算滑块到缺口的距离,这时候使用Chaojiying无疑是高效实现。

在超级鹰平台上有一个名为"Chaojiying"的Python API接口,是平台写好并让Python user来直接调用的,下载地址为:
https://www.chaojiying.com/download/Chaojiying_Python.rar。而在main主程序上我们只需要在同一路径下实现该代码即可

from chaojiying import Chaojiying_Client

引入该服务后,需要上传在超级鹰平台注册的账号密码,还有需要识别的验证码图片。具体代码如下:

# 超级鹰 def cjy(self): # 用户中心>>软件ID 生成一个替换 910001 self.chaojiying = Chaojiying_Client('', '', '') # chaojiying = Chaojiying_Client('超级鹰用户名', '超级鹰密码', '910001') # 本地图片文件路径 来替换 a.jpg 有时WIN系统须要// im = open('./img/code_picture2.png', 'rb').read() #验证码图片存放位置 # 9101 验证码类型  官方网站>>价格体系 3.4+版 print 后要加() # 咨询了一下滑动验证码是选择9101 re = self.chaojiying.PostPic(im, 9101) print('两个坐标') print(re) # 减去一半滑块长度 self.diff = int(re['pic_str'].split(',')[0]) self.distance = int(re['pic_str'].split('|')[1].split(',')[0]) - self.diff print(self.distance) self.im_id = re['pic_id'] print(self.im_id)

解析:首先读者需要在Chaojiying_client这句函数里面写上相关的用户名、密码和软件ID。其次以二进制格式读取移动滑块验证码图片并使用Chaojiying内置函数Postpic上传至平台。这样平台就会返回我们滑块到缺口gap的坐标,我们利用两坐标相减即可得到路径。注意:在得到路径上应该减去一半滑块长度,在第二部分已经讲解为何减去一半滑块长度。

在得到滑块移动的路径后,更谨慎的我们还需要赋予滑块一个速度,以防止网站发现是机器人操纵。具体代码如下:

# 获取移动轨迹 def get_track(self): self.track = []
        mid = self.distance * 3 / 5 current = 0 t = 0.2 v = 0 while current < self.distance: if current < mid: a = 8 else: a = -12 v0 = v
            v = v0 + a * t
            move = v * t + 1 / 2 * a * t * t
            current += move self.track.append(round(move))
        print(self.track) # 模拟移动 def move(self): self.track = self.track 
        ActionChains(self.driver).click_and_hold(self.slider).perform() for x in self.track: ActionChains(self.driver).move_by_offset(xoffset=x, yoffset=0).perform()
        time.sleep(2)
        ActionChains(self.driver).release().perform() self.slider.click()

第一个def类---get_track就是定义滑块的移动每0.2秒的移动距离。本文定义为在移动路径的$frac{2}{5}$路径之前加速度为8,即做加速直线运动。后半程的加速度为-12即做减速直线运动。结合速度公式和位移公式即可得到每一步滑块需要走多远。

最后使用selenium库中的悬停鼠标工具ActionChains拖拽住滑块,遍历每一步的步长,讲滑块移动到缺口位置。

  • 总代码

可以看到上述的代码时以类方式封装的,因此介绍上使用者如何运行代码,首先是主程序main

from no_img import scratch_main
url = 'https://www.xiaoso.net/m/member/action/login'
username = '' #输入小不点账号 password = '' #输入小不点密码 enter_xpath = '//*[@id="TencentCaptcha"]'
frame = 'tcaptcha_iframe'
slider_xpath = '//*[@id="tcaptcha_drag_thumb"]'
gap_xpath = 'tc-jpp-img unselectable'
img_xpath = '//*[@id="slideBgWrap"]'
process = scratch_main(url,username,password,enter_xpath,frame,slider_xpath,gap_xpath,img_xpath)
process.main()

这个需要使用者在username和password上输入自己在小不点上注册的账号密码,接着是no_img程序,

'''
无原图滑块验证码案例
author:henry
''' from selenium import webdriver
from selenium.webdriver.common.action_chains import ActionChains
import time
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
from PIL import Image
from chaojiying import Chaojiying_Client class scratch_main(): def __init__(self,url,username,password,enter_xpath,frame,slider_xpath,gap_xpath,img_xpath): self.url = url self.file_path = './img/code_picture.png' self.file_path2 = './img/code_picture2.png' self.distance = 0 self.key = 0 self.account = username self.pwd = password self.enter = enter_xpath self.frame = frame self.slider_xpath = slider_xpath self.gap_xpath = gap_xpath self.img_xpath = img_xpath # 启动浏览器 def Launch_browser(self): self.driver = webdriver.Chrome() self.wait = WebDriverWait(self.driver, 10, 0.5) self.driver.get(self.url)
        account = self.account
        pwd = self.pwd
        time.sleep(2) self.driver.find_element_by_name('username').send_keys(account) self.driver.find_element_by_name('password').send_keys(pwd) self.driver.find_element_by_xpath(self.enter).click()
        time.sleep(2) self.driver.switch_to_frame(self.frame) #一定要转到验证码的框,才能定位!!! self.slider = self.driver.find_element_by_xpath(self.slider_xpath) # 截图 def get_picture(self): ## 先将滑块隐藏,获取原图,在截图,在复原 self.driver.find_element_by_xpath(self.img_xpath).screenshot(self.file_path2) self.driver.execute_script("document.getElementsByClassName('{}')[0].style['display'] = 'block'".format(self.gap_xpath)) # 分割截图获取验证码图片,由于使用超级鹰,所以直接用上面截图验证码部分即可,不用截图缺口 def crop_picture(self):
        pass # 超级鹰 def cjy(self): # 用户中心>>软件ID 生成一个替换 910001 self.chaojiying = Chaojiying_Client('', '', '') # chaojiying = Chaojiying_Client('超级鹰用户名', '超级鹰密码', '910001') # 本地图片文件路径 来替换 a.jpg 有时WIN系统须要// im = open('./img/code_picture2.png', 'rb').read()
        re = self.chaojiying.PostPic(im, 9202)
        print('两个坐标')
        print(re) # print(re['pic_str']) # 减去一半滑块长度 self.diff = int(re['pic_str'].split(',')[0]) self.distance = int(re['pic_str'].split('|')[1].split(',')[0]) - self.diff
        print(self.distance) self.im_id = re['pic_id']
        print(self.im_id) # 获取轨迹 def get_track(self): self.track = []
        mid = self.distance * 2 / 5 current = 0 t = 0.2 v = 0 while current < self.distance: if current < mid: a = 8 else: a = -12 v0 = v
            v = v0 + a * t
            move = v * t + 1 / 2 * a * t * t
            current += move self.track.append(round(move))
        print(self.track) # 模拟移动 def move(self): self.track = self.track 
        ActionChains(self.driver).click_and_hold(self.slider).perform() for x in self.track: ActionChains(self.driver).move_by_offset(xoffset=x, yoffset=0).perform()
        time.sleep(2)
        ActionChains(self.driver).release().perform() self.slider.click() def check(self):
        pass # 关闭浏览器 def quit(self):
        time.sleep(5) self.driver.quit() # main方法 def main(self): self.Launch_browser() self.get_picture() self.crop_picture() self.cjy() self.get_track() self.move() self.check() if __name__ == '__main__':
    ma = scratch_main()
    ma.main()

这里需要使用者在cjy类中输入自己在超级鹰上注册的账号密码和软件ID。此外,使用者需要把mainChaojiyingno_img三个程序放到一个相同路径下。

数据分析咨询请扫描二维码

客服在线
立即咨询