Files
natfrp-auto-check-in/sakura_frp_auto_check/main.py
T

347 lines
13 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
from playwright.sync_api import sync_playwright
from pathlib import Path
import random
import time
import cv2
def get_gap_offset(bg_path, fullbg_path, debug=False):
"""比较有缺口背景图和完整背景图,找到缺口 x 坐标(取前两个最大轮廓,选x更大者)"""
# 1. 灰度读取
bg_img = cv2.imread(bg_path, 0)
fullbg_img = cv2.imread(fullbg_path, 0)
if debug:
cv2.imwrite("debug_01_bg_gray.png", bg_img)
cv2.imwrite("debug_02_fullbg_gray.png", fullbg_img)
# 2. 计算绝对差
diff = cv2.absdiff(fullbg_img, bg_img)
if debug:
cv2.imwrite("debug_03_diff.png", diff)
# 3. 二值化
_, thresh = cv2.threshold(diff, 25, 255, cv2.THRESH_BINARY)
if debug:
cv2.imwrite("debug_04_thresh.png", thresh)
# 4. 找轮廓
contours, _ = cv2.findContours(thresh, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
if len(contours) < 2:
raise ValueError("[ERROR] 找到的轮廓少于两个,可能识别失败")
# 5. 按面积排序,取前两个最大的
contours_sorted = sorted(contours, key=cv2.contourArea, reverse=True)[:2]
# 6. 获取它们的 bounding box
rects = [cv2.boundingRect(c) for c in contours_sorted]
# 7. 选择 x 坐标更大的那个
target_rect = max(rects, key=lambda r: r[0])
x, y, w, h = target_rect
# 8. 调试输出
if debug:
debug_img = cv2.cvtColor(bg_img, cv2.COLOR_GRAY2BGR)
for rect in rects:
cx, cy, cw, ch = rect
cv2.rectangle(debug_img, (cx, cy), (cx+cw, cy+ch), (0,255,0), 2)
cv2.rectangle(debug_img, (x, y), (x+w, y+h), (0,0,255), 2)
cv2.imwrite("debug_05_with_box.png", debug_img)
return x
# ========= 配置 =========
domain = "www.natfrp.com"
target_url = f"https://{domain}/user/"
ACCOUNT_FILE = Path("account.txt") # 第一行用户名,第二行密码
STATE_FILE = "state.json"
MAX_RETRY = 5 # 0 表示无限重试,>0 表示最大重试次数
# --- 新增/修改 ---
ALREADY_SIGNED_TEXT = "今天已经签到过啦" # 用于判定成功的文案(包含即可)
SIGNED_ANCESTOR_LEVELS = 3 # 向上取第 3 层父级
SIGNED_ANCESTOR_SCREENSHOT = "checkin.png" # 成功时保存截图的文件名
# ========================
# ---------------- 读取账号密码 ----------------
def load_username_password(path: Path):
if not path.exists():
raise FileNotFoundError(f"{path} 文件不存在")
lines = [l.strip() for l in path.read_text(encoding="utf-8").strip().splitlines() if l.strip()]
if len(lines) < 2:
raise ValueError("account.txt 必须至少有两行:用户名、密码")
return lines[0], lines[1]
# ---------------- 检测函数 ----------------
def is_cookie_expired(page, timeout=6000):
try:
# 页面出现“Nyatwork 登录”说明需要重新登录
page.wait_for_selector("text=Nyatwork 登录", timeout=timeout)
return True
except:
return False
def confirm_age_if_needed(page, timeout=6000):
try:
page.wait_for_selector('text=是,我已满18岁', timeout=timeout)
page.click('text=是,我已满18岁')
print("[INFO] 已点击满18岁确认按钮")
except:
print("[INFO] 无需进行年龄确认")
def is_logged_in(page, timeout=6000):
try:
page.wait_for_selector("text=账号信息", timeout=timeout)
print("[INFO] 检测到已登录状态")
return True
except:
return False
def is_sign_button_visible(page, timeout=6000):
try:
page.wait_for_selector("text=点击这里签到", timeout=timeout)
return True
except:
return False
def is_captcha_visible(page, timeout=6000):
try:
page.wait_for_selector(".geetest_slider_button", timeout=timeout)
return True
except:
return False
def wait_captcha_disappear(page, timeout=10000):
try:
page.wait_for_selector("text=签到成功", timeout=timeout)
return True
except:
return False
# --- 新增/修改:查找“今天已经签到过啦”的元素(包含匹配),并取其第 N 层父级 ---
def find_signed_text_locator(page, timeout=6000):
"""
返回包含“今天已经签到过啦”的元素 locator(取第一个可见)。
使用 normalize-space 规避前后空格影响,并做“包含”匹配。
"""
selector = "text=%s" % ALREADY_SIGNED_TEXT
try:
page.wait_for_selector(selector, timeout=timeout)
return page.locator(selector).first
except Exception as e:
return None
def screenshot_signed_text_ancestor(page, levels: int = SIGNED_ANCESTOR_LEVELS,
path: str = SIGNED_ANCESTOR_SCREENSHOT,
timeout: int = 6000) -> bool:
"""
截图“包含『今天已经签到过啦』的元素”的第 levels 层父级。
若未找到该元素或父级,兜底整页截图。返回 True 表示截到了祖先元素,False 表示做了兜底。
"""
base = find_signed_text_locator(page, timeout=timeout)
if base is None:
# 没找到文本元素,整页兜底
try:
page.screenshot(path=path, full_page=True)
print(f"[WARN] 未找到包含『{ALREADY_SIGNED_TEXT}』的元素,已兜底整页截图:{path}")
except Exception as e:
print(f"[ERROR] 兜底整页截图失败:{e}")
return False
return False
try:
# 取第 N 层父级
ancestor = base.locator(f"xpath=ancestor::*[{levels}]").first
# 有些页面结构不够深,尝试逐级回退
for lvl in range(levels, 0, -1):
candidate = base.locator(f"xpath=ancestor::*[{lvl}]").first
try:
candidate.wait_for(state="visible", timeout=1200)
try:
candidate.scroll_into_view_if_needed()
except:
pass
candidate.screenshot(path=path)
print(f"[INFO] 已保存签到截图:{path}")
return True
except:
continue
# 如果所有层级都没成功,整页兜底
page.screenshot(path=path, full_page=True)
print(f"[WARN] 祖先节点截图未成功,已兜底整页截图:{path}")
return False
except Exception as e:
print(f"[ERROR] 祖先截图异常:{e}")
try:
page.screenshot(path=path, full_page=True)
print(f"[WARN] 异常时已兜底整页截图:{path}")
except:
return False
return False
# ---------------- 操作函数 ----------------
def login_with_user_pass(page, username, password):
page.fill("#username", username)
page.fill("#password", password)
page.click("button[id=login]")
print("[INFO] 已提交登录表单")
def click_sign_in_button(page):
page.click("text=点击这里签到")
print("[INFO] 已点击签到按钮")
def drag_slider_fixed_steps(page, slider_element, distance, button_type="middle", debug_pause=False):
box = slider_element.bounding_box()
start_x = int(box['x'])
start_y = int(box['y'] + box['height'] / 2)
print(f"[INFO] 滑块初始坐标: ({start_x}, {start_y})")
print(f"[INFO] 目标滑动距离: {distance} 像素")
page.mouse.move(start_x, start_y)
page.mouse.down(button=button_type)
page.wait_for_timeout(300)
page.mouse.move(start_x + distance + random.randint(2, 8), start_y)
page.wait_for_timeout(500)
page.mouse.move(start_x + distance - 2, start_y)
page.mouse.move(start_x + distance - 6, start_y)
page.wait_for_timeout(400)
page.mouse.move(start_x + distance - 8, start_y)
page.mouse.up(button=button_type)
print("[INFO] 滑动完成")
if debug_pause:
page.pause()
def solve_geetest_puzzle(page):
bg_path = "bg.png"
fullbg_path = "fullbg.png"
page.query_selector('.geetest_canvas_bg').screenshot(path=bg_path)
page.evaluate("""document.querySelector('.geetest_canvas_fullbg').style.display = 'block';""")
page.query_selector('.geetest_canvas_fullbg').screenshot(path=fullbg_path)
page.evaluate("""document.querySelector('.geetest_canvas_fullbg').style.display = 'none';""")
gap_x = get_gap_offset(bg_path, fullbg_path, debug=True)
print(f"[INFO] 缺口坐标: {gap_x}")
slider = page.query_selector('.geetest_slider_button')
distance = gap_x
drag_slider_fixed_steps(page, slider, distance, button_type="left", debug_pause=False)
print("[INFO] 验证码滑动完成")
# --- 新增/修改:以“今天已经签到过啦”判定成功 ---
def wait_signed_text_and_shoot(page, timeout=8000) -> bool:
"""
等待出现包含『今天已经签到过啦』的元素;出现则截图其第 3 层父级并返回 True。
若未出现则返回 False。
"""
loc = find_signed_text_locator(page, timeout=timeout)
if loc is None:
return False
# 找到了就截图祖先
screenshot_signed_text_ancestor(page, SIGNED_ANCESTOR_LEVELS, SIGNED_ANCESTOR_SCREENSHOT)
return True
# ---------------- 主逻辑 ----------------
def main() -> bool:
"""
主流程
:return: True 表示成功,False 表示失败需要重试
"""
username, password = load_username_password(ACCOUNT_FILE)
with sync_playwright() as p:
browser = p.chromium.launch(headless=True, slow_mo=200)
# 先尝试从 state.json 加载状态(cookie/session)
try:
context = browser.new_context(storage_state=STATE_FILE)
print("[INFO] 已加载保存的状态文件")
except:
print("[WARN] 未找到状态文件,将新建上下文")
context = browser.new_context()
page = context.new_page()
page.goto(target_url, timeout=8000)
# 登录后再检查 18 岁确认(若页面已处于登录态也会直接检测)
if is_logged_in(page):
confirm_age_if_needed(page)
else:
# 检查状态文件里的 cookie 是否失效
if is_cookie_expired(page):
print("[WARN] Cookie 已过期,使用账号密码登录")
login_with_user_pass(page, username, password)
if is_logged_in(page):
print("[INFO] 登录成功,更新状态文件")
context.storage_state(path=STATE_FILE)
# 登录成功后再检查一次 18 岁确认
confirm_age_if_needed(page)
else:
print("[ERROR] 登录失败")
browser.close()
return False
# --- 新增/修改:页面若已出现“今天已经签到过啦”,直接判定成功并截图祖先 ---
if wait_signed_text_and_shoot(page, timeout=2000):
print("[INFO] 已经签到过啦(页面已有提示)")
browser.close()
return True
# 正常签到流程
if is_sign_button_visible(page):
click_sign_in_button(page)
if is_captcha_visible(page):
solve_geetest_puzzle(page)
# 成功判定:等待出现“今天已经签到过啦”
if wait_signed_text_and_shoot(page, timeout=10000):
print("[INFO] 签到成功(出现『今天已经签到过啦』)")
browser.close()
return True
else:
print("[ERROR] 签到失败:未出现『今天已经签到过啦』提示")
browser.close()
return False
else:
# 无验证码路径:同样等待出现成功提示
if wait_signed_text_and_shoot(page, timeout=8000):
print("[INFO] 签到成功(无需验证码)")
browser.close()
return True
else:
print("[ERROR] 签到失败:未出现『今天已经签到过啦』提示(无需验证码路径)")
browser.close()
return False
else:
# 不再以“找不到按钮”作为成功判据;此分支仅作为提示与失败返回
print("[ERROR] 未发现签到按钮,且未检测到『今天已经签到过啦』提示")
browser.close()
return False
# ---------------- 带重试启动 ----------------
if __name__ == "__main__":
attempt = 0
while True:
attempt += 1
print(f"[INFO] 第 {attempt} 次执行签到")
try:
success = main()
except Exception as e:
print(f"[ERROR] 程序异常: {e}")
success = False
if success:
print("[INFO] 本次执行成功,退出程序")
break
else:
if MAX_RETRY > 0 and attempt >= MAX_RETRY:
print("[ERROR] 超过最大重试次数,退出程序")
break
print("[WARN] 本次失败,10秒后重试")
time.sleep(10)