280 lines
8.9 KiB
Python
280 lines
8.9 KiB
Python
|
|
#!/usr/bin/env python3
|
|||
|
|
# -*- coding: utf-8 -*-
|
|||
|
|
"""
|
|||
|
|
根据背景图 + 拼图块图计算滑块目标偏移,并可选使用 DrissionPage 执行拖动。
|
|||
|
|
|
|||
|
|
默认读取当前项目根目录:
|
|||
|
|
- 下载 (1).png (背景图)
|
|||
|
|
- 下载.png (拼图块,RGBA 透明图)
|
|||
|
|
"""
|
|||
|
|
from __future__ import annotations
|
|||
|
|
|
|||
|
|
import argparse
|
|||
|
|
import random
|
|||
|
|
import time
|
|||
|
|
from pathlib import Path
|
|||
|
|
|
|||
|
|
import numpy as np
|
|||
|
|
from PIL import Image, ImageDraw
|
|||
|
|
|
|||
|
|
|
|||
|
|
def _load_images(bg_path: Path, piece_path: Path) -> tuple[np.ndarray, np.ndarray]:
|
|||
|
|
bg = np.array(Image.open(bg_path).convert("RGB"), dtype=np.int16)
|
|||
|
|
piece_rgba = np.array(Image.open(piece_path).convert("RGBA"), dtype=np.int16)
|
|||
|
|
return bg, piece_rgba
|
|||
|
|
|
|||
|
|
|
|||
|
|
def _piece_bbox(alpha: np.ndarray, threshold: int = 12) -> tuple[int, int, int, int, np.ndarray]:
|
|||
|
|
mask = alpha > threshold
|
|||
|
|
if not mask.any():
|
|||
|
|
raise ValueError("拼图块图片 alpha 全透明,无法匹配")
|
|||
|
|
|
|||
|
|
ys, xs = np.where(mask)
|
|||
|
|
y0, y1 = int(ys.min()), int(ys.max()) + 1
|
|||
|
|
x0, x1 = int(xs.min()), int(xs.max()) + 1
|
|||
|
|
return x0, y0, x1, y1, mask
|
|||
|
|
|
|||
|
|
|
|||
|
|
def calc_drag_distance(
|
|||
|
|
bg_path: Path,
|
|||
|
|
piece_path: Path,
|
|||
|
|
alpha_threshold: int = 12,
|
|||
|
|
) -> dict:
|
|||
|
|
"""返回最佳匹配位置与建议拖动距离。"""
|
|||
|
|
bg, piece_rgba = _load_images(bg_path, piece_path)
|
|||
|
|
|
|||
|
|
bh, bw = bg.shape[:2]
|
|||
|
|
ph, pw = piece_rgba.shape[:2]
|
|||
|
|
if ph != bh:
|
|||
|
|
raise ValueError(f"图片高度不一致:背景={bh}, 拼图块={ph}")
|
|||
|
|
|
|||
|
|
alpha = piece_rgba[:, :, 3]
|
|||
|
|
x0, y0, x1, y1, _ = _piece_bbox(alpha, threshold=alpha_threshold)
|
|||
|
|
|
|||
|
|
piece_crop = piece_rgba[y0:y1, x0:x1, :3]
|
|||
|
|
mask = alpha[y0:y1, x0:x1] > alpha_threshold
|
|||
|
|
ys, xs = np.where(mask)
|
|||
|
|
piece_pixels = piece_crop[ys, xs]
|
|||
|
|
|
|||
|
|
patch_h, patch_w = piece_crop.shape[:2]
|
|||
|
|
if patch_w > bw or patch_h > bh:
|
|||
|
|
raise ValueError("拼图块裁剪后尺寸超过背景图")
|
|||
|
|
|
|||
|
|
max_x = bw - patch_w
|
|||
|
|
best_x = 0
|
|||
|
|
best_score = float("inf")
|
|||
|
|
second_best = float("inf")
|
|||
|
|
|
|||
|
|
# 固定 y,按 x 扫描。评分越小越好。
|
|||
|
|
for x in range(max_x + 1):
|
|||
|
|
patch_pixels = bg[y0 + ys, x + xs]
|
|||
|
|
score = float(np.abs(patch_pixels - piece_pixels).mean())
|
|||
|
|
if score < best_score:
|
|||
|
|
second_best = best_score
|
|||
|
|
best_score = score
|
|||
|
|
best_x = x
|
|||
|
|
elif score < second_best:
|
|||
|
|
second_best = score
|
|||
|
|
|
|||
|
|
# 如果拼图初始从最左侧出发,建议拖动距离可用:best_x - x0
|
|||
|
|
drag_distance = best_x - x0
|
|||
|
|
confidence_ratio = (second_best / best_score) if best_score > 0 else float("inf")
|
|||
|
|
|
|||
|
|
return {
|
|||
|
|
"target_x": int(best_x),
|
|||
|
|
"piece_bbox_x0": int(x0),
|
|||
|
|
"piece_bbox_y0": int(y0),
|
|||
|
|
"piece_bbox_w": int(patch_w),
|
|||
|
|
"piece_bbox_h": int(patch_h),
|
|||
|
|
"drag_distance": int(drag_distance),
|
|||
|
|
"best_score": best_score,
|
|||
|
|
"second_best": second_best,
|
|||
|
|
"confidence_ratio": confidence_ratio,
|
|||
|
|
"bg_width": int(bw),
|
|||
|
|
"bg_height": int(bh),
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
|
|||
|
|
def save_debug_overlay(bg_path: Path, out_path: Path, match: dict) -> None:
|
|||
|
|
img = Image.open(bg_path).convert("RGB")
|
|||
|
|
draw = ImageDraw.Draw(img)
|
|||
|
|
x = int(match["target_x"])
|
|||
|
|
y = int(match["piece_bbox_y0"])
|
|||
|
|
w = int(match["piece_bbox_w"])
|
|||
|
|
h = int(match["piece_bbox_h"])
|
|||
|
|
draw.rectangle([x, y, x + w, y + h], outline=(255, 0, 0), width=2)
|
|||
|
|
img.save(out_path)
|
|||
|
|
|
|||
|
|
|
|||
|
|
def build_human_track(distance: int) -> list[int]:
|
|||
|
|
"""生成拟人拖动轨迹(整数步长列表)。"""
|
|||
|
|
if distance == 0:
|
|||
|
|
return []
|
|||
|
|
|
|||
|
|
direction = 1 if distance > 0 else -1
|
|||
|
|
remaining = abs(int(round(distance)))
|
|||
|
|
|
|||
|
|
track: list[int] = []
|
|||
|
|
moved = 0
|
|||
|
|
velocity = 0.0
|
|||
|
|
interval = 0.02
|
|||
|
|
accelerate_until = remaining * 0.7
|
|||
|
|
|
|||
|
|
while moved < remaining:
|
|||
|
|
if moved < accelerate_until:
|
|||
|
|
acc = random.uniform(2.0, 4.0)
|
|||
|
|
else:
|
|||
|
|
acc = -random.uniform(3.0, 6.0)
|
|||
|
|
|
|||
|
|
step = max(1, int(velocity * interval + 0.5 * acc * interval * interval * 100))
|
|||
|
|
moved += step
|
|||
|
|
velocity = max(0.0, velocity + acc * interval)
|
|||
|
|
track.append(step)
|
|||
|
|
|
|||
|
|
overflow = moved - remaining
|
|||
|
|
if overflow > 0 and track:
|
|||
|
|
track[-1] -= overflow
|
|||
|
|
if track[-1] <= 0:
|
|||
|
|
track.pop()
|
|||
|
|
|
|||
|
|
# 小幅回拉,增加拟人性
|
|||
|
|
if remaining >= 30:
|
|||
|
|
back = random.randint(1, 3)
|
|||
|
|
track.extend([-back, back])
|
|||
|
|
|
|||
|
|
return [direction * s for s in track if s != 0]
|
|||
|
|
|
|||
|
|
|
|||
|
|
def drag_slider_with_dp(page, slider_selector: str, distance: int) -> None:
|
|||
|
|
slider = page.ele(slider_selector, timeout=8)
|
|||
|
|
if not slider:
|
|||
|
|
raise RuntimeError(f"未找到滑块元素:{slider_selector}")
|
|||
|
|
|
|||
|
|
# 若元素支持直接 drag,优先用内置实现
|
|||
|
|
if hasattr(slider, "drag"):
|
|||
|
|
try:
|
|||
|
|
slider.drag(offset_x=distance, offset_y=0, duration=0.8)
|
|||
|
|
return
|
|||
|
|
except Exception:
|
|||
|
|
pass
|
|||
|
|
|
|||
|
|
actions = page.actions
|
|||
|
|
actions.move_to(slider, duration=0.2)
|
|||
|
|
|
|||
|
|
hold_ok = False
|
|||
|
|
for hold_call in (
|
|||
|
|
lambda: actions.hold(slider),
|
|||
|
|
lambda: actions.hold(),
|
|||
|
|
):
|
|||
|
|
try:
|
|||
|
|
hold_call()
|
|||
|
|
hold_ok = True
|
|||
|
|
break
|
|||
|
|
except Exception:
|
|||
|
|
continue
|
|||
|
|
|
|||
|
|
if not hold_ok:
|
|||
|
|
raise RuntimeError("无法按下滑块,请确认 DrissionPage 版本与页面状态")
|
|||
|
|
|
|||
|
|
for step in build_human_track(distance):
|
|||
|
|
dy = random.randint(-1, 1)
|
|||
|
|
moved = False
|
|||
|
|
for move_call in (
|
|||
|
|
lambda: actions.move(step, dy, duration=random.uniform(0.01, 0.04)),
|
|||
|
|
lambda: actions.move(offset_x=step, offset_y=dy, duration=random.uniform(0.01, 0.04)),
|
|||
|
|
lambda: actions.move(step, dy),
|
|||
|
|
lambda: actions.move(offset_x=step, offset_y=dy),
|
|||
|
|
):
|
|||
|
|
try:
|
|||
|
|
move_call()
|
|||
|
|
moved = True
|
|||
|
|
break
|
|||
|
|
except Exception:
|
|||
|
|
continue
|
|||
|
|
if not moved:
|
|||
|
|
raise RuntimeError("动作链 move 调用失败,请按当前 DrissionPage 版本调整 move 参数")
|
|||
|
|
|
|||
|
|
released = False
|
|||
|
|
for release_call in (
|
|||
|
|
lambda: actions.release(),
|
|||
|
|
lambda: actions.release(on_ele=None),
|
|||
|
|
):
|
|||
|
|
try:
|
|||
|
|
release_call()
|
|||
|
|
released = True
|
|||
|
|
break
|
|||
|
|
except Exception:
|
|||
|
|
continue
|
|||
|
|
|
|||
|
|
if not released:
|
|||
|
|
raise RuntimeError("动作链 release 调用失败")
|
|||
|
|
|
|||
|
|
|
|||
|
|
def maybe_run_dp_drag(args: argparse.Namespace, distance: int) -> None:
|
|||
|
|
if not args.url or not args.slider:
|
|||
|
|
return
|
|||
|
|
|
|||
|
|
from DrissionPage import ChromiumOptions, ChromiumPage
|
|||
|
|
|
|||
|
|
if args.port:
|
|||
|
|
co = ChromiumOptions().set_local_port(port=args.port)
|
|||
|
|
page = ChromiumPage(addr_or_opts=co)
|
|||
|
|
else:
|
|||
|
|
page = ChromiumPage()
|
|||
|
|
|
|||
|
|
page.get(args.url)
|
|||
|
|
if args.wait_after_open > 0:
|
|||
|
|
time.sleep(args.wait_after_open)
|
|||
|
|
|
|||
|
|
drag_slider_with_dp(page, args.slider, distance)
|
|||
|
|
|
|||
|
|
|
|||
|
|
def parse_args() -> argparse.Namespace:
|
|||
|
|
root = Path(__file__).resolve().parent
|
|||
|
|
|
|||
|
|
parser = argparse.ArgumentParser(description="DP 滑块验证辅助脚本")
|
|||
|
|
parser.add_argument("--bg", type=Path, default=root / "下载 (1).png", help="背景图路径")
|
|||
|
|
parser.add_argument("--piece", type=Path, default=root / "下载.png", help="拼图块图路径")
|
|||
|
|
parser.add_argument("--alpha-threshold", type=int, default=12, help="透明阈值(0-255)")
|
|||
|
|
parser.add_argument("--debug-out", type=Path, default=root / "slider_match_debug.png", help="匹配结果标注图")
|
|||
|
|
|
|||
|
|
# 以下参数用于真实页面拖动(可选)
|
|||
|
|
parser.add_argument("--url", type=str, default="", help="验证码页面 URL")
|
|||
|
|
parser.add_argument("--slider", type=str, default="", help="滑块元素选择器")
|
|||
|
|
parser.add_argument("--port", type=int, default=0, help="连接已有浏览器的本地端口")
|
|||
|
|
parser.add_argument("--distance-adjust", type=int, default=0, help="对计算结果追加修正像素")
|
|||
|
|
parser.add_argument("--wait-after-open", type=float, default=1.0, help="打开页面后等待秒数")
|
|||
|
|
|
|||
|
|
return parser.parse_args()
|
|||
|
|
|
|||
|
|
|
|||
|
|
def main() -> None:
|
|||
|
|
args = parse_args()
|
|||
|
|
|
|||
|
|
if not args.bg.exists():
|
|||
|
|
raise FileNotFoundError(f"背景图不存在:{args.bg}")
|
|||
|
|
if not args.piece.exists():
|
|||
|
|
raise FileNotFoundError(f"拼图块图不存在:{args.piece}")
|
|||
|
|
|
|||
|
|
match = calc_drag_distance(args.bg, args.piece, alpha_threshold=args.alpha_threshold)
|
|||
|
|
save_debug_overlay(args.bg, args.debug_out, match)
|
|||
|
|
|
|||
|
|
distance = int(match["drag_distance"]) + int(args.distance_adjust)
|
|||
|
|
|
|||
|
|
print("匹配结果:")
|
|||
|
|
print(f" target_x={match['target_x']}")
|
|||
|
|
print(f" piece_bbox_x0={match['piece_bbox_x0']}")
|
|||
|
|
print(f" 建议拖动距离 drag_distance={match['drag_distance']}")
|
|||
|
|
print(f" 调整后拖动距离 distance={distance}")
|
|||
|
|
print(f" best_score={match['best_score']:.4f}, second_best={match['second_best']:.4f}")
|
|||
|
|
print(f" confidence_ratio={match['confidence_ratio']:.4f} (越大越好)")
|
|||
|
|
print(f" 标注图已输出:{args.debug_out}")
|
|||
|
|
|
|||
|
|
if args.url and args.slider:
|
|||
|
|
maybe_run_dp_drag(args, distance)
|
|||
|
|
print("DP 拖动已执行。")
|
|||
|
|
|
|||
|
|
|
|||
|
|
if __name__ == "__main__":
|
|||
|
|
main()
|