|
@@ -0,0 +1,152 @@
|
|
|
|
|
+"""易盾滑块验证码:云码识别 + 模拟拖拽。"""
|
|
|
|
|
+import base64
|
|
|
|
|
+import math
|
|
|
|
|
+import random
|
|
|
|
|
+import time
|
|
|
|
|
+
|
|
|
|
|
+import requests
|
|
|
|
|
+
|
|
|
|
|
+from commons.Logger import logger
|
|
|
|
|
+
|
|
|
|
|
+CAPTCHA_TOKEN = "zPzmt1mG1ouCU6GTzsZN2Lmm8pdZypapPcLJTBRETco"
|
|
|
|
|
+CAPTCHA_API_URL = "http://api.jfbym.com/api/YmServer/customApi"
|
|
|
|
|
+
|
|
|
|
|
+SLIDER_OFFSET_FIX = 10
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+def call_captcha_api(image_bytes):
|
|
|
|
|
+ """调用云码平台识别滑块距离,失败返回 None。"""
|
|
|
|
|
+ try:
|
|
|
|
|
+ b64 = base64.b64encode(image_bytes).decode()
|
|
|
|
|
+ resp = requests.post(
|
|
|
|
|
+ CAPTCHA_API_URL,
|
|
|
|
|
+ json={"token": CAPTCHA_TOKEN, "type": "22222", "image": b64},
|
|
|
|
|
+ headers={"Content-Type": "application/json"},
|
|
|
|
|
+ timeout=15,
|
|
|
|
|
+ ).json()
|
|
|
|
|
+ logger.info("验证码 API 返回: %s", resp)
|
|
|
|
|
+ if not isinstance(resp, dict):
|
|
|
|
|
+ return None
|
|
|
|
|
+ data = resp.get("data")
|
|
|
|
|
+ if isinstance(data, dict):
|
|
|
|
|
+ dist = data.get("data")
|
|
|
|
|
+ else:
|
|
|
|
|
+ dist = data
|
|
|
|
|
+ if dist is None:
|
|
|
|
|
+ logger.error("验证码 API 未返回距离字段: %s", resp)
|
|
|
|
|
+ return None
|
|
|
|
|
+ try:
|
|
|
|
|
+ d = float(dist)
|
|
|
|
|
+ except (TypeError, ValueError):
|
|
|
|
|
+ logger.error("验证码距离无法解析为数字: %r", dist)
|
|
|
|
|
+ return None
|
|
|
|
|
+ if not math.isfinite(d):
|
|
|
|
|
+ logger.error("验证码距离非有限数值: %r", dist)
|
|
|
|
|
+ return None
|
|
|
|
|
+ return d
|
|
|
|
|
+ except Exception as e:
|
|
|
|
|
+ logger.exception("验证码 API 调用失败: %s", e)
|
|
|
|
|
+ return None
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+def generate_human_track(distance):
|
|
|
|
|
+ try:
|
|
|
|
|
+ distance = float(distance)
|
|
|
|
|
+ except (TypeError, ValueError):
|
|
|
|
|
+ return []
|
|
|
|
|
+ if distance <= 0 or not math.isfinite(distance):
|
|
|
|
|
+ return []
|
|
|
|
|
+ tracks = []
|
|
|
|
|
+ current = 0
|
|
|
|
|
+ mid = distance * 0.7
|
|
|
|
|
+ t = 0.2
|
|
|
|
|
+ v = 0
|
|
|
|
|
+ move_points = []
|
|
|
|
|
+
|
|
|
|
|
+ while current < mid:
|
|
|
|
|
+ a = random.uniform(2, 4)
|
|
|
|
|
+ v0 = v
|
|
|
|
|
+ v = v0 + a * t
|
|
|
|
|
+ move = v0 * t + 0.5 * a * t * t
|
|
|
|
|
+ current += move
|
|
|
|
|
+ move_points.append(move)
|
|
|
|
|
+
|
|
|
|
|
+ while current < distance:
|
|
|
|
|
+ a = -random.uniform(0.5, 1.5)
|
|
|
|
|
+ v0 = v
|
|
|
|
|
+ v = v0 + a * t
|
|
|
|
|
+ if v < 0.5:
|
|
|
|
|
+ v = 0.5
|
|
|
|
|
+ move = v0 * t + 0.5 * a * t * t
|
|
|
|
|
+ current += move
|
|
|
|
|
+ move_points.append(move)
|
|
|
|
|
+
|
|
|
|
|
+ total_points = len(move_points)
|
|
|
|
|
+ for i, move in enumerate(move_points):
|
|
|
|
|
+ y_offset = random.randint(-2, 2) if i % random.randint(2, 4) == 0 else 0
|
|
|
|
|
+
|
|
|
|
|
+ if i < total_points * 0.3:
|
|
|
|
|
+ duration = random.uniform(0.01, 0.03)
|
|
|
|
|
+ elif i > total_points * 0.7:
|
|
|
|
|
+ duration = random.uniform(0.03, 0.08)
|
|
|
|
|
+ else:
|
|
|
|
|
+ duration = random.uniform(0.02, 0.05)
|
|
|
|
|
+
|
|
|
|
|
+ if random.random() < 0.05:
|
|
|
|
|
+ duration += random.uniform(0.05, 0.1)
|
|
|
|
|
+
|
|
|
|
|
+ tracks.append((move, y_offset, duration))
|
|
|
|
|
+
|
|
|
|
|
+ if random.random() < 0.7:
|
|
|
|
|
+ tracks.append((-random.randint(1, 3), 0, 0.05))
|
|
|
|
|
+
|
|
|
|
|
+ return tracks
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+def simulate_slider_drag(driver, slider_element, target_distance):
|
|
|
|
|
+ if target_distance <= 0:
|
|
|
|
|
+ logger.warning("滑块目标距离无效: %s", target_distance)
|
|
|
|
|
+ return
|
|
|
|
|
+ driver.actions.move_to(slider_element).hold()
|
|
|
|
|
+ for offset_x, offset_y, duration in generate_human_track(target_distance):
|
|
|
|
|
+ driver.actions.move(offset_x, offset_y, duration=duration / 1000)
|
|
|
|
|
+ driver.actions.release()
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+def solve_slider_captcha(driver):
|
|
|
|
|
+ """检测并处理易盾滑块验证码,成功返回 True。"""
|
|
|
|
|
+ driver.wait.doc_loaded()
|
|
|
|
|
+ time.sleep(2)
|
|
|
|
|
+
|
|
|
|
|
+ yidun = driver.ele("xpath://div[@class='yidun_modal']", timeout=3)
|
|
|
|
|
+ if not yidun:
|
|
|
|
|
+ return True
|
|
|
|
|
+
|
|
|
|
|
+ logger.info("检测到滑块验证码,开始处理")
|
|
|
|
|
+ jpg_bytes = yidun.get_screenshot(as_bytes="jpg")
|
|
|
|
|
+
|
|
|
|
|
+ distance = call_captcha_api(jpg_bytes)
|
|
|
|
|
+ if distance is None:
|
|
|
|
|
+ logger.error("验证码识别失败")
|
|
|
|
|
+ return False
|
|
|
|
|
+
|
|
|
|
|
+ logger.info("滑块距离: %s", distance)
|
|
|
|
|
+ slider = driver.ele(
|
|
|
|
|
+ "xpath://div[contains(@class,'yidun_slider--hover')]", timeout=5
|
|
|
|
|
+ )
|
|
|
|
|
+ if not slider:
|
|
|
|
|
+ logger.error("未找到滑块元素")
|
|
|
|
|
+ return False
|
|
|
|
|
+
|
|
|
|
|
+ try:
|
|
|
|
|
+ drag_distance = float(distance) + SLIDER_OFFSET_FIX
|
|
|
|
|
+ except (TypeError, ValueError):
|
|
|
|
|
+ logger.error("滑块距离非数字: %r", distance)
|
|
|
|
|
+ return False
|
|
|
|
|
+
|
|
|
|
|
+ if not math.isfinite(drag_distance) or drag_distance <= 0:
|
|
|
|
|
+ logger.error("滑块距离无效: %s", drag_distance)
|
|
|
|
|
+ return False
|
|
|
|
|
+ simulate_slider_drag(driver, slider, drag_distance-6)
|
|
|
|
|
+ time.sleep(3)
|
|
|
|
|
+ return True
|