本文正在参加「Python主题月」,详情查看 活动链接
前景回顾
我在 掘金登录引发的思考 一文中分析了掘金登录的一些知识,虽说通过程序组装登录参数后直接调用登录接口是有可行性,但是性价比太低。再者如果登录策略修改,可能到头来是 竹篮打水一场空,白忙活。
所以今天直接换个角度思考问题,借助 Selenium (一个用于Web应用程序测试的工具)完成登录。
技术背景
关于本文用到技术的一些简介,只需要简单了解即可。当然这些技术不了解也没有关系,不妨碍我们往下看。
- 前端知识 cookie 简介
- 前端知识 XPath 教程
- 图像处理 OpenCV中文文档
- Web应用程序测试 Python Selenium 使用指南
破解登录
我们直接看如何借助 Selenium 完成登录,大致分为以下步骤:
- 通过模拟浏览器的操作获取滑动验证码
- 破解滑动验证码
- a. 计算出滑块需要滑动的距离
- b. 模拟浏览器拖拽完成滑动验证码识别
- 模拟浏览器实现登录
1. 环境介绍
- 硬件 Mac
- 语言 Python3
- 编辑器 PyCharm
- 浏览器 Chrome
2. 获取滑动验证码
通过分析登录过程可以确定滑块验证码由两张图片构成。一张为滑块图片,另一张为背景图。这里只需要通过 Selenium 模拟浏览器的操作完成登录前的页面跳转最终获取验证码图片 URL,此步骤没有太多的难点,代码如下:
from selenium.webdriver import ActionChains
from selenium.webdriver.common.by import By
driver = webdriver.Chrome(executable_path="./chromedriver")
driver.get("https://juejin.cn/")
# 代码中的注释为从首页开始到获取URL的每一步骤,可以自己打开浏览器尝试
# 点击登录按钮
login_button = driver.find_element(By.XPATH, '''//button[text()="登录"]''')
ActionChains(driver).move_to_element(login_button).click().perform()
time.sleep(2)
# 选择账号密码登录
other_login_span = driver.find_element(By.XPATH, '''//span[text()="
其他登录方式
"]''')
ActionChains(driver).move_to_element(other_login_span).click().perform()
time.sleep(2)
# 填充用户名密码
username_input = driver.find_element(By.XPATH, '//input[@name="loginPhoneOrEmail"]')
password_input = driver.find_element(By.XPATH, '//input[@name="loginPassword"]')
username_input.send_keys("") # 掘金的账号
password_input.send_keys("") # 掘金的密码
# 点击登录
login_button = driver.find_element(By.XPATH, '''//button[text()="
登录
"]''')
ActionChains(driver).move_to_element(login_button).click().perform()
time.sleep(2)
# 获取滑块验证码图片
verify_image1 = driver.find_element(By.XPATH, '''//img[@id="captcha-verify-image"]/../img[1]''')
verify_image2 = driver.find_element(By.XPATH, '''//img[@id="captcha-verify-image"]/../img[2]''')
verify_image1_src = verify_image1.get_attribute("src")
verify_image2_src = verify_image1.get_attribute("src")
复制代码
3. 破解滑块验证码
下图为对应滑块、背景图以及对应的识别过程
简单的来说:识别过程就是将滑块对应的图片拖到背景图凹陷的地方。
3.1 定位滑块坐标
这里使用 Python Opencv 的模板匹配。模板匹配就是在整个图像区域发现与给定子图像匹配的小块区域。过程大致为在待检测图像上,从左到右,从上向下计算模板图像与重叠子图像的匹配度,匹配程度越大,两者相同的可能性越大。
cv2.matchTemplate
前两个参数分别是待模板图片、背景图片,第三个参数是匹配算法,这里使用是 TM_CCOEFF_NORMED (标准相关性系数匹配)。该函数返回结果是由每个位置的比较结果组合所构成的一个结果集(多维数组)。
例如输入图像(原始图像)尺寸是W H,模板的尺寸是w h,则返回值的大小为(W-w+1)* (H-h+1)。
# 这里需要注意 slider_pic background_pic 是灰度化 二值化后的图片
# 读取滑块
slider_pic = cv2.imread("slider_pic")
# 读取背景图
background_pic = cv2.imread("background_pic")
# 比较两张图的重叠区域
result = cv2.matchTemplate(slider_pic, background_pic, cv2.TM_CCOEFF_NORMED)
# 获取图片的缺口位置 top, left 为滑块相对于图片左上角的位置
top, left = np.unravel_index(result.argmax(), result.shape)
复制代码
备注:np.unravel_index(result.argmax(), result.shape)
获取多维数组最大值对应坐标的值(对应为被找到图的像素坐标)。因为滑块移动只考虑水平移动,所以这里直接省略 top
即可。
3.2 生成滑动轨迹
众所周知人在拖拽图片时很难做到匀速操作,所以这里需要使用程序模拟人的拖拽。使得图片的运动轨迹更加无序。这里思路以及代码源于 极验滑动验证码识别
因为不能一次性将滑块拖到指定位置,这里需要模拟正常的拖动。所以拖动的逻辑为前段滑块做匀加速运动,后段滑块做匀减速运动,利用物理学的加速度公式即可完成验证。
滑块滑动的加速度用 a 来表示,当前速度用 v 表示,初速度用 v0 表示,位移用 x 表示,所需时间用 t 表示,它们之间满足如下关系:
x = v0 * t + 0.5 * a * t * t
v = v0 + a * t
复制代码
# 其中 track 为滑块拖动的轨迹
def get_track(distance): # distance为传入的总距离
# 移动轨迹
track = []
# 当前位移
current = 0
# 减速阈值
mid = distance * 4 / 5
# 计算间隔
t = 0.2
# 初速度
v = 1
while current < distance:
if current < mid:
# 加速度
a = 4
else:
# 加速度
a = -3
v0 = v
# 当前速度
v = v0 + a * t
# 移动距离
move = v0 * t + 1 / 2 * a * t * t
# 当前位移
current += move
# 加入轨迹
track.append(round(move))
return track
复制代码
4. 页面滑块操作
上一步借助 opencv 完成滑块验证码定位以及轨迹的生成,下面就是在页面上完成对滑块的拖拽。
# 定位到移动按钮
verify_div = self.driver.find_element(By.XPATH, '''//div[@class="sc-kkGfuU bujTgx"]''')
# 按下鼠标左键
ActionChains(self.driver).click_and_hold(verify_div).perform()
time.sleep(0.5)
# 遍历轨迹进行滑动
for t in track:
time.sleep(0.01)
ActionChains(self.driver).move_by_offset(xoffset=t, yoffset=0).perform()
# 释放鼠标 完成拖拽
ActionChains(self.driver).release(on_element=verify_div).perform()
复制代码
注意事项: 代码中的 time.sleep()
是为了使页面操作有短暂的停顿,而不是连贯的快速操作。
5. 获取 cookie
在完成滑块验证码的识别后,页面正常登录跳转成功,此时只需要获取到 juejin.cn
对应的 cookie即可。
# 获取当前页面 cookie
driver.get_cookies()
'''
[{'domain': '.juejin.cn',
'expiry': 1632902943,
'httpOnly': False,
'name': 'MONITOR_WEB_ID',
'path': '/',
'secure': False,
'value': 'e7fa2492-...-8ff5e04c6727'},
# ...
{'domain': '.juejin.cn',
'expiry': 1630310943,
'httpOnly': True,
'name': 'sessionid_ss',
'path': '/',
'sameSite': 'None',
'secure': True,
'value': 'bfac25b956...f7be812054f'},
{'domain': '.juejin.cn',
'expiry': 1630310943,
'httpOnly': True,
'name': 'sessionid',
'path': '/',
'secure': False,
'value': 'bfac25b956b...42f7be812054f'},
]
'''
复制代码
后记
需要优化的点
- 图片多次本地存储、读取耗时,优化为直接在内存中处理图片。
- 代码在一个文件中杂乱无章,优化为按功能划分不同的对象。
- 识别有错误的几率,这里需要增加错误重试机制(如下图)。