爬虫——验证码
滑动验证码
千辛万苦找到了2个不同的滑动验证码的网站,一个是极验的,是以canvas标签放验证图片的,第二个是YY的,使用iframe标签做验证的,两者获取图片的方式会略有不同。该贴仅做技术积累。
canvas验证码识别
canvas的标签比较容易定位:

我们需要的就是这三个canvas,其中geetest_canvas_fullbg不需要,因为它并不展示display:none;,而且它是原图,我们要的是滑块图geetest_canvas_slice以及缺口图geetest_canvas_bg。
from selenium import webdriver
from selenium.webdriver.chrome.service import Service
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.common.by import By
from selenium.webdriver.support import expected_conditions as EC
import base64
import cv2
import numpy as np
def test():
url = 'https://demos.geetest.com/slide-float.html'
driver_path = r"E:\selenium\driver\chromedriver.exe"
chrome_path = r'E:\selenium\chrome\Application\chrome.exe'
profile_path = r"E:\selenium\data\profiles"
cache_path = r"E:\selenium\data\caches"
options = webdriver.ChromeOptions()
options.binary_location = chrome_path
options.add_argument('--no-sandbox')
options.add_argument("--start-maximized")
options.add_argument('--disable-dev-shm-usage')
options.add_argument(f'--user-data-dir={profile_path}')
options.add_argument(f'--disk-cache-dir={cache_path}')
options.add_argument("--disable-blink-features=AutomationControlled") # 禁用自动化控制提示
options.add_experimental_option("excludeSwitches", ["enable-automation"]) # 隐藏自动化标志
user_agent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36"
options.add_argument(f"user-agent={user_agent}")
#options.add_argument("--window-size=1920,1080")
# 启用 WebGL 和 GPU 加速(部分反爬检测 GPU 支持)
options.add_argument("--use-gl=desktop")
options.add_argument("--enable-webgl")
options.set_capability(
"goog:loggingPrefs", {"performance": "ALL"}
) # 开启日志
service = Service(executable_path=driver_path)
driver = webdriver.Chrome(service=service, options=options)
driver.get(url)
tcaptcha = WebDriverWait(driver, 10).until(
EC.element_to_be_clickable((By.CLASS_NAME, 'geetest_radar_tip')))
#print("元素位置:" + tcaptcha.location)
# can only concatenate str (not "dict") to str
tcaptcha.click()
tcaptcha_slice = WebDriverWait(driver, 15).until(
EC.visibility_of_element_located((By.CLASS_NAME, 'geetest_canvas_slice')))
js_bg = 'return document.getElementsByClassName("geetest_canvas_bg")[0].toDataURL("image/png");' # 获取背景图片
js_slice = 'return document.getElementsByClassName("geetest_canvas_slice")[0].toDataURL("image/png");' # 获取滑块图片
bg_data = base64.b64decode(driver.execute_script(js_bg).split(',')[1]) # 解码背景图片
slice_data = base64.b64decode(driver.execute_script(js_slice).split(',')[1]) # 解码滑块图片
img_bg = cv2.imdecode(np.frombuffer(bg_data, dtype=np.uint8), cv2.IMREAD_COLOR) # 解码背景图片
img_slice = cv2.imdecode(np.frombuffer(slice_data, dtype=np.uint8), cv2.IMREAD_COLOR) # 解码滑块图片
# 保存图片
cv2.imwrite(r'E:\Temp\pic\2\bg4.png', img_bg) # 保存背景图片
cv2.imwrite(r'E:\Temp\pic\2\slice4.png', img_slice) # 保存滑块图片
if driver:
driver.quit()# 退出
这里解释一下为什么要保存到本地:因为cv2.matchTemplate在匹配上存在一些问题,所以将图片下载下来进行匹配并找规律。
js_bg = 'return document.getElementsByClassName("geetest_canvas_bg")[0].toDataURL("image/png");' # 获取背景图片
js_slice = 'return document.getElementsByClassName("geetest_canvas_slice")[0].toDataURL("image/png");' # 获取滑块图片
# 这两句话是获取canvas图片的关键,因为从上面的截图可以发现,无法正常获取内容,没有src!没有url!所以只能通过执行js的方式获取了。
路径分析
将图片文件使用opencv-python读取,然后匹配位置,得到坐标。具体的做法是:
- 读取图片那文件。
- 移除阴影部分,当然不移除也行,可能结果上有偏差吧。我自己测试的话,还不如不移除!!!
- 裁剪图片。因为我发现,如果不裁剪图片,每次计算的结果,或者说是应该移动的变量什么的,每次都是0,进一步发现,是解码的图片除了滑块的位置的
BGR数值都是[6,6,6],可能是这些数值影响了匹配结果,那不管了,直接对他们进行裁剪,裁剪到只剩下滑块的大小。 - 经过变换裁剪之后的图片通道是
BGRA4通道,而缺口图片是BGR3通道,所以要进行变换 - 匹配数值,并验证
def pic_match():
img_bg = cv2.imread(r'E:\Temp\pic\2\bg4.png')
img_slice = cv2.imread(r'E:\Temp\pic\2\slice4.png')
裁剪
问的deepseek怎么写的…
def auto_crop_slice(slice_img):
"""
自动裁剪滑块有效区域(基于亮度阈值)
返回值:
cropped: 裁剪后的有效区域图像
y_start: 有效区域的起始Y坐标(用于对齐背景图)
"""
# 转换为灰度图
gray = cv2.cvtColor(slice_img, cv2.COLOR_BGR2GRAY)
clahe = cv2.createCLAHE(clipLimit=3.0, tileGridSize=(8,8))
enhanced = clahe.apply(gray)
# 应用阈值分割
# 自适应阈值处理
thresh = cv2.adaptiveThreshold(enhanced, 255,
cv2.ADAPTIVE_THRESH_GAUSSIAN_C,
cv2.THRESH_BINARY_INV, 15, 5)
# 形态学操作(先开运算去噪,再闭运算填充)
kernel = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (5, 5))
opened = cv2.morphologyEx(thresh, cv2.MORPH_OPEN, kernel, iterations=1)
closed = cv2.morphologyEx(opened, cv2.MORPH_CLOSE, kernel, iterations=2)
# 寻找轮廓并筛选
contours, _ = cv2.findContours(closed, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
if not contours:
return cv2.cvtColor(slice_img, cv2.COLOR_BGR2BGRA), 0
# 根据面积和宽高比筛选有效轮廓
valid_contours = []
for cnt in contours:
area = cv2.contourArea(cnt)
x, y, w, h = cv2.boundingRect(cnt)
aspect_ratio = w / float(h)
if area > 100 and 0.5 < aspect_ratio < 2.5: # 根据实际情况调整
valid_contours.append(cnt)
if not valid_contours:
return cv2.cvtColor(slice_img, cv2.COLOR_BGR2BGRA), 0
# 合并有效轮廓
combined_cnt = np.vstack(valid_contours)
x, y, w, h = cv2.boundingRect(combined_cnt)
# 创建透明通道并裁剪
bgra = cv2.cvtColor(slice_img, cv2.COLOR_BGR2BGRA)
mask = np.zeros_like(enhanced)
cv2.drawContours(mask, [combined_cnt], -1, 255, -1)
bgra[:, :, 3] = cv2.bitwise_and(bgra[:, :, 3], mask)
# 二次精确裁剪(去除边缘黑边)
roi = bgra[y:y + h, x:x + w]
gray_roi = cv2.cvtColor(roi, cv2.COLOR_BGRA2GRAY)
_, th_roi = cv2.threshold(gray_roi, 1, 255, cv2.THRESH_BINARY)
contours_roi, _ = cv2.findContours(th_roi, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
if contours_roi:
x2, y2, w2, h2 = cv2.boundingRect(max(contours_roi, key=cv2.contourArea))
final_roi = roi[y2:y2 + h2, x2:x2 + w2]
return final_roi, y + y2
return roi, y
这段代码中:
clahe = cv2.createCLAHE(clipLimit=3.0, tileGridSize=(8,8)),创建一个CLAHE (Contrast Limited Adaptive Histogram Equalization) 对比度限制自适应直方图均衡化对象,用于增强图像的局部对比度,特别适用于低对比度区域。clipLimit=3.0: 对比度限制阈值,控制过度增强的程度。较低的值会限制过度对比度增强,避免噪声放大。默认值通常是40.0,较低的值可以更好地抑制噪声。tileGridSize=(8,8): 图像分块大小,用于局部直方图均衡化。
enhanced = clahe.apply(gray): 对灰度图像应用CLAHE算法,增强局部对比度。thresh = cv2.adaptiveThreshold(enhanced, 255, cv2.ADAPTIVE_THRESH_GAUSSIAN_C, cv2.THRESH_BINARY_INV, 15, 5):对增强后的图像应用自适应阈值分割,将图像转化为二值图像。cv2.ADAPTIVE_THRESH_GAUSSIAN_C: 自适应阈值算法类型。ADAPTIVE_THRESH_GAUSSIAN_C表示使用高斯加权平均来计算局部阈值。这种方法能更好地适应图像的亮度变化。cv2.THRESH_BINARY_INV: 阈值类型。THRESH_BINARY_INV表示将阈值处理为反二值,即低于阈值的像素变为白色(255),高于阈值的像素变为黑色(0)。15: 邻域大小,用于计算每个像素的局部阈值(15x15像素的邻域)。5: 常数C,减去它之后得到的局部阈值,这有助于调节图像的二值化结果。
opened = cv2.morphologyEx(thresh, cv2.MORPH_OPEN, kernel, iterations=1):进行开运算,主要用于去噪。iterations=1: 迭代次数,表示开运算执行的次数。通常,较高的迭代次数有助于更彻底地去除噪声。
closed = cv2.morphologyEx(opened, cv2.MORPH_CLOSE, kernel, iterations=2):进行闭运算,主要用于填充小的空洞。cv2.findContours:查找所有外部轮廓。cv2.RETR_EXTERNAL:轮廓检索模式,仅检测最外层轮廓(忽略孔洞)。cv2.CHAIN_APPROX_SIMPLE:轮廓压缩方法,仅保留关键点坐标(如矩形只保留4个角点)。- 返回
contours:检测到的轮廓列表,每个轮廓由点集组成。
valid_contours为了进行轮廓筛选,将筛选出面积大于100且宽高比在0.5到2.5之间的轮廓。np.vstack(valid_contours)和cv2.boundingRect(combined_cnt)是合并多个有效轮廓,并计算其外接矩形,返回外接矩形的左上角坐标和宽高。cv2.cvtColor(slice_img, cv2.COLOR_BGR2BGRA):将BGR图像转换为BGRA格式(添加Alpha通道)。cv2.drawContours:在掩模上绘制轮廓,填充为白色(255)。cv2.bitwise_and:将Alpha通道与掩模结合,去除背景。cv2.boundingRect:计算最大轮廓的外接矩形。
至此,图形裁剪告一段落了 ~ ~ ~
匹配
我们先接收一下裁剪之后的滑块图片,
def pic_match():
#...
cropped_slice, y_start = auto_crop_slice(img_slice)
bg_roi = img_bg[y_start:y_start + cropped_slice.shape[0], :]
# 根据原理,滑块的y轴位置,必定与缺口位置一致,不一致就无法正常匹配。当然,也有恶心的会将滑块位置与缺口位置的y轴拆开,那就有点反人类了。
# 仅考虑滑块与缺口处在同一水平上,然后裁剪bg图片。得到bg_roi
从上文中我们得知,cropped_slice是4通道的,因为有执行cv2.cvtColor(slice_img, cv2.COLOR_BGR2BGRA)这句话,而bg_roi则是3通道的,所以我们需要对他进行通道转换。将cropped_slice转换为3通道的,这样才能进行匹配,否则会报错。
def pic_match():
#...
bg_roi = img_bg[y_start:y_start + cropped_slice.shape[0], :]
# 检查通道数
#print("bg_roi通道数:", bg_roi.shape[2] if len(bg_roi.shape) == 3 else 1)
#print("cropped_slice通道数:", cropped_slice.shape[2] if len(cropped_slice.shape) == 3 else 1)
# 如果cropped_slice是4通道(BGRA),转换为3通道(BGR)
if cropped_slice.shape[2] == 4:
cropped_slice = cv2.cvtColor(cropped_slice, cv2.COLOR_BGRA2BGR)
# 如果bg_roi是3通道,确保滑块也是3通道
if bg_roi.shape[2] == 3 and cropped_slice.shape[2] != 3:
cropped_slice = cv2.cvtColor(cropped_slice, cv2.COLOR_GRAY2BGR)
使用匹配模板,cv2的匹配模板有两种cv2.TM_SQDIFF_NORMED和cv2.TM_CCOEFF_NORMED,两种里面,匹配的结果略有不同,我更倾向于使用,cv2.TM_CCOEFF_NORMED,从我的匹配经验来看,cv2.TM_CCOEFF_NORMED的结果更接近一点,deepseek还跟我犟,说第四个参数是移动距离,人家就是给的这么用的!=。=可能最后是他对吧,但是我觉得从这个验证码来看,我觉得还是绝的第三个参数是移动距离。
def pic_match():
#...
if bg_roi.shape[2] == 3 and cropped_slice.shape[2] != 3:
cropped_slice = cv2.cvtColor(cropped_slice, cv2.COLOR_GRAY2BGR)
result = cv2.matchTemplate(bg_roi, cropped_slice, cv2.TM_CCOEFF_NORMED)
_, _, max_loc , min_loc = cv2.minMaxLoc(result)
print(max_loc)
print(min_loc)
打印结果可以得到
(69, 0)
(28, 0)
使用电脑自带的画图工具可以查看像素位置:

也就是说,max_loc和真实的像素位置进差6个像素点,所以他更加接近。
我们通过将max_loc提取出x轴,然后通过再通过计算浏览器渲染与实际情况的比较,计算真实应该移动的距离。
def pic_match():
# ...
_, _, max_loc , min_loc = cv2.minMaxLoc(result)
gap_center_x = max_loc[0] - random.randint(3,5) # 其实有个范围上的误差,只要再这个误差里面,都判定是人的操作,这里就随机搞一下,不搞也没事。
# 因为y轴必须再同一水平面上,所以y轴的差值为0,就不考虑y轴了。
# 获取Canvas元素在页面中的实际尺寸
#canvas = driver.find_element(By.CLASS_NAME, "geetest_canvas_bg")
#canvas_width = canvas.size['width']
# 计算图像到实际页面的缩放比例
#scale_ratio = canvas_width / bg_roi.shape[1]
# 假设比例为1,真实的情况以上面注释的内容为主
scale_ratio = 1
adjusted_distance = gap_center_x * scale_ratio
随机轨迹
来分析一下人的正常的操作:
1.按住然后加速拖动前20%,
2.匀速拖动中间的60%,
3.后面20%减速。
我觉的很合理,时间上在2.2s到2.8s这个区间很合理。
def generate_optimized_track(distance):
""" 优化版轨迹生成函数 (时间控制: 2.2-2.8秒) """
track = []
current = 0
# 阶段划分参数
accelerate_end = distance * 0.2 # 前20%加速
decelerate_start = distance * 0.8 # 后20%减速
while current < distance:
# 动态调整步长范围
if current < accelerate_end:
step = random.randint(4, 8) # 加速段大步长
elif current < decelerate_start:
step = random.randint(3, 5) # 中段稳定速度
else:
step = random.randint(1, 3) # 减速段小步长
# 添加随机扰动 (更符合人手特征)
step += random.choice([-1, 0, 0, 1]) # 偏向正向扰动
# 防止超出目标距离
if current + step > distance:
step = distance - current
track.append(step)
current += step
步长OK了,但是时间还需要重新计算,就比如要讲时间控制在2.2s到2.8s区间里面。所以我们要计算一下时间。
def generate_optimized_track(distance, min_time=2.2, max_time=2.8):
""" 优化版轨迹生成函数 (时间控制: 2.2-2.8秒) """
track = []
current = 0
# 阶段划分参数
accelerate_end = distance * 0.5 # 前50%加速
decelerate_start = distance * 0.8 # 后20%减速
while current < distance:
if current < accelerate_end:
step = random.randint(int(distance * 0.08), int(distance * 0.15)) # 大跨度步长
elif current < decelerate_start:
step = random.randint(int(distance * 0.04), int(distance * 0.08))
else:
step = random.randint(1, max(2, int(distance * 0.02)))
# 智能防溢出
step = min(step, distance - current)
track.append(step)
current += step
# 计算目标时间参数
target_time = random.uniform(min_time, max_time)
time_per_step = target_time / len(track)
# 生成紧凑时间序列
time_sequence = []
for i in range(len(track)):
if i < len(track) // 3:
# 前段快速移动 (60%时间分配)
t = random.uniform(time_per_step * 0.4, time_per_step * 0.8)
elif i < len(track) * 2 // 3:
# 中段正常速度 (30%时间)
t = random.uniform(time_per_step * 0.8, time_per_step * 1.2)
else:
# 末段减速 (10%时间)
t = random.uniform(time_per_step * 1.5, time_per_step * 2.0)
time_sequence.append(round(t, 3))
# 强制时间总量校准
total_time = sum(time_sequence)
scale_factor = target_time / total_time
time_sequence = [t * scale_factor for t in time_sequence]
return list(zip(track, time_sequence))
移动
将使用selenium自带的移动模块,因为滑动移动肯定要滑动的!
from selenium.webdriver import ActionChains
def human_drag(driver, element, distance):
optimized_track = generate_optimized_track(distance)
actions = ActionChains(driver, duration=0)
actions.click_and_hold(element)
start = time.time()
# 执行滑动
actions.click_and_hold(element)
for step, t in optimized_track:
y = random.choice([0, 0, 0, 1, -1])
actions.move_by_offset(step, y)
if t > 0.01: # 仅对大于10ms的暂停生效
actions.pause(t)
actions.release().perform()
# 计算实际耗时
actual_total = time.time() - start
print(actual_total)
def test():
# ...
adjusted_distance = gap_x * scale_ratio
print(f"需滑动像素距离: {adjusted_distance:.1f}")
slider = driver.find_element(By.CLASS_NAME, "geetest_slider_button")
human_drag(driver, slider, adjusted_distance)

canvas就酱~
iframe验证
iframe的验证与canvas类似,只不过要切换到iframe里面。

可以看到,有两个iframe,所以要切换两次iframe,切换一次是不够的。
def iframe_verify(driver, url):
try:
driver.get(url)
#parent_html
WebDriverWait(driver, 10).until(
EC.visibility_of_element_located((By.CLASS_NAME, 'mod-forgotPrd')))
iframe_father_xpath = "//iframe[contains(@src,'https://zc.yy.com/reg/udb/reg4udb.do?mode=udb&type=&appid=1&action=3&busiurl=https%3A%2F%2Faq.yy.com&fromadv=udbclsd_r&reqDomainList=')]"
iframe_child_xpath = "//iframe[contains(@src,'https://zc.yy.com/reg/ca/complexVerify.do?isNeedVcode=11&mode=aquname&appid=1&anchorId=interActiveWrap')]"
WebDriverWait(driver, 10).until(
EC.frame_to_be_available_and_switch_to_it((By.XPATH, iframe_father_xpath)))
WebDriverWait(driver, 10).until(
EC.frame_to_be_available_and_switch_to_it((By.XPATH, iframe_child_xpath)))
except Exception as e:
print("iframe验证失败:", e)
finally:
if driver:
driver.quit()
获取背景图片和滑块图片。

可以看到,一开始并没有给缺口的图片,也没有给滑块的图片,需要我们点击之后才会出现。

那么我们就点击之后,再搞图片咯。

可以看到,第一个是背景(包含缺口的),第二个是滑块,然后又发现,诶嘿!有路径,直接搞!
def iframe_verify(driver, url):
# ...
WebDriverWait(driver, 10).until(
EC.frame_to_be_available_and_switch_to_it((By.XPATH, iframe_child_xpath)))
slice_button = WebDriverWait(driver, 10).until(
EC.element_to_be_clickable((By.XPATH, '//span[@node-name="dragPanel"]')))
actions = ActionChains(driver)
actions.move_to_element(slice_button).pause(0.5).click().perform()
img_bg_src = WebDriverWait(driver, 15).until(
EC.presence_of_element_located((By.XPATH, "//img[@node-name='bgImageSrc']"))
)
img_bg_display_width = int(img_bg_src.get_attribute("width")) # 到时候计算放大或者缩小的比例的。
img_bg_response = requests.get(img_bg_src.get_attribute("src"))
img_bg = cv2.imdecode(np.asarray(bytearray(img_bg_response.content), dtype=np.uint8), cv2.IMREAD_COLOR)
img_slice_src = WebDriverWait(driver, 15).until(
EC.presence_of_element_located((By.XPATH, "//img[@node-name='dragImageSrc']"))
)
img_slice_response = requests.get(img_slice_src.get_attribute("src"))
img_slice = cv2.imdecode(np.asarray(bytearray(img_slice_response.content), dtype=np.uint8), cv2.IMREAD_COLOR)


可以使用canvas里面的裁剪的方式吗,可以试试,只不过答案是不可以的,因为两者的画布大小并不一致!所以直接匹配即可。
def iframe_verify(driver, url):
# ...
img_slice = cv2.imdecode(np.asarray(bytearray(img_slice_response.content), dtype=np.uint8), cv2.IMREAD_COLOR) # 接上面的内容
result = cv2.matchTemplate(img_bg, img_slice, cv2.TM_CCOEFF_NORMED)
_, _, max_loc, min_loc = cv2.minMaxLoc(result)
gap_x = max_loc[0] - random.randint(15, 20) # 差距好像挺大的,所以这里就随机减一段像素
scale_ratio = img_bg_display_width / img_bg.shape[1]
adjusted_distance = gap_x * scale_ratio
print(f"需滑动像素距离: {adjusted_distance:.1f}")
闪动一段时间之后,不能够进行移动,所以我们就点击之后,沉默2秒以上。
print(f"需滑动像素距离: {adjusted_distance:.1f}")
slider = WebDriverWait(driver, 10).until(
EC.element_to_be_clickable((By.XPATH, '//span[@node-name="dragPanel"]')))
time.sleep(5)
human_drag(driver, slider, adjusted_distance)
except Exception as e:
print("iframe验证失败:", e)
finally:
if driver:
driver.quit()

简单版本就酱,iframe还需要进一步训练,存在以下问题:
- 当存在两个缺口时,可能会识别到第一个缺口
iframe允许的像素误差太小了!
更多推荐




所有评论(0)