SakuraFrp 自动签到(main + sakura_frp_auto_check + SakuraFrpQiandao),已排除 .env 与日志

This commit is contained in:
RRRosmontis
2026-08-27 21:43:38 +08:00
commit 2a9f96fb18
21 changed files with 2209 additions and 0 deletions
+346
View File
@@ -0,0 +1,346 @@
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)