利用python实现自动通过滑动验证码

1.自动填写用户名和密码

1.工具:selenium 库

在搜索引擎的帮助下,我发现selenium 库可以编写自动填写账号密码的爬虫。

阅读selenium 库的doc,很快就能理解使用selenium 进行自动化网页操作的基本需求:

image-20200929193000968.png

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") 
image-20200929194419338.png

似乎所有的图片都没有正常加载,尝试刷新后发现页面变得完全空白。起初我认为是chromedriver与chrome版本不匹配,但是再三确认后发现其他网站的访问都是正常的,于是想到信息门户可能启用了反爬虫。

由于我还没有使用selenium 在页面元素上做出其他任何操作,我在csdn上一番搜寻后看到了一种可能:

image-20200929195115271.png

推测是Chromedriver的特征码被信息门户识别然后拒绝了我的访问。

于是抱着试一试的心态,尝试换用firefox

from selenium import webdriver
driver = 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:

image-20200929200602676.png

//[@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.思路分析

  1. 检查网页元素,确定能够获取到的资源

    ​ 在一番查找后发现,信息门户的验证码图片的分两张存储,id分别为:img1,img2(我也没想到就是这么朴素的两个名字)为缺口图形和背景图形(已挖去缺口),

    示例如图:

  2. 依据可获取的资源思考解决方案

    ​ 与早期的三张图片的形式不同,现在的验证码只给出了两张图片,没有原图,这加大了匹配的难度。在一番查找后,发现了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)# *使用TM_CCOEFF方法计算匹配
x, y = np.unravel_index(result.argmax(), result.shape)# *获取得到的x,y结果
# *在背景图中绘制绿色框线来检查匹配效果
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
# 保存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 webdriver
from selenium.webdriver.common.action_chains import ActionChains
import time
import urllib
import cv2
import numpy as np
import urllib.request
import math
import random

driver = 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
# 保存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 webdriver
from selenium.webdriver.common.action_chains import ActionChains
import urllib
import cv2 as cv
import numpy as np
import urllib.request
import math
driver = 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行,增强了可读性

5.另一种思路

还有一种更简洁的算法,来自亲爱的@Closer

from PIL import Image
from selenium import webdriver
from selenium.webdriver import ActionChains
import base64

#找到拼图缺口,并返回横坐标,即滑块应滑动的距离
def 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))
#白色的RGB值都很大,直线以上偏暗部分RGB值较小,40为自行定义的一个差值,可根据实际情况调节
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):#测得这条直线长度为65,故在小于等于65这一范围内取一合理限制值即可
length = 0
if length > 59:
break
return int(xy[0]*28/59)#返回缩放后所需的真实值

#将getdistance所得到的距离转换成一个变速运动的偏移量列表,避免拖动滑块时被检测到,返回的列表的每个元素为这一段应滑动的偏移量
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"
#以下5行分别为:进入目标地址,输入账号密码,点击登录,获取拼图图片
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(",")#去掉头部,分离出要用的base64编码
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行左右,更加简洁,唯一的缺点是对于缺口旋转的验证码需要判断直线位置,直线颜色也需要接近白色,但大多数情况下可以做到精准的识别。