

1、打开F12查看源码,有距离像素值。拖动并查看属性值的变化。

2、通过模拟人工随机滑动每次1~2个像素,每次滑动完分别puzzle和slider的像素距离值,存在两组列表里面。
#拖动和实时获取像素点数据
def drag(browser, drag_ele: WebElement, distance: int):
"""
拖动滑块,同时每步通过 xpath 获取滑块和滑轨 style 数据。
:param browser: 已打开的网页对象
:param distance: 拖动距离
:return: (slider_style_list, track_style_list)
"""
slider_list = []
puzzle_list = []
x, y, width, height = drag_ele.get_bounding()
pos_center_x = x + width / 2
pos_center_y = y + height / 2
win32.manual_motion_on(
motion_move=True, motion_click=True, motion_delay=True,
min_time=0.01, max_time=0.1
)
# 移动到元素中心并按下
win32.mouse_move(point_x=pos_center_x, point_y=pos_center_y, move_speed="middle")
win32.mouse_click(button="left", click_type="down", hardware_driver_click=True)
# ---- 拖到目标 ----
current_x = pos_center_x
target_x = pos_center_x + glv['width']
total_distance = glv['width']
while current_x < target_x:
progress = (current_x - pos_center_x) / total_distance
s_factor = math.sin(progress * math.pi / 2)
step = random.choice([1, 2])
current_x += step
if current_x > target_x:
current_x = target_x
jitter_y = pos_center_y + random.uniform(-0.5, 0.5)
print(f'拖动{step} 到 current_x : {current_x}, target_x : {target_x}, ')
win32.mouse_move(point_x=current_x, point_y=jitter_y, move_speed="fast", delay_after=0.01)
time.sleep(random.uniform(0.001, 0.01))
puzzle_list, slider_list = get_px(browser, puzzle_list, slider_list)
puzzle_list = [float(item.split(': ')[1].replace('px;', '')) for item in puzzle_list]
slider_list = [float(item.split(': ')[1].replace('px;', '')) for item in slider_list]
predict_x, deg, formula, coefs = fit_data.fit_from_y(
puzzle_list, x_data=slider_list, max_degree=6, invert=True)
print("拟合公式(反函数):", formula)
distance = predict_x(distance)
win32.mouse_move(pos_center_x + distance, pos_center_y, move_speed="middle")
win32.mouse_click(button="left", click_type="up", hardware_driver_click=True)
win32.manual_motion_off()
return puzzle_list, slider_list3、计算缺口与左边起点距离的像素值。
import cv2
import numpy as np
import base64
import requests
def load_image(input_str: str):
"""
输入可以是图片 URL 或 base64 字符串
返回 OpenCV 图像对象 (numpy.ndarray)
"""
if input_str.startswith("http://") or input_str.startswith("https://"):
# 远程 URL
resp = requests.get(input_str, timeout=10)
img_data = resp.content
else:
# base64 格式
if input_str.startswith("data:image"):
input_str = input_str.split(",", 1)[1]
img_data = base64.b64decode(input_str)
np_arr = np.frombuffer(img_data, np.uint8)
img = cv2.imdecode(np_arr, cv2.IMREAD_COLOR)
return img
def find_gap(bg_input: str, block_input: str, debug_save: str = None):
"""
查找滑块缺口位置
:param bg_input: 背景图(URL 或 base64)
:param block_input: 缺口小块图(URL 或 base64)
:param debug_save: 调试用保存文件名(可选)
:return: (x, y, confidence)
"""
# 加载图像
bg_img = load_image(bg_input)
block_img = load_image(block_input)
# 模板匹配
result = cv2.matchTemplate(bg_img, block_img, cv2.TM_CCOEFF_NORMED)
_, max_val, _, max_loc = cv2.minMaxLoc(result)
x, y = max_loc
h, w = block_img.shape[:2]
if debug_save:
debug_img = bg_img.copy()
cv2.rectangle(debug_img, (x, y), (x + w, y + h), (0, 0, 255), 2)
cv2.imwrite(debug_save, debug_img)
return x, y, max_val4、find_gap返回的x就是puzzle缺口位置到左边起点的距离,我这里一般加1-2个像素点,因为计算出来的值就是靠左一点点的,实际需要偏移一下。

