1.环境
python3.8
pycharm2021.2
需要导入包
import time
from io import BytesIO
from scipy import signal
from PIL import Image
from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.support.wait import WebDriverWait
from selenium.webdriver import ActionChains
import cv2
2.完整代码
2.1说明
该部分代码,除测试代码外,全部都写在类DoubanLogin下
class DoubanLogin():
2.2初始化参数
2.2.1 初始化全局常量
这三个常量分别是登录的手机号码和密码,以及登录网址
PHONENUMS = '12345678912'
PASSWORD = 'qw123123'
url = 'https://accounts.douban.com/passport/login'
2.2.2 初始化类参数
def __init__(self):
self.browser = webdriver.Chrome()
self.url = url
self.phone = PHONENUMS
self.password = PASSWORD
self.wait = WebDriverWait(self.browser, 20)
2.3 进入登录页面
进入登录页面,并且需要转到用账号密码登录的子页面,然后输入账号和密码的信息。
def open(self):
# 访问豆瓣的登录页面
self.browser.get(self.url)
# 获取进入账号密码登录子页面的按钮
button_password_login = self.wait.until(EC.element_to_be_clickable((By.CLASS_NAME, 'account-tab-account')))
# 点击事件进入账号密码登录子页面
button_password_login.click()
# 获取输入账号密码的输入框,并输入值
input_phonenums = self.wait.until(EC.presence_of_element_located((By.CLASS_NAME, 'account-form-input')))
input_password = self.wait.until(EC.presence_of_element_located((By.NAME, 'password')))
input_phonenums.send_keys(PHONENUMS)
input_password.send_keys(PASSWORD)
2.4 进入拼图验证码界面
2.4.1 点击登录按钮
获取登录按钮
def get_login_button(self):
button_login = self.wait.until(EC.element_to_be_clickable((By.CLASS_NAME,"btn-account")))
return button_login
并点击(点击事件在测试代码中),然后进入拼图验证码界面
2.4.2 获取拼图界面截图
2.4.2.1 创建屏幕截图对象
def get_screenshot(self):
# 获取截图对象
screenshot = self.browser.get_screenshot_as_png()
screenshot = Image.open(BytesIO(screenshot))
return screenshot
2.4.2.2 获取拼图的位置信息
由于豆瓣中拼图是在子界面(节点iframe)下,所以需要先用location函数和size函数,获取iframe子界面的位置(location)和大小(size)。然后需要用switch_to.frame()函数,切换到子界面下,然后获取拼图的相对于iframe子界面的位置(location)和大小(size),然后才能获取到拼图相对于父界面的位置信息。
这里有一个有疑问的地方,按理说,拼图的位置信息应该是子界面位置信息加上拼图相对于子界面的位置信息,但是实际操作并不是这样的,实际操作需要乘以2,才能获取到拼图相对于父界面的位置信息(也就是拼图的左上角的点的位置),然后计算拼图右下角的点,也需要加上两倍的拼图大小(size)。
具体什么原因,我目前也不知道。
def get_position(self,image_name):
print(2)
# 获取子frame节点
frame = self.wait.until(EC.presence_of_element_located((By.ID,'tcaptcha_iframe')))
# 获取子节点的位置和大小
location_frame = frame.location
size_frame = frame.size
x_i = location_frame['x']
y_i = location_frame['y']
# 切换到iframe
iframe_name = 'tcaptcha_iframe'
self.goto_iframe(iframe_name=iframe_name)
# 获取iframe中img的位置和大小
img = self.wait.until(EC.presence_of_element_located((By.CLASS_NAME,image_name)))
location = img.location
size = img.size
x = location['x']
y = location['y']
w = size['width']
h = size['height']
top=2*(y+y_i)
bottom = 2*(y+y_i+h)
left = 2*(x+x_i)
right = 2*(x+x_i+w)
return (top, bottom, left, right)
其中
def goto_iframe(self,iframe_name):
self.browser.switch_to.frame(iframe_name)
2.4.2.3 截取拼图
获取截图位置信息,调用截图对象,然后进行截图并保存,返回一个截图对象。
def get_geetest_image(self, name='captcha1.png', image_name='tc-bg-img'):
"""
获取验证码图片
:return: 图片对象
"""
top, bottom, left, right = self.get_position(image_name=image_name)
print('验证码位置', top, bottom, left, right)
screenshot = self.get_screenshot()
captcha = screenshot.crop((left, top, right, bottom))
captcha.save(name)
return captcha
2.4.2.3 获取待拼图块的截图
其实和上面步骤一样,因此直接使用上述函数get_geetest_image获取,但是需要给name赋不同值,image_name也不同,其中name表示待获取截图的名字,image_name表示的是在html代码中,截图对象的节点class_name属性值。
name用于
captcha.save(name)
image_name用于
```python
# 获取待截取图片的上下左右边界
top, bottom, left, right = self.get_position(image_name=image_name)
```
和
# 获取图片对象
self.wait.until(EC.presence_of_element_located((By.CLASS_NAME,image_name)))
然后用下述代码,获取拼图和待拼图块的截图
# 因为原函数有默认值,所以有一个默认值没传,这里获取的是拼图截图
image1 = crack.get_geetest_image('captcha1.png')
# 因为在get_geetest_image函数中有 self.goto_iframe(iframe_name=iframe_name)函数,所以需要切换到父界面,否则获取第二个截图时,两次使用切换到子# 界面函数,要么找到不想要的东西,要么找不到想要的东西。
crack.back_to_parent_frame()
# 这里获取了待拼图块的截图
image2 = crack.get_geetest_image(name='captcha2.png',image_name='tc-jpp-img')
2.5 拼图
2.5.1 拼图缺口位置
这里其实根据网页不同,方案也多种多样,
- 有些网页可以获取原图和有缺口的拼图,只需要比较像素,找到像素不同的点,就是待拼图块应该移动到的位置。
- 还有些网页没有原图,只有拼图和待拼图块。豆瓣就是这种
上述的第二点的解决方法很多,涉及到图像处理的相关知识。我这里用了一种简单粗暴的方式(先都做灰度处理,再都做锐化处理,然后做卷积核,最后遍历查找卷积核矩阵中,值最大的),准确率达不到百分百,视截取的拼图中处理后的线条多少决定,线条越少准确率越高。由于截图的图像大小和图像本身的大小也不同,所以还需要做图像缩放的处理。
2.5.1.1 图像缩放处理
def resizeImage(self, image, width=None, height=None, inter=cv2.INTER_AREA):
newsize = (width, height)
# 获取图像尺寸
(h, w) = image.shape[:2]
# 缩放图像
newimage = cv2.resize(image, newsize, interpolation=inter)
cv2.imwrite('newimage.png', newimage)
return newimage
2.5.1.2 灰度处理
def img2gray(self,image):
img_rgb = cv2.imread(image) # 读入图片
img_gray = cv2.cvtColor(img_rgb, cv2.COLOR_BGR2GRAY) # 转灰度图片
cv2.imwrite(image, img_gray) # 保存图片,第一个参数:path, 第二个参数:保存的图片
return img_gray
2.5.1.3 锐化处理
def canny_edge(self,image):
# 这里做了灰度处理,并用image原来的名字保存,也就是说image被灰度化后的图像覆盖了
self.img2gray(image)
# 然后又读取了灰度处理后的图像
img = cv2.imread(image, 0)
blur = cv2.GaussianBlur(img, (3, 3), 0) # 用高斯滤波处理原图像降噪
canny = cv2.Canny(blur, threshold1=200, threshold2=200) # 锐化图片
cv2.imwrite(image, canny) # 保存图片
cv2.waitKey()
cv2.destroyAllWindows() # 关闭窗口
return canny
2.5.1.4 计算卷积核
用灰度处理后再锐化处理后的图像,做卷积核计算
def convole2d(self, bg_array, fillter):
bg_h, bg_w = bg_array.shape[:2]
fillter_h, fillter_w = fillter.shape[:2]
c_full = signal.convolve(bg_array, fillter, mode='full')
kr, kc = fillter_h // 2, fillter_w // 2
c_same = c_full[
fillter_h - kr - 1: bg_h + fillter_h - kr - 1,
fillter_w - kc - 1: bg_w + fillter_w - kr - 1,
]
return c_same
2.5.1.5 寻找拼图缺口位置
从卷积核的二维矩阵中,行从上往下遍历,列从后往前遍历,这是因为,豆瓣的拼图截图,其实会有两个看起来是缺口的位置,一个是待拼图块,一个是真正的缺口位置,如果列从前往后遍历,就会先找到待拼图块的位置,并且由于是用待拼图块和拼图做的卷积核,所以待拼图块和待拼图块的卷积核比待拼图块和缺口位置的卷积核更大,也就是说,就算遍历完整个矩阵,也找不到缺口位置。所以要从后往前遍历,并且由于豆瓣拼图验证码的特点,其缺口位置总是在截图的右半区域。所以列的遍历只用到cols/2。不然还是会找到待拼图块占据的拼图位置。
def find_max_point(self, arrays):
max_point = 0
max_point_pos = None
array_rows = arrays.shape[0]
arrays_cols = arrays.shape[1]
for row in range(array_rows):
for col in reversed(range(int(arrays_cols / 2), arrays_cols)):
if arrays[row, col] > max_point:
max_point = arrays[row, col]
max_point_pos = col, row
return max_point_pos
2.5.2 移动滑块到缺口位置
2.5.2.1 获取滑块按钮
def get_silider(self):
slider = self.wait.until(EC.element_to_be_clickable((By.CLASS_NAME, 'tc-drag-thumb')))
return slider
2.5.2.2 调用函数,获取移动距离
调用2.5.1的函数获取移动距离,但是测试发现,移动距离还是会有偏离,所以需要添加常量修正。
def get_move_dist(self,image1='captcha1.png',image2='captcha2.png'):
captcha1 = cv2.imread(image1)
captcha2 = cv2.imread(image2)
pic1 = self.resizeImage(captcha1, 341, 195, cv2.INTER_AREA)
pic2 = self.resizeImage(captcha2, 68, 68, cv2.INTER_AREA)
cv2.imwrite('newpic1.png', pic1)
cv2.imwrite('newpic2.png', pic2)
canny_1 = self.canny_edge('newpic1.png')
print(canny_1)
print("######################")
canny_2 = self.canny_edge('newpic2.png')
print(canny_2)
convoled_after = self.convole2d(canny_1, canny_2)
print("######################")
print(convoled_after)
distance = self.find_max_point(convoled_after)
print(distance[0]-55)
return distance[0]-55
2.5.1.7 模拟移动过程
先获取移动轨迹
# 注意这部分要控制移动轨迹的长度(移动次数),长度太长,会导致验证的等待时间过长,会验证失败,
#其次这部分最好用random随机生成计算间隔和加速度,这样每次得到的移动轨迹长度不定,然后验证的
#时候验证时间也不同,更好的模拟了人的行为。否则每次验证的时间都是一样长的,会被认为机器人程序。
def get_track(self, distance):
"""
根据偏移量获取移动轨迹
:param distance: 偏移量
:return: 移动轨迹
"""
# 移动轨迹
track = []
# 当前位移
current = 0
# 减速阈值
mid = distance * 4 / 5
# 计算间隔
t = 2.1
# 初速度
v = 0
while current < distance:
if current < mid:
# 加速度为正5
a = 5
else:
# 加速度为负3
a = 3
# 初速度v0
v0 = v
# 当前速度v = v0 + at
v = v0 + a * t
# 移动距离x = v0t + 1/2 * a * t^2
move = v0 * t + 1 / 2 * a * t * t
# 当前位移
current += move
# 加入轨迹
track.append(round(move))
# 如果最后一次位移超过了偏移量,则需要修正。否则匹配不上
if current>distance:
track.append(distance-current)
return track
再根据移动轨迹,来移动滑块。
def move_to_gap(self, slider, track):
"""
拖动滑块到缺口处
:param slider: 滑块
:param track: 轨迹
:return:
"""
ActionChains(self.browser).click_and_hold(slider).perform()
for x in track:
ActionChains(self.browser).move_by_offset(xoffset=x, yoffset=0).perform()
time.sleep(0.5)
ActionChains(self.browser).release().perform()
3.其他
上述过程中,有些问题没有解决,比如为什么location和size要乘以2,以及是什么原因导致的要用常数项来修正。如果有知道的朋友,可以在评论区留言。解决大家的疑惑。