利用python实现自动通过滑动验证码
1.自动填写用户名和密码
1.工具:selenium 库
在搜索引擎的帮助下,我发现selenium 库可以编写自动填写账号密码的爬虫。
阅读selenium 库的doc,很快就能理解使用selenium 进行自动化网页操作的基本需求:
2.伟大的第一步,打开浏览器!
选取WebDriver后,就从启动浏览器开始,一开始我使用chromedriver 访问网页,指定chromedriver 的目录:
chromedriver = "C:\Program Files (x86)\Google\Chrome\Application\chromedriver.exe" driver = webdriver.Chrome(chromedriver)
chromedriver 的配置其实是相对麻烦的,首先需要找到对应版本的chromedriver.exe ,还需要放置到chrome程序目录下以及python\scripts目录下以确保可以被正常使用,实际上在csdn上还发现有需要单独将chromedriver.exe 添加到path的情况。
接下来尝试打开一些网站:
driver.get("https://www.baidu.com" ) driver.get("https://www.weibo.com" ) driver.get("https://www.google.com" )
测试后正常,然而在指定访问信息门户登录界面时,发现了异常情况:
driver.get("https://idas.uestc.edu.cn/authserver/login" )
似乎所有的图片都没有正常加载,尝试刷新后发现页面变得完全空白。起初我认为是chromedriver与chrome版本不匹配,但是再三确认后发现其他网站的访问都是正常的,于是想到信息门户可能启用了反爬虫。
由于我还没有使用selenium 在页面元素上做出其他任何操作,我在csdn上一番搜寻后看到了一种可能:
推测是Chromedriver的特征码被信息门户识别然后拒绝了我的访问。
于是抱着试一试的心态,尝试换用firefox :
from selenium import webdriverdriver = webdriver.Firefox() url='https://idas.uestc.edu.cn/authserver/login?service=http%3A%2F%2Feportal.uestc.edu.cn%2Flogin%3Fservice%3Dhttp%3A%2F%2Feportal.uestc.edu.cn%2Fnew%2Findex.html' driver.get(url)
测试发现能够成功访问,开始思考接下来的问题。
3.获取用户名框和密码框的元素
为了获取到信息门户登录的用户名和密码的元素,我使用了函数find_element_by_xpath
使用浏览器的调试工具,很容易可以获取到用户名和密码的Xpath:
//[@id=“username”]
//[@id=“password”]
然后用同样的方法获取到登录按钮的xpath,使用click()点击完成登录:
elem=driver.find_element_by_xpath('//*[@id="username"]' ) elem.send_keys('xxx' ) elem=driver.find_element_by_xpath('//*[@id="password"]' ) elem.send_keys('xxx' ) elem=driver.find_element_by_xpath('//*[@id="casLoginForm"]/p[4]/button' ) elem.click()
流程就完成了,不出意料,接下来弹出了滑动验证码。
4.关键部分:通过滑动验证码
1.思路分析
检查网页元素,确定能够获取到的资源
在一番查找后发现,信息门户的验证码图片的分两张存储,id分别为:img1,img2(我也没想到就是这么朴素的两个名字)为缺口图形和背景图形(已挖去缺口),
示例如图:
依据可获取的资源思考解决方案
与早期的三张图片的形式不同,现在的验证码只给出了两张图片,没有原图,这加大了匹配的难度。在一番查找后,发现了opencv库有matchTemplate 函数可以做粗略的模板匹配:
百度了一番之后,大致了解了该函数的工作方式,也看到了应用的实例,最终确定尝试使用opencv来完成匹配的重任
2.尝试实现的过程和遭遇的困难
1.匹配原图位置并定位到网页上图片的位置
完成登录操作后,首先获取两张图片到本地,定义一个get_pics 函数,传入参数为当前使用的浏览器driver:
def get_pics (driver ): image1 = driver.find_element_by_id( 'img1' ).get_attribute('src' ) image2 = driver.find_element_by_id( 'img2' ).get_attribute('src' ) req = urllib.request.Request(image1) fullpic = open('slide_fullpic.png' , 'wb+' ) fullpic.write(urllib.request.urlopen(req).read()) fullpic.close() req = urllib.request.Request(image2) blockpic = open('slide_block.png' , 'wb+' ) blockpic.write(urllib.request.urlopen(req).read()) blockpic.close() return 'slide_fullpic.png' , 'slide_block.png'
此时两个png已经保存到了程序目录下,然后使用opencv尝试匹配并获取xy轴距离:
def get_distance (fullpic, blockpic ): block = cv2.imread(blockpic, 0 ) template = cv2.imread(fullpic, 0 ) cv2.imwrite('template.jpg' , template) cv2.imwrite('block.jpg' , block) block = cv2.imread('block.jpg' ) block = cv2.cvtColor(block, cv2.COLOR_BGR2GRAY) block = abs(255 - block) w, h = block.shape[::-1 ] cv2.imwrite('block.jpg' , block) block = cv2.imread('block.jpg' ) template = cv2.imread('template.jpg' ) result = cv2.matchTemplate(block, template, cv2.TM_CCOEFF) x, y = np.unravel_index(result.argmax(), result.shape) draw = cv2.rectangle(template, (y, x), (y + w, x + h), (7 , 249 , 151 ), 2 ) cv2.imwrite("vertical_flip%d.jpg" %(random.randrange(1 ,10 )), draw) return y, template
(这里x,y与背景的xy恰好互异,实际上用y作为我们直观看到的x坐标)
匹配的结果并不很理想,我选择等待后续优化,先尝试实现正确地拖动滑块
测试发现,信息门户使用的图片大小原图为背景590x360和滑块93x361
然而图像在网页上的比例发生了变化,背景变化为280*155
所以我们需要把获得的x坐标再做一次处理得到*280/590得到对应的坐标位置,最后再补上滑块的一半宽度20来补偿初始位置:
distance, template = get_distance(fullpic, blockpic) real_distance = int((distance*28 /59 )+8 )
经过验证,这样的数据处理使得在网页定位的结果和opencv匹配的位置基本相同
2.模拟滑动操作
这里的操作我选择了***ActionChains***函数来模拟鼠标操作,首先用Xpath定位滑块 :
slider = driver.find_element_by_xpath('//*[@id="captcha"]/div/div[2]/div' )
然后调用函数***ActionChains***模拟鼠标按住 :
ActionChains(driver).click_and_hold(on_element=slider).perform()
设置移动路径 ,一开始使用匀速运动,后来发现有些网站存在匀速检测的功能,所以改成了先变速拖动,超出范围再往回拖动来模拟人手的行为:
def get_tracks (x_value ): v = 0 t = 0.3 tracks = [] constant = 0 temp = distance*4 /5 while constant <= x_value: if constant < temp: a = 2 else : a = -3 v0 = v s = v0*t+0.5 *a*(t**2 ) constant += s tracks.append(round(s)) v = v0+a*t return tracks
在for 循环中调用***ActionChains***实现拖动 (事实上并不连续):
tracks = get_tracks(real_distance) tracks.append(-(sum(tracks)-real_distance)) for track in tracks: ActionChains(driver).move_by_offset(xoffset=track, yoffset=0 ).perform()
此时在测试样例里已经可以正确的滑动滑块了,模拟滑动功能实现且不会被识别为机器
将代码组合后,已经可以完成基本的登录、识别、滑动的操作
代码存档:
from selenium import webdriverfrom selenium.webdriver.common.action_chains import ActionChainsimport timeimport urllibimport cv2import numpy as npimport urllib.requestimport mathimport randomdriver = webdriver.Firefox() url = 'https://idas.uestc.edu.cn/authserver/login?service=http%3A%2F%2Feportal.uestc.edu.cn%2Flogin%3Fservice%3Dhttp%3A%2F%2Feportal.uestc.edu.cn%2Fnew%2Findex.html' driver.get(url) def get_login (driver, url ): driver.get(url) elem = driver.find_element_by_xpath('//*[@id="username"]' ) elem.send_keys('此处填写登录账号' ) elem = driver.find_element_by_xpath('//*[@id="password"]' ) elem.send_keys('此处填写登录密码' ) elem = driver.find_element_by_xpath( '/html/body/div/div[2]/div[2]/div/div[6]/div/form/p[4]/button' ) elem.click() return driver driver = get_login(driver, url) def get_pics (driver ): image1 = driver.find_element_by_id( 'img1' ).get_attribute('src' ) image2 = driver.find_element_by_id( 'img2' ).get_attribute('src' ) req = urllib.request.Request(image1) fullpic = open('slide_fullpic.png' , 'wb+' ) fullpic.write(urllib.request.urlopen(req).read()) fullpic.close() req = urllib.request.Request(image2) blockpic = open('slide_block.png' , 'wb+' ) blockpic.write(urllib.request.urlopen(req).read()) blockpic.close() return 'slide_fullpic.png' , 'slide_block.png' def get_distance (fullpic, blockpic ): block = cv2.imread(blockpic, 0 ) template = cv2.imread(fullpic, 0 ) cv2.imwrite('template.jpg' , template) cv2.imwrite('block.jpg' , block) block = cv2.imread('block.jpg' ) block = cv2.cvtColor(block, cv2.COLOR_BGR2GRAY) block = abs(255 - block) w, h = block.shape[::-1 ] cv2.imwrite('block.jpg' , block) block = cv2.imread('block.jpg' ) template = cv2.imread('template.jpg' ) result = cv2.matchTemplate(block, template, cv2.TM_CCOEFF) x, y = np.unravel_index(result.argmax(), result.shape) draw = cv2.rectangle(template, (y, x), (y + w, x + h), (7 , 249 , 151 ), 2 ) cv2.imwrite("vertical_flip%d.jpg" % (random.randrange(1 , 10 )), draw) print('x坐标为:%d' % (y+20 )) if y+20 > 400 : elem = driver.find_element_by_xpath('//*[@id="captcha"]/i[1]' ) time.sleep(1 ) elem.click() fullpic, blockpic = get_pics(driver) y, template = get_distance(fullpic, blockpic) return y, template fullpic, blockpic = get_pics(driver) distance, template = get_distance(fullpic, blockpic) def get_tracks (x_value ): v = 0 t = 0.3 tracks = [] constant = 0 temp = distance*4 /5 while constant <= x_value: if constant < temp: a = 2 else : a = -3 v0 = v s = v0*t+0.5 *a*(t**2 ) constant += s tracks.append(round(s)) v = v0+a*t return tracks real_distance = int((distance*28 /59 )+8 ) tracks = get_tracks(real_distance) tracks.append(-(sum(tracks)-real_distance)) slider = driver.find_element_by_xpath( '/html/body/div/div[2]/div[2]/div/div[1]/div/div/div/div[2]/div/div/div[2]/div' ) ActionChains(driver).click_and_hold(on_element=slider).perform() for track in tracks: ActionChains(driver).move_by_offset(xoffset=track, yoffset=0 ).perform() time.sleep(0.5 ) ActionChains(driver).release(on_element=slider).perform() time.sleep(3 )
3.尝试优化匹配方法,提高匹配率
opencv自带的模板匹配在图片滑块区域颜色较深或明暗对比不强烈的情况下识别率较低,为优化识别率,提高识别效率,优化使用以下算法:
对图像二值化
对二值化的图像进行高斯模糊处理
用canny进行边缘检测
用霍夫变换寻找直线、线段
对符合条件的直线处理寻找交点,求出阴影块到原点的距离
考虑到滑块本身有明显的线段交点的固有属性,寻找直线交点或许可以做到较高的识别率,代码实现如下:
from selenium import webdriverfrom selenium.webdriver.common.action_chains import ActionChainsimport urllibimport cv2 as cvimport numpy as npimport urllib.requestimport mathdriver = webdriver.Firefox() url = 'https://idas.uestc.edu.cn/authserver/login?service=http%3A%2F%2Feportal.uestc.edu.cn%2Flogin%3Fservice%3Dhttp%3A%2F%2Feportal.uestc.edu.cn%2Fnew%2Findex.html' driver.get(url) def get_login (driver, url ): driver.get(url) driver.find_element_by_xpath('//*[@id="username"]' ).send_keys('2019091616016' ) driver.find_element_by_xpath('//*[@id="password"]' ).send_keys('heyuheng1.22.3' ) driver.find_element_by_xpath('//*[@id="casLoginForm"]/p[4]/button' ).click() return driver driver = get_login(driver, url) def get_pics (driver ): image1 = driver.find_element_by_id('img1' ).get_attribute('src' ) req = urllib.request.Request(image1) fullpic = open('slide_fullpic.png' , 'wb+' ) fullpic.write(urllib.request.urlopen(req).read()) fullpic.close() return 'slide_fullpic.png' fullpic = get_pics(driver) template = cv.imread(fullpic, 0 ) cv.imwrite('template.jpg' , template) template = cv.imread('template.jpg' ) def find_lines (image ): image = cv.cvtColor(image, cv.COLOR_BGR2GRAY) blurred = cv.GaussianBlur(image, (5 , 5 ), 0 ) canny = cv.Canny(blurred, 200 , 400 ) lines = cv.HoughLinesP(canny, 1 , np.pi / 180 , 20 ,minLineLength=15 , maxLineGap=8 ) result_lines = [] for x1, y1, x2, y2 in lines[:, 0 , :]: if (abs(y2 - y1) < 5 or abs(x2 - x1) < 5 ) and min(x1, x2) > 60 : result_lines.append([x1, y1, x2, y2]) return result_lines def dist_abs (point_exm, list_exm ): x, y = point_exm x1, y1, x2, y2 = list_exm dist_1 = math.sqrt(abs((y2 - y1) + (x2 - x1) + 1 )) dist_2 = math.sqrt(abs((y1 - y) + (x1 - x) + 1 )) + math.sqrt(abs((y2 - y) + (x2 - x) + 1 )) return abs(dist_2 - dist_1) def find_point (line1, line2 ): poit_status = False xp = line1 yp = line2 xp[0 ], yp[0 ], xp[1 ], yp[1 ] = line1 xp[2 ], yp[2 ], xp[3 ], yp[3 ] = line2 x = y = 0 if xp[0 ] == xp[1 ] or xp[2 ] == xp[3 ]: poit_status = False elif (yp[0 ]-yp[1 ])/(xp[0 ]-xp[1 ]) == (yp[2 ]-yp[3 ])/(xp[2 ]-xp[3 ]): poit_status = False else : x = ((xp[2 ] - xp[3 ])*(xp[1 ]*yp[0 ] - xp[0 ]*yp[1 ])-(xp[0 ]-xp[1 ])*(xp[3 ]*yp[2 ] - xp[2 ]*yp[3 ])) / ((xp[2 ] - xp[3 ])*(yp[0 ]-yp[1 ])-(xp[0 ]-xp[1 ])*(yp[2 ]-yp[3 ])) y = ((yp[2 ] - yp[3 ])*(yp[1 ]*xp[0 ] - yp[0 ]*xp[1 ])-(yp[0 ]-yp[1 ])*(yp[3 ]*xp[2 ] - yp[2 ]*xp[3 ])) / ((yp[2 ] - yp[3 ])*(xp[0 ]-xp[1 ])-(yp[0 ]-yp[1 ])*(xp[2 ]-xp[3 ])) poit_status = True return poit_status, [x, y] def find_distance (result_lines ): for x1, y1, x2, y2 in result_lines: for x3, y3, x4, y4 in result_lines: point_is_exist, [x, y] = find_point( [x1, y1, x2, y2], [x3, y3, x4, y4]) if point_is_exist: dist_len1 = dist_abs([x, y], [x1, y1, x2, y2]) dist_len2 = dist_abs([x, y], [x3, y3, x4, y4]) if dist_len1 < 5 and dist_len2 < 5 : if abs(y2 - y1) < 5 : if abs(x1 - x) + abs(y1 - y) < 5 : return x*28 /59 else : return (x - 65 )*28 /59 else : if abs(x3 - x) + abs(y3 - y) < 5 : return x*28 /59 else : return (x - 65 )*28 /59 return 0 def get_tracks (x_value ): v = 0 t = 0.3 tracks = [] constant = 0 temp = xoffset*4 /5 while constant <= x_value: a = -3 v0 = v s = v0*t+0.5 *a*(t**2 ) constant += s tracks.append(round(s)) v = v0+a*t return tracks lines = find_lines(template) xoffset = find_distance(lines) tracks = get_tracks(xoffset) tracks.append(-(sum(tracks)-xoffset)) slider = driver.find_element_by_xpath('//*[@id="captcha"]/div/div[2]/div' ) ActionChains(driver).click_and_hold(on_element=slider).perform() for track in tracks: ActionChains(driver).move_by_offset(xoffset=track, yoffset=0 ).perform() ActionChains(driver).release(on_element=slider).perform()
最终得到结果的识别率明显提高,能满足通过验证码的需求。
2020-10-13 20:25更新,压缩了代码量,代码更加简洁,从180+行压缩到113行,增强了可读性
还有一种更简洁的算法,来自亲爱的@Closer
from PIL import Imagefrom selenium import webdriverfrom selenium.webdriver import ActionChainsimport base64def get_distance (image0 ): length = 0 xy = (0 ,0 ) image = Image.open(image0) width = image.size[0 ] heigth = image.size[1 ] ''' 由下至上,由左至右的遍历图片的像素点,由拼图特征可得,底边总是长的且颜色为白色的直线,且这条直线以上便是阴影部分, 颜色较暗,所以可以通过这一特征来找到拼图的底边,便可得到的位置,然后返回偏移量。 ''' for h in range(heigth-1 ,2 ,-1 ): for w in range(width): img = image.getpixel((w,h)) img1 = image.getpixel((w,h-1 )) if (img[0 ]-img1[0 ]>40 and img[1 ]-img1[1 ]>40 and img[2 ]-img1[2 ]>40 ): if (length == 0 ): xy = (w,h) length += 1 elif (length<60 ): length = 0 if length > 59 : break return int(xy[0 ]*28 /59 ) def to_offsetlist (offset ): offsetlist = [] a = 0 v = 0 tmp = 0 while (tmp < offset): offsetlist.append(v) if tmp < offset//2 : a = 3 else : a = -2 v += a tmp += v offsetlist.append(offset-tmp+v) return offsetlist def get_pic (browser ): username = "xxx" password = "xxx" browser.get("https://idas.uestc.edu.cn/authserver/login" ) browser.find_element_by_xpath('//*[@id="username"]' ).send_keys(username) browser.find_element_by_xpath('//*[@id="password"]' ).send_keys(password) browser.find_element_by_xpath('//*[@id="casLoginForm"]/p[4]/button' ).click() tmp = browser.find_element_by_id('img1' ).get_attribute('src' ) img_base = tmp.split("," ) img = base64.b64decode(img_base[1 ]) fp = open('test.png' ,'wb' ) fp.write(img) fp.close() def slide (browser,offsetlist ): slider = browser.find_element_by_xpath('//*[@id="captcha"]/div/div[2]/div' ) action = ActionChains(browser) action.click_and_hold(slider).perform() for i in offsetlist: action.move_by_offset(i, 0 ).perform() action = ActionChains(browser) action.release(slider).perform() if __name__ == '__main__' : browser = webdriver.Firefox("E:/Mozilla Firefox" ) get_pic(browser) offset = get_distance("D:/CODE/test.png" ) offsetlist = to_offsetlist(offset) slide(browser,offsetlist)
去除注释和空行可以做到代码只有70行左右,更加简洁,唯一的缺点是对于缺口旋转的验证码需要判断直线位置,直线颜色也需要接近白色,但大多数情况下可以做到精准的识别。