使用python进行自动化操作或者爬虫过程中,可能会遇到需要进行验证的情况。本文介绍了两种通过滑块验证的方法:
测试地址:https://accounts.douban.com/passport/login
以豆瓣网为例,首先需要获取滑块验证码中的滑块背景图。通过开发者工具分析页面元素,可以看到滑块背景图被放在一个div块中,图片的url被放在div的style属性中。我们可以通过正则表达式将style属性中的图片url提取出来。然后通过request模块将图片保存到本地。
# 滑块背景
slide_bg_str = driver.find_element(By.ID, 'slideBg').get_attribute("style")
# print(slide_bg_str)
pattern = 'background-image: url\\(\".*?\"\\);'
# 正则表达式提取图片url
slide_bg_url = str(re.findall(pattern, slide_bg_str, re.S)[0]).split('"')[1]
# print(slide_bg_url)
# 通过图片url将图片保存到本地
request.urlretrieve(url=slide_bg_url, filename='./MarkPicture/slide_bg.png')
这里使用的是轮廓检测算法,具体参考:https://blog.csdn.net/flyfish1986/article/details/147332518
以下是封装的方法:
# 封装的计算图片距离的算法
# imageSrc图片路径,传入滑块背景图的路径,返回缺口的x坐标
def get_pos(imageSrc):
# 读取图像文件并返回一个image数组表示的图像对象
image = cv2.imread(imageSrc)
# GaussianBlur方法进行图像模糊化/降噪操作。
# 它基于高斯函数(也称为正态分布)创建一个卷积核(或称为滤波器),该卷积核应用于图像上的每个像素点。
blurred = cv2.GaussianBlur(image, (5, 5), 0, 0)
# Canny方法进行图像边缘检测
# image: 输入的单通道灰度图像。
# threshold1: 第一个阈值,用于边缘链接。一般设置为较小的值。
# threshold2: 第二个阈值,用于边缘链接和强边缘的筛选。一般设置为较大的值
canny = cv2.Canny(blurred, 0, 100) # 轮廓
# findContours方法用于检测图像中的轮廓,并返回一个包含所有检测到轮廓的列表。
# contours(可选): 输出的轮廓列表。每个轮廓都表示为一个点集。
# hierarchy(可选): 输出的轮廓层次结构信息。它描述了轮廓之间的关系,例如父子关系等。
contours, hierarchy = cv2.findContours(canny, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE)
# 遍历检测到的所有轮廓的列表
for contour in contours:
# contourArea方法用于计算轮廓的面积
area = cv2.contourArea(contour)
# arcLength方法用于计算轮廓的周长或弧长
length = cv2.arcLength(contour, True)
# 如果检测区域面积在5025-7225之间,周长在300-380之间,则是目标区域 可以根据实际情况进行调整
if 5025 < area < 7225 and 300 < length < 380:
# 计算轮廓的边界矩形,得到坐标和宽高
# x, y: 边界矩形左上角点的坐标。
# w, h: 边界矩形的宽度和高度。
x, y, w, h = cv2.boundingRect(contour)
print("计算出目标区域的坐标及宽高:", x, y, w, h)
# 在目标区域上画一个红框看看效果
cv2.rectangle(image, (x, y), (x + w, y + h), (0, 0, 255), 2)
cv2.imwrite("./MarkPicture/mark_picture.png", image)
return x
return 0
找到缺口位置后,会标记出来,如上图所示。
这里需要注意的是,因为获取的是源图片,展示在页面中的图片的比例有所变化,所以需要按比例缩放得到的距离:新缺口坐标 = 原缺口坐标 * 新画布宽度 / 原画布宽度
newDis = int(dis * 340 / 672)
打开开发者工具,找到滑块按钮所在的div块,右键鼠标,复制元素的XPath。使用selenium通过XPath找到该元素:
# 滑块按钮
swipe = driver.find_element(By.XPATH, '//*[@id="tcOperation"]/div[6]')
# 滑块按钮的x坐标
swipe_x = swipe.location['x']
# 需要下方滑块需要移动的距离
need_move_dis = newDis - swipe_x
ActionChains(driver).click_and_hold(swipe).perform()
step = 0
cur_step = 0
while step < need_move_dis:
cur_step = random.randint(3, 10)
step += cur_step
ActionChains(driver).move_by_offset(xoffset=cur_step, yoffset=0).perform()
ActionChains(driver).release().perform()
import re
import random
from selenium import webdriver # 导入selenium的webdriver模块
from selenium.common import TimeoutException
from selenium.webdriver.common.by import By # 引入By类选择器
import time
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
import urllib.request as request
from selenium.webdriver.common.action_chains import ActionChains # 动作类
import cv2
driver = webdriver.Edge()
# 封装的计算图片距离的算法
def get_pos(imageSrc):
# 读取图像文件并返回一个image数组表示的图像对象
image = cv2.imread(imageSrc)
# GaussianBlur方法进行图像模糊化/降噪操作。
# 它基于高斯函数(也称为正态分布)创建一个卷积核(或称为滤波器),该卷积核应用于图像上的每个像素点。
blurred = cv2.GaussianBlur(image, (5, 5), 0, 0)
# Canny方法进行图像边缘检测
# image: 输入的单通道灰度图像。
# threshold1: 第一个阈值,用于边缘链接。一般设置为较小的值。
# threshold2: 第二个阈值,用于边缘链接和强边缘的筛选。一般设置为较大的值
canny = cv2.Canny(blurred, 0, 100) # 轮廓
# findContours方法用于检测图像中的轮廓,并返回一个包含所有检测到轮廓的列表。
# contours(可选): 输出的轮廓列表。每个轮廓都表示为一个点集。
# hierarchy(可选): 输出的轮廓层次结构信息。它描述了轮廓之间的关系,例如父子关系等。
contours, hierarchy = cv2.findContours(canny, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE)
# 遍历检测到的所有轮廓的列表
for contour in contours:
# contourArea方法用于计算轮廓的面积
area = cv2.contourArea(contour)
# arcLength方法用于计算轮廓的周长或弧长
length = cv2.arcLength(contour, True)
# 如果检测区域面积在5025-7225之间,周长在300-380之间,则是目标区域 可以根据实际情况进行调整
if 5025 < area < 7225 and 300 < length < 380:
# 计算轮廓的边界矩形,得到坐标和宽高
# x, y: 边界矩形左上角点的坐标。
# w, h: 边界矩形的宽度和高度。
x, y, w, h = cv2.boundingRect(contour)
print("计算出目标区域的坐标及宽高:", x, y, w, h)
# 在目标区域上画一个红框看看效果
cv2.rectangle(image, (x, y), (x + w, y + h), (0, 0, 255), 2)
cv2.imwrite("./MarkPicture/mark_picture.png", image)
return x
return 0
try:
driver.get("https://accounts.douban.com/passport/login")
driver.implicitly_wait(2)
# 切换到密码登录
driver.find_element(by=By.CLASS_NAME, value="account-tab-account").click()
print("切换到密码登录")
driver.find_element(By.ID, 'username').send_keys("18012345678")
print("输入账号")
driver.find_element(By.ID, 'password').send_keys("123456")
print("输入密码")
driver.find_element(By.CLASS_NAME, 'btn.btn-account.btn-active').click()
print("点击登录豆瓣")
# 点击登陆后会弹出滑动验证
# 滑块验证在一个iframe中,需要将鼠标放入iframe中,不然找不到元素
driver.switch_to.frame('tcaptcha_iframe_dy')
try:
WebDriverWait(driver, 10).until(
EC.visibility_of_element_located((By.ID, 'slideBg'))
)
# 滑块背景
slide_bg_str = driver.find_element(By.ID, 'slideBg').get_attribute("style")
# print(slide_bg_str)
pattern = 'background-image: url\\(\".*?\"\\);'
slide_bg_url = str(re.findall(pattern, slide_bg_str, re.S)[0]).split('"')[1]
# print(slide_bg_url)
request.urlretrieve(url=slide_bg_url, filename='./MarkPicture/slide_bg.png')
# 找到滑块图片需要补全的部分
dis = get_pos('./MarkPicture/slide_bg.png')
# 找到下方滑块按钮
swipe = driver.find_element(By.XPATH, '//*[@id="tcOperation"]/div[6]')
swipe_x = swipe.location['x']
# 下方滑块按钮到目标区域的移动距离(缺口坐标的水平位置距离小滑块的水平坐标相减的差)
# 新缺口坐标=原缺口坐标*新画布宽度/原画布宽度
newDis = int(dis * 340 / 672)
# 需要下方滑块按钮需要移动的距离
need_move_dis = newDis - swipe_x
ActionChains(driver).click_and_hold(swipe).perform()
step = 0
cur_step = 0
while step < need_move_dis:
cur_step = random.randint(3, 10)
step += cur_step
ActionChains(driver).move_by_offset(xoffset=cur_step, yoffset=0).perform()
ActionChains(driver).release().perform()
except TimeoutException:
print("未找到滑动验证")
time.sleep(3)
finally:
driver.close()
测试地址:https://demos.geetest.com/slide-custom.html
注意:由于极验验证码的安全性太强,此方法仅能做到拖动滑块到缺口,但是这样仍能被极验网站判定为非人为操作,导致验证失败。所以本部分内容仅作为通过滑块验证的方法分享。
与上面不同,这里的滑块背景在canvas块中,无法通过元素属性获得图片地址,所以采用的方案是截图。
分析页面中的元素,可以看到滑块验证码中有两个重叠的canvas块,一个是滑块背景图,一个是小滑块。这里的思路是先将小滑块canvas的display
属性设置为none
,这样将小滑块暂时隐藏,然后截图保存滑块背景图,然后将小滑块canvas的display
属性设置为block
,这样小滑块就重新展示出来了。
# 滑块背景
slice_bg = driver.find_element(By.CLASS_NAME, 'geetest_canvas_bg.geetest_absolute')
# 滑块
small_slice = driver.find_element(By.CLASS_NAME, 'geetest_canvas_slice.geetest_absolute')
# 隐藏滑块
driver.execute_script("arguments[0].style.display='none';", small_slice)
# 截取滑块背景
es.element_shot(driver, slice_bg, "./MarkPicture/canvas_slice_bg.png")
# 显示滑块
driver.execute_script("arguments[0].style.display='block';", small_slice)
element_shot(driver: webdriver, element, save_path: str)是重新封装的函数,具体实现如下:
def element_shot(driver: webdriver, element, save_path: str):
try:
# 获取元素位置和尺寸
location = element.location
size = element.size
# 获取设备像素比例 (解决高分辨率屏幕缩放问题)
# device_pixel_ratio = driver.execute_script("return window.devicePixelRatio")
# 计算实际像素坐标(乘以设备像素比例)
# x = location['x'] * device_pixel_ratio
# y = location['y'] * device_pixel_ratio
# width = size['width'] * device_pixel_ratio
# height = size['height'] * device_pixel_ratio
x = location['x']
y = location['y']
width = size['width']
height = size['height']
# 截取全屏并保存
driver.save_screenshot('./MarkPicture/temp_screenshot.png')
# 使用Pillow打开截图并裁剪元素区域
full_image = Image.open("./MarkPicture/temp_screenshot.png")
element_image = full_image.crop((
x, # 左边界
y, # 上边界
x + width, # 右边界
y + height # 下边界
)).convert("RGB")
# 保存元素截图
element_image.save(save_path, optimize=False, quality=100)
print(save_path + " 保存成功!!!")
except Exception as e:
print(f"截图失败: {e}")
同获取滑块背景图片一样的步骤,不过还要多出一步。因为截图的是整个canvas,canvas中滑块只占了一小部分,所以还需要使用轮廓检测算法算出滑块截图中滑块的位置,然后再次截图保存。在截图之前要记得先使用轮廓检测算法找到滑块的位置并记录下来。
截图保存的这一步可以封装为以下这个函数:
def image_cut_bg(driver: webdriver, imageSrc):
# 读取图像文件并返回一个image数组表示的图像对象
image = cv2.imread(imageSrc)
# 转灰度
image = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
# print("imageSrc: " + imageSrc)
# GaussianBlur方法进行图像模糊化/降噪操作。
# 它基于高斯函数(也称为正态分布)创建一个卷积核(或称为滤波器),该卷积核应用于图像上的每个像素点。
blurred = cv2.GaussianBlur(image, (5, 5), 0, 0)
# Canny方法进行图像边缘检测
# image: 输入的单通道灰度图像。
# threshold1: 第一个阈值,用于边缘链接。一般设置为较小的值。
# threshold2: 第二个阈值,用于边缘链接和强边缘的筛选。一般设置为较大的值
canny = cv2.Canny(blurred, 100, 200) # 轮廓
# findContours方法用于检测图像中的轮廓,并返回一个包含所有检测到轮廓的列表。
# contours(可选): 输出的轮廓列表。每个轮廓都表示为一个点集。
# hierarchy(可选): 输出的轮廓层次结构信息。它描述了轮廓之间的关系,例如父子关系等。
contours, hierarchy = cv2.findContours(canny, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE)
x = 0
y = 0
w = 0
h = 0
# 遍历检测到的所有轮廓的列表
for contour in contours:
# contourArea方法用于计算轮廓的面积
area = cv2.contourArea(contour)
print("area: " + str(area))
# arcLength方法用于计算轮廓的周长或弧长
length = cv2.arcLength(contour, True)
print("length: " + str(length))
# 如果检测区域面积在5025-7225之间,周长在300-380之间,则是目标区域
if 1500 < area < 2700 and 140 < length < 280:
# 计算轮廓的边界矩形,得到坐标和宽高
# x, y: 边界矩形左上角点的坐标。
# w, h: 边界矩形的宽度和高度。
x, y, w, h = cv2.boundingRect(contour)
print("es 计算出目标区域的坐标及宽高:", x, y, w, h)
# 在目标区域上画一个红框看看效果
# cv2.rectangle(image, (x, y), (x + w, y + h), (0, 0, 255), 2)
# cv2.imwrite("./MarkPicture/canvas_small_slice.png", image)
# 使用Pillow打开截图并裁剪元素区域
full_image = Image.open(imageSrc)
element_image = full_image.crop((
x, # 左边界
y, # 上边界
x + w, # 右边界
y + h # 下边界
)).convert("RGB")
# 保存元素截图
element_image.save(imageSrc, optimize=False, quality=100)
print("去除背景完成")
这里主要用的是模板匹配算法,具体参考:OpenCV第十章——模板匹配
# 模板匹配
def detect_captcha_gap(bg, tp):
"""
bg: 滑块背景图片
tp: 滑块图片
return:空缺距背景图左边的距离
"""
# 读取背景图片和缺口图片
bg_img = cv2.imread(bg) # 背景图片
tp_img = cv2.imread(tp) # 缺口图片
# bg_img = bg
# tp_img = tp
# 识别图片边缘
bg_edge = cv2.Canny(bg_img, 200, 300)
tp_edge = cv2.Canny(tp_img, 300, 350)
# 转换图片格式
bg_pic = cv2.cvtColor(bg_edge, cv2.COLOR_GRAY2RGB)
tp_pic = cv2.cvtColor(tp_edge, cv2.COLOR_GRAY2RGB)
cv2.imwrite("./MarkPicture/bg_style.png", bg_pic) # 保存背景轮廓提取
cv2.imwrite("./MarkPicture/slide_style.png", tp_pic) # 保存滑块背景提取
# 缺口匹配
res = cv2.matchTemplate(bg_pic, tp_pic, cv2.TM_CCOEFF_NORMED)
min_val, max_val, min_loc, max_loc = cv2.minMaxLoc(res) # 寻找最优匹配
th, tw = tp_pic.shape[:2]
tl = max_loc # 左上角点的坐标
# 返回缺口的左上角X坐标
br = (tl[0] + tw, tl[1] + th) # 右下角点的坐标
cv2.rectangle(bg_img, tl, br, (0, 0, 255), 2) # 绘制矩形
cv2.imwrite("./MarkPicture/result_new.png", bg_img) # 保存在本地
# 返回缺口的左上角X坐标
return tl[0]
# 滑块按钮
swipe = driver.find_element(By.CLASS_NAME, 'geetest_slider_button')
ActionChains(driver).click_and_hold(swipe).perform()
step = 0
need_step = dis1 - dis2
cur_step = 0
# 模拟抖动
while step < need_step:
cur_step = random.randint(-4, 15)
if step + cur_step > need_step:
ActionChains(driver).move_by_offset(xoffset=need_step - step + 1, yoffset=random.randint(-5, 7)).perform()
break
step += cur_step
ActionChains(driver).move_by_offset(xoffset=cur_step, yoffset=random.randint(-5, 7)).perform()
ActionChains(driver).release().perform()
# 跳过极验滑块验证
import re
import random
from selenium import webdriver # 导入selenium的webdriver模块
from selenium.common import TimeoutException
from selenium.webdriver.common.by import By # 引入By类选择器
import time
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
import urllib.request as request
from selenium.webdriver.common.action_chains import ActionChains # 动作类
import cv2
import utils.element_shot as es
# 防止识别出来是selenium
option = webdriver.EdgeOptions()
option.add_experimental_option('excludeSwitches', ['enable-automation'])
# 如何实现让selenium规避被检测的风险
driver = webdriver.Edge(options=option)
script = 'Object.defineProperty(navigator,"webdriver",{get:()=>undefined,});'
driver.execute_script(script)
# 封装的计算图片距离的算法
def get_pos(imageSrc):
# 读取图像文件并返回一个image数组表示的图像对象
image = cv2.imread(imageSrc)
# 转灰度
image = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
print("imageSrc: " + imageSrc)
# GaussianBlur方法进行图像模糊化/降噪操作。
# 它基于高斯函数(也称为正态分布)创建一个卷积核(或称为滤波器),该卷积核应用于图像上的每个像素点。
blurred = cv2.GaussianBlur(image, (5, 5), 0, 0)
# Canny方法进行图像边缘检测
# image: 输入的单通道灰度图像。
# threshold1: 第一个阈值,用于边缘链接。一般设置为较小的值。
# threshold2: 第二个阈值,用于边缘链接和强边缘的筛选。一般设置为较大的值
canny = cv2.Canny(blurred, 100, 200) # 轮廓
# findContours方法用于检测图像中的轮廓,并返回一个包含所有检测到轮廓的列表。
# contours(可选): 输出的轮廓列表。每个轮廓都表示为一个点集。
# hierarchy(可选): 输出的轮廓层次结构信息。它描述了轮廓之间的关系,例如父子关系等。
contours, hierarchy = cv2.findContours(canny, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE)
# 遍历检测到的所有轮廓的列表
for contour in contours:
# contourArea方法用于计算轮廓的面积
area = cv2.contourArea(contour)
print("area: " + str(area))
# arcLength方法用于计算轮廓的周长或弧长
length = cv2.arcLength(contour, True)
print("length: " + str(length))
# 如果检测区域面积在5025-7225之间,周长在300-380之间,则是目标区域
if 1500 < area < 2700 and 140 < length < 280:
# 计算轮廓的边界矩形,得到坐标和宽高
# x, y: 边界矩形左上角点的坐标。
# w, h: 边界矩形的宽度和高度。
x, y, w, h = cv2.boundingRect(contour)
print("计算出目标区域的坐标及宽高:", x, y, w, h)
# 在目标区域上画一个红框看看效果
cv2.rectangle(image, (x, y), (x + w, y + h), (0, 0, 255), 2)
cv2.imwrite("./MarkPicture/mark_picture.png", image)
return x
return 0
# 模板匹配
def detect_captcha_gap(bg, tp):
"""
bg: 背景图片
tp: 缺口图片
return:空缺距背景图左边的距离
"""
# 读取背景图片和缺口图片
bg_img = cv2.imread(bg) # 背景图片
tp_img = cv2.imread(tp) # 缺口图片
# bg_img = bg
# tp_img = tp
# 识别图片边缘
bg_edge = cv2.Canny(bg_img, 200, 300)
tp_edge = cv2.Canny(tp_img, 300, 350)
# 转换图片格式
bg_pic = cv2.cvtColor(bg_edge, cv2.COLOR_GRAY2RGB)
tp_pic = cv2.cvtColor(tp_edge, cv2.COLOR_GRAY2RGB)
cv2.imwrite("./MarkPicture/bg_style.png", bg_pic) # 保存背景轮廓提取
cv2.imwrite("./MarkPicture/slide_style.png", tp_pic) # 保存滑块背景提取
# 缺口匹配
res = cv2.matchTemplate(bg_pic, tp_pic, cv2.TM_CCOEFF_NORMED)
min_val, max_val, min_loc, max_loc = cv2.minMaxLoc(res) # 寻找最优匹配
th, tw = tp_pic.shape[:2]
tl = max_loc # 左上角点的坐标
# 返回缺口的左上角X坐标
br = (tl[0] + tw, tl[1] + th) # 右下角点的坐标
cv2.rectangle(bg_img, tl, br, (0, 0, 255), 2) # 绘制矩形
cv2.imwrite("./MarkPicture/result_new.png", bg_img) # 保存在本地
# 返回缺口的左上角X坐标
return tl[0]
try:
driver.get("https://demos.geetest.com/slide-custom.html")
button = driver.find_element(By.ID, 'captcha')
time.sleep(2)
button.click()
WebDriverWait(driver, 10).until(
EC.visibility_of_element_located((By.CLASS_NAME, 'geetest_canvas_bg.geetest_absolute'))
)
print("找到滑块背景")
slice_bg = driver.find_element(By.CLASS_NAME, 'geetest_canvas_bg.geetest_absolute')
small_slice = driver.find_element(By.CLASS_NAME, 'geetest_canvas_slice.geetest_absolute')
# small_slice.__setattr__("style", 'display: none;')
# 隐藏小滑块
driver.execute_script("arguments[0].style.display='none';", small_slice)
time.sleep(1)
# 截取滑块背景
es.element_shot(driver, slice_bg, "./MarkPicture/canvas_slice_bg.png")
# dis1 = get_pos('./MarkPicture/canvas_slice_bg.png')
# 显示小滑块
driver.execute_script("arguments[0].style.display='block';", small_slice)
# 隐藏滑块背景
driver.execute_script("arguments[0].style.display='none';", slice_bg)
time.sleep(1)
# 截取小滑块
es.element_shot(driver, small_slice, "./MarkPicture/canvas_small_slice.png")
dis2 = get_pos('./MarkPicture/canvas_small_slice.png')
# dis2 = 3
print("dis2: " + str(dis2))
# 去掉白色背景
es.image_cut_bg(driver, "./MarkPicture/canvas_small_slice.png")
# 显示滑块背景
driver.execute_script("arguments[0].style.display='block';", slice_bg)
dis1 = detect_captcha_gap("./MarkPicture/canvas_slice_bg.png", "./MarkPicture/canvas_small_slice.png")
print("dis1: " + str(dis1))
swipe = driver.find_element(By.CLASS_NAME, 'geetest_slider_button')
ActionChains(driver).click_and_hold(swipe).perform()
step = 0
need_step = dis1 - dis2
cur_step = 0
while step < need_step:
cur_step = random.randint(-4, 15)
if step + cur_step > need_step:
ActionChains(driver).move_by_offset(xoffset=need_step - step + 1, yoffset=random.randint(-5, 7)).perform()
break
step += cur_step
ActionChains(driver).move_by_offset(xoffset=cur_step, yoffset=random.randint(-5, 7)).perform()
ActionChains(driver).release().perform()
except TimeoutException:
print("未找到滑块验证")
finally:
time.sleep(3)
driver.close()