5、刚才取得的列表,需要拟合成一个函数,可能是多项式函数(找ai写即可),我一开始搞反了x和y,所以又添一个反函数的操作。
import numpy as np
def _dedupe_xy(x, y):
"""
去重:相同 x 的 y 取平均,避免重复点引起的拟合不稳定。
"""
x = np.asarray(x, dtype=float).ravel()
y = np.asarray(y, dtype=float).ravel()
uniq = {}
for xi, yi in zip(x, y):
if xi in uniq:
uniq[xi].append(yi)
else:
uniq[xi] = [yi]
x_new = []
y_new = []
for xi, ys in uniq.items():
x_new.append(xi)
y_new.append(np.mean(ys))
order = np.argsort(x_new)
x_clean = np.asarray(x_new)[order]
y_clean = np.asarray(y_new)[order]
return x_clean, y_clean
def _best_polyfit_by_aicc(x, y, max_degree=10):
"""
用 AICc 自动选择最佳多项式阶数,返回最佳系数与阶数。
"""
x = np.asarray(x, dtype=float).ravel()
y = np.asarray(y, dtype=float).ravel()
n = len(x)
best = None
best_deg = None
best_aicc = np.inf
max_deg = int(min(max_degree, max(1, n - 2))) # 防止过高阶数
for deg in range(1, max_deg + 1):
coefs = np.polyfit(x, y, deg) # 高次在前
y_hat = np.polyval(coefs, x)
rss = np.sum((y - y_hat) ** 2)
k = deg + 1 # 参数个数
# 避免 log(0)
rss_per = rss / n if rss > 0 else 1e-12
aic = n * np.log(rss_per) + 2 * k
aicc = aic + (2 * k * (k + 1)) / max(n - k - 1, 1)
if aicc < best_aicc:
best_aicc = aicc
best = coefs
best_deg = deg
return best, best_deg
def _poly_to_string(coefs, var='x'):
"""
把多项式系数转成可读公式字符串。
"""
terms = []
deg = len(coefs) - 1
for i, c in enumerate(coefs):
power = deg - i
if abs(c) < 1e-12:
continue
if power == 0:
terms.append(f"{c:.8g}")
elif power == 1:
terms.append(f"({c:.8g})*{var}")
else:
terms.append(f"({c:.8g})*{var}**{power}")
if not terms:
return "y = 0"
return "y = " + " + ".join(terms)
def fit_from_y(y_data, x_data=None, max_degree=10, start_index=1, step=1.0, invert=False):
"""
拟合一个可调用函数:
- 若仅传 y_data:默认 x = start_index, start_index+step, ...
- 若也传 x_data:用你给的 x_data。
- invert=False(默认):拟合 y = f(x),predict(x_new) -> y
- invert=True:拟合 x = g(y),predict(y_new) -> x(即“反函数”拟合)
返回:
predict (callable), best_degree (int), formula (str), coefs (np.ndarray)
"""
y = np.asarray(y_data, dtype=float).ravel()
if x_data is None:
# 只有 y:用等距索引作为 x
n = len(y)
x = start_index + step * np.arange(n, dtype=float)
else:
x = np.asarray(x_data, dtype=float).ravel()
if len(x) != len(y):
raise ValueError("x_data 与 y_data 长度不一致。")
# invert 模式:把自变量/因变量交换来拟合“反函数”
if invert:
xx, yy = _dedupe_xy(y, x) # 拟合 x = g(y)
coefs, best_deg = _best_polyfit_by_aicc(xx, yy, max_degree=max_degree)
def predict(y_new):
y_new = np.asarray(y_new, dtype=float)
return np.polyval(coefs, y_new)
formula = _poly_to_string(coefs, var='y')
return predict, best_deg, formula, coefs
# 正常模式:拟合 y = f(x)
xx, yy = _dedupe_xy(x, y)
coefs, best_deg = _best_polyfit_by_aicc(xx, yy, max_degree=max_degree)
def predict(x_new):
x_new = np.asarray(x_new, dtype=float)
return np.polyval(coefs, x_new)
formula = _poly_to_string(coefs, var='x')
return predict, best_deg, formula, coefs总结:根据OpenCV去识别缺口距离,再通过滑动并实时抓取puzzle和slider的实际距离像素数据,通过拟合函数计算出puzzle到位的时候,slider滑动的实际距离是多少,最后把滑块移到终点放手即可。