转载请注明链接
环境:ubuntu14.04 + firefox60.0.2 + python3.4
之前使用xdotools模拟鼠标键盘实现自动登录打卡考勤,但是后来公司加了验证码,所以此方案不再适用,改由python实现,这个在博文中已有。
最近公司又改成了jigsaw滑块验证,之前的验证码方案已经不再适用。
一、方案
对于目前网上搜索到的滑块验证现状,一般是以下两种方案:
- 最初版本的滑块验证在网页源码中能够下载到完整的拼图图片,带缺口的拼图背景及缺口图片,用opencv对比完整图与缺口图,计算出缺口位置。
- 次新版本的滑块验证在网页源码中只能够下载到带缺口的拼图背景及缺口图片,仍然可以用opencv matchTemplate计算缺口的最佳匹配位置,大约有90%的成功率。重试三次基本是100%成功。
在公司最近配置的滑块验证模块,上述两个方案都已经失效,网页源码中虽然有带缺口的拼图背景及缺口图片的src链接,但是无法下载,返回404。这应该是模块为应对网上现有的爬虫方案进行了升级。
改进版方案:
因为无法通过src下载,只能通过截图方式下载,有两点:
- 进入滑块验证页面后,执行滑块点击,显示拼图,通过元素的screenshot方法截取拼图背景图及缺口图:
ActionChains(self.driver).click_and_hold(slider).perform()
target = self.wait.until(EC.presence_of_element_located((By.XPATH, "//div[@class='jigimgB']/img")))
template = self.wait.until(EC.presence_of_element_located((By.XPATH, "//div[@class='jigimgS']/img")))
if target and template:
print("开始下载图片")
bigimage = self.driver.find_element_by_id("bigImage")
bigimage.screenshot(self.target_path)
smallimage = self.driver.find_element_by_id("smallImage")
smallimage.screenshot(self.template_path)
- 截取的拼图背景图上最左边的有缺口图,这样matchTemplate计算位置肯定是0,因此需要截取掉,计算完毕后加上截取掉的宽度。
target = cv2.imread(self.target_path)
sp = target.shape
cropImg = target[ 0:sp[0], 60:sp[1]] # 裁剪图像
cv2.imwrite(self.target_path, cropImg) # 写入图像路径
croptarget = cv2.imread(self.target_path)
template = cv2.imread(self.template_path)
distance = self._match_templet(croptarget, template)
二.关键点:
除了方案中提到的截图,还有以下几点注意:
- 计算滑动轨迹的track时,网上搜索到的一般显示forward,然后backward,如果直接forward需要改一下track最后一段距离:
current += s
if current > distance:
forward_tracks.append(round(s)-round(current-distance))
else:
forward_tracks.append(round(s))
- 计算完距离后,这个距离除了要加上方案中提到的截图宽度,还要加上滑块宽度的一半。仔细观察,会发现鼠标在滑块中央滑动时会先空滑滑块一半的距离,然后缺口图片才会跟随滑动:
distance = self._match_templet(croptarget, template)
# 默认是从滑块中央开始滑动,这里有个滑动阈值,为滑块宽度的一半,60为截图宽度,20为滑块一半的宽度
distance += 60 + 20
三.源码:
- main.py:
from slider.slider import kaoqin_Slider
if __name__ == '__main__':
jd = JD_Slider(url='http://kaoqin.test.com/index.jsp', username='username', pwd='#password')
jd.main()
- slider/slider.py:
#!/usr/bin/env python
# -*- coding:utf-8 -*-
import json
import random
import re
import time
import cv2
import numpy as np
from PIL import Image
from selenium import webdriver
from selenium.webdriver import ActionChains
from selenium.webdriver.common.by import By
from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.support.wait import WebDriverWait
from urllib import request,error
class kaoqin_Slider(object):
def __init__(self, url, username, pwd=''):
super(JD_Slider, self).__init__()
# 实际地址
self.url = url
self.driver = webdriver.Firefox()
self.wait = WebDriverWait(self.driver, 10)
# 账户信息
self.username = username
self.password = pwd
# 下载图片的临时路径
self.target_path = "target.png"
self.template_path = "template.png"
# 网页图片缩放
self.zoom = 1
self.testcount = 0
def get_random_float(min, max, digits=4):
return round(random.uniform(min, max), digits)
def get_random_int(min, max):
return round(random.randint(min, max))
def open(self, url=None):
self.driver.get(url if url else self.url)
def close(self):
self.driver.close()
def refresh(self):
self.driver.refresh()
def main(self, is_open=True):
print("开始准备")
try:
if is_open:
self.open()
time.sleep(1)
self._login()
for i in range(1, 4):
if self._crack_slider():
btn_reg = self.driver.find_element_by_id('loginButton')
btn_reg.click()
time.sleep(3)
btn_signin = self.driver.find_element_by_xpath("//a[@class='mr36']")
btn_signin.click()
break
except:
print("程序出现异常")
finally:
self.driver.close()
print("结束程序")
def is_login(self):
try:
self.wait.until(EC.presence_of_element_located((By.ID, "loginname")))
print("登录失败")
return False
except:
print("登录成功")
return True
def _login(self):
"""
登录
:return:
"""
print("填写账号")
username = self.driver.find_element_by_xpath("//div[@class='logonPanel']/div[2]/div[2]/input[1]")
username.clear()
# username
time.sleep(random.uniform(0.1, 0.5))
username.send_keys(self.username[0:3])
time.sleep(random.uniform(0.5, 0.8))
username.send_keys(self.username[3:])
# pwd
time.sleep(random.uniform(0.8, 1.2))
pwd = self.driver.find_element_by_xpath("//div[@class='logonPanel']/div[2]/div[3]/input[1]")
pwd.clear()
pwd.send_keys(self.password)
# 滑块
def _crack_slider(self):
print("开始移动滑块")
try:
slider = self.wait.until(EC.element_to_be_clickable((By.CLASS_NAME, 'ui-slider-btn')))
ActionChains(self.driver).click_and_hold(slider).perform()
target = self.wait.until(EC.presence_of_element_located((By.XPATH, "//div[@class='jigimgB']/img")))
template = self.wait.until(EC.presence_of_element_located((By.XPATH, "//div[@class='jigimgS']/img")))
if target and template:
print("开始下载图片")
bigimage = self.driver.find_element_by_id("bigImage")
bigimage.screenshot(self.target_path)
smallimage = self.driver.find_element_by_id("smallImage")
smallimage.screenshot(self.template_path)
time.sleep(1)
# zoom
# 281 is width of target image
local_img = Image.open(self.target_path)
size_loc = local_img.size
self.zoom = 281 / int(size_loc[0])
print("计算缩放比例 zoom = %f" % round(self.zoom, 4))
pic_success = True
else:
print("未找到缺口图片")
return False
except error.HTTPError as e:
print(e.reason, e.code, e.headers, sep='\n')
print("获取缺口图片异常")
return False
slider_success = False
if pic_success:
# 模板匹配
target = cv2.imread(self.target_path)
sp = target.shape
cropImg = target[ 0:sp[0], 60:sp[1]] # 裁剪图像
cv2.imwrite(self.target_path, cropImg) # 写入图像路径
croptarget = cv2.imread(self.target_path)
template = cv2.imread(self.template_path)
distance = self._match_templet(croptarget, template)
# 默认是从滑块中央开始滑动,这里有个滑动阈值,为滑块宽度的一半
distance += 60 + 20
# print("distance %d" % distance)
# distance = self._getsliderrealdistance(280, 60, distance)
print("位移距离 distance = %d" % distance)
# yoffset_random = random.uniform(-2, 4)
# ActionChains(self.driver).move_by_offset(xoffset=distance, yoffset=yoffset_random).perform()
# 轨迹
tracks = self._get_tracks3(distance * self.zoom)
# 正向滑动
for track in tracks:
yoffset_random = random.uniform(-2, 4)
ActionChains(self.driver).move_by_offset(xoffset=track, yoffset=yoffset_random).perform()
time.sleep(random.uniform(0.1, 0.2))
ActionChains(self.driver).release().perform()
print("滑块移动成功")
time.sleep(3)
slidertext = self.wait.until(EC.presence_of_element_located((By.CLASS_NAME, 'ui-slider-text')))
print(slidertext.text)
if '验证成功' in slidertext.text:
slider_success = True
else:
slider_success = False
print(slider_success)
return slider_success
def _match_templet(self, img_target, img_template):
result = cv2.matchTemplate(img_target, img_template, cv2.TM_CCOEFF_NORMED)
# 查找数组中匹配的最大值
min_val, max_val, min_loc, max_loc = cv2.minMaxLoc(result)
left_up = max_loc
print('匹配结果区域起点x坐标为:%d' % max_loc[0])
return left_up[0]
def _get_tracks3(self, distance):
# distance += 20
v = 0
t = 0.2
forward_tracks = []
current = 0
mid = distance * 4 / 5 # 减速阀值
while current < distance:
if current < mid:
a = 20 # 加速度为+2
else:
a = -v/2
s = v * t + 0.5 * a * (t ** 2)
v = v + a * t
current += s
if current > distance:
forward_tracks.append(round(s)-round(current-distance))
else:
forward_tracks.append(round(s))
print(forward_tracks)
return forward_tracks