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
+10
View File
@@ -0,0 +1,10 @@
/bg.png
/cookies.txt
/debug_01_bg_gray.png
/debug_02_fullbg_gray.png
/debug_03_diff.png
/debug_04_thresh.png
/debug_05_with_box.png
/fullbg.png
/state.json
/account.txt
+77
View File
@@ -0,0 +1,77 @@
# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## Project Overview
This is an automated FRP (NATFRP) check-in bot built with Python and Playwright. The application automates daily sign-in on www.natfrp.com with CAPTCHA solving capabilities.
## Architecture
### Core Components
- **main.py**: Single-file application containing the entire automation logic
- **Computer Vision Module** (`get_gap_offset`): Uses OpenCV to solve sliding CAPTCHA puzzles by detecting gaps in background images
- **Browser Automation**: Uses Playwright to interact with the NATFRP website
- **State Management**: Preserves login sessions using `state.json` to avoid repeated logins
- **Retry Logic**: Built-in retry mechanism with configurable maximum attempts
### Key Functions
- `get_gap_offset()`: CAPTCHA solver using image difference detection in main.py:7-51
- `main()`: Main automation workflow in main.py:251-324
- `solve_geetest_puzzle()`: Handles GeeTest CAPTCHA solving in main.py:220-235
- `wait_signed_text_and_shoot()`: Success detection and screenshot capture in main.py:238-248
## Running the Application
### Basic Execution
```bash
python main.py
```
### Dependencies
The project requires:
- playwright
- opencv-python (cv2)
- pathlib (built-in)
Install Playwright browsers after installing the package:
```bash
playwright install chromium
```
## Configuration Files
- **account.txt**: Contains username (line 1) and password (line 2) for NATFRP login
- **state.json**: Browser session state for maintaining login sessions
- **.gitignore**: Excludes sensitive files and debug images
## Generated Files
- **bg.png, fullbg.png**: CAPTCHA background images extracted during solving
- **debug_*.png**: Debug images for CAPTCHA solving analysis (when debug=True)
- **checkin.png**: Screenshot saved upon successful sign-in
## Configuration Variables
Key settings in main.py:53-64:
- `MAX_RETRY`: Maximum retry attempts (0 for infinite)
- `ALREADY_SIGNED_TEXT`: Success detection text ("今天已经签到过啦")
- `SIGNED_ANCESTOR_LEVELS`: Screenshot capture depth (3 parent levels)
- `SIGNED_ANCESTOR_SCREENSHOT`: Success screenshot filename
## CAPTCHA Solving Process
1. Extract background image with gap (`bg.png`)
2. Extract complete background image (`fullbg.png`)
3. Calculate absolute difference between images
4. Apply binary threshold and find contours
5. Select gap location based on largest contours
6. Simulate human-like mouse dragging to solve
## Browser Configuration
- Runs in non-headless mode by default (`headless=False`)
- Uses `slow_mo=200` for human-like interaction timing
- Maintains session state between runs via `state.json`
+85
View File
@@ -0,0 +1,85 @@
# CLAUDE_CN.md
此文件为 Claude Code (claude.ai/code) 在此代码仓库中工作时提供中文指导。
## 项目概述
这是一个使用 Python 和 Playwright 构建的自动化 FRP (NATFRP) 签到机器人。该应用程序可以自动在 www.natfrp.com 上进行每日签到,具备验证码破解能力。
## 架构设计
### 核心组件
- **main.py**: 包含所有自动化逻辑的单文件应用程序
- **计算机视觉模块** (`get_gap_offset`): 使用 OpenCV 通过检测背景图像缺口来破解滑动验证码
- **浏览器自动化**: 使用 Playwright 与 NATFRP 网站交互
- **状态管理**: 使用 `state.json` 保存登录会话以避免重复登录
- **重试机制**: 内置重试逻辑,支持配置最大重试次数
### 关键函数
- `get_gap_offset()`: 验证码破解器,使用图像差异检测 (main.py:7-51)
- `main()`: 主要自动化工作流程 (main.py:251-324)
- `solve_geetest_puzzle()`: 处理极验验证码破解 (main.py:220-235)
- `wait_signed_text_and_shoot()`: 成功检测和截图保存 (main.py:238-248)
## 运行应用程序
### 基本执行
```bash
python main.py
```
### 依赖项
项目需要以下依赖:
- playwright
- opencv-python (cv2)
- pathlib (内置模块)
安装 Playwright 浏览器:
```bash
playwright install chromium
```
## 配置文件
- **account.txt**: 包含 NATFRP 登录用户名(第1行)和密码(第2行)
- **state.json**: 浏览器会话状态,用于保持登录状态
- **.gitignore**: 排除敏感文件和调试图像
## 生成文件
- **bg.png, fullbg.png**: 验证码破解过程中提取的背景图像
- **debug_*.png**: 验证码破解分析的调试图像(当 debug=True 时)
- **checkin.png**: 签到成功时保存的截图
## 配置变量
main.py:53-64 中的关键设置:
- `MAX_RETRY`: 最大重试次数(0表示无限重试)
- `ALREADY_SIGNED_TEXT`: 成功检测文本("今天已经签到过啦")
- `SIGNED_ANCESTOR_LEVELS`: 截图捕获深度(3层父级元素)
- `SIGNED_ANCESTOR_SCREENSHOT`: 成功截图文件名
## 验证码破解流程
1. 提取带缺口的背景图像(`bg.png`)
2. 提取完整背景图像(`fullbg.png`)
3. 计算两张图像的绝对差异
4. 应用二值化阈值并查找轮廓
5. 根据最大轮廓选择缺口位置
6. 模拟人类般的鼠标拖拽来破解验证码
## 浏览器配置
- 默认以非无头模式运行(`headless=False`)
- 使用 `slow_mo=200` 实现类人交互时序
- 通过 `state.json` 在运行间保持会话状态
## 使用说明
1. 确保 `account.txt` 文件包含正确的用户名和密码
2. 首次运行会创建 `state.json` 保存登录状态
3. 程序会自动检测是否已签到,避免重复操作
4. 遇到验证码时会自动进行图像识别和破解
5. 签到成功后会保存截图作为凭证
+201
View File
@@ -0,0 +1,201 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
+116
View File
@@ -0,0 +1,116 @@
# SAKURA FRP 自动签到脚本
> 自动访问 `https://www.natfrp.com/user/` 完成每日签到;支持在本地直接运行,也支持在 **QingLong(青龙)** 定时任务中运行。
> 成功后会自动保存签到结果截图(默认:`checkin.png`),并在必要时自动处理滑块验证。
## 一、目录结构
```
.
├─ main.py # 主程序(无需改动即可用)
├─ account.txt # 账号文件(必填:第1行用户名;第2行密码)
├─ state.json # 登录状态缓存文件(自动生成与更新)
└─ checkin.png # 成功时保存的签到区域截图(自动生成)
```
> 说明:程序会优先尝试复用 `state.json` 中的登录状态;状态失效时会自动回退为账号密码登录。
## 二、准备工作
1. **创建账号文件 `account.txt`**
- 第 1 行:用户名
- 第 2 行:密码
- 文件编码建议使用 `UTF-8`,不要多余空行或空格。
示例:
```
your_username
your_password
```
2. **(可选)检查/调整配置** — 打开 `main.py` 顶部“配置”区域,可按需修改:
- `ACCOUNT_FILE`:账号文件路径(默认 `account.txt`)
- `STATE_FILE`:状态缓存文件(默认 `state.json`)
- `MAX_RETRY`:失败重试次数(`0` 表示无限重试)
- `SIGNED_ANCESTOR_SCREENSHOT`:成功截图文件名(默认 `checkin.png`)
> **无需修改网站域名与地址**,默认已指向 `https://www.natfrp.com/user/`。
## 三、环境要求
需要安装Playwright及其依赖,推荐使用 Python 3.7 及以上版本。
## 四、在 QingLong(青龙)中使用
> 适用于青龙面板 v2.XX 及以上版本(路径请以你面板的实际目录为准)。
### 1)放置脚本与账号文件
方式 A:在面板「文件管理」中将 `main.py` 和 `account.txt` 上传到脚本目录,例如:
```
/ql/data/scripts/natfrp_checkin/
```
方式 B:通过仓库/订阅拉取(如你有自建仓库),确保最终能在脚本目录看到 `main.py` 与 `account.txt`。
> **注意**:`account.txt` 一定要跟 `main.py` 放在同一目录(或同步修改 `ACCOUNT_FILE` 为你的自定义路径)。
### 2)创建定时任务
在青龙面板 ->「定时任务」->「新建任务」:
- **名称**:NATFRP 自动签到
- **命令**(按你的实际路径替换):
```bash
python3 /ql/data/scripts/natfrp_checkin/main.py
```
- **定时规则(CRON)**:建议每日固定时间运行一次,例如每天 8:10:
```
10 8 * * *
```
保存后即可生效。你也可以在任务列表中手动点击「运行」测试是否正常。
### 3)查看运行日志与产物
- 在任务详情页可查看实时日志;
- 成功后会生成 `checkin.png`(截图)与 `state.json`(状态);
- 若失败,会打印原因并根据 `MAX_RETRY` 自动重试。
## 五、常见问题
1. **提示账号文件不存在或格式错误**
- 确认 `account.txt` 与 `main.py` 在同一目录;
- 确认文件编码为 `UTF-8`,且只有两行:第一行用户名,第二行密码;
- 不要包含多余空行或空格。
2. **一直提示登录状态过期**
- 删除同目录下旧的 `state.json` 后重跑;
- 确认账号密码正确;
- 网络/地区问题可能导致访问异常,可稍后再试。
3. **出现滑块但始终无法通过**
- 目标站点的风控策略可能调整;
- 可适当增加重试次数(`MAX_RETRY`),或更换执行时间段;
- 若长时间失败,建议手动登录一次,让站点信任度恢复。
4. **成功了但没有截图**
- 程序以出现“今天已经签到过啦”的提示为成功判据;
- 如果站点文案发生变化,可修改 `ALREADY_SIGNED_TEXT`;
- 截图保存路径由 `SIGNED_ANCESTOR_SCREENSHOT` 控制(默认 `checkin.png`)。
5. **青龙里命令执行不动或报路径错误**
- 确认命令中的绝对路径与实际文件路径一致;
- 如果你把文件放在其它目录,务必同步修改命令与 `ACCOUNT_FILE` 路径。
## 六、日志与产物
- **日志输出**:执行时会输出每一步进度与判定结果,便于排查。
- **截图与状态**:
- `checkin.png`:成功时的页面关键区域截图;
- `state.json`:登录状态缓存(自动生成/刷新);
- 若启用内部调试流程,可能在同目录产生若干 `debug_*.png` 用于图像比对,这些文件可忽略或自行清理。
---
**至此即可完成配置与使用。**
如需嵌入到你现有的青龙仓库,只要保证 `main.py` 与 `account.txt` 的相对位置一致,并按上文的命令与定时规则召唤即可。祝签到愉快 🎯
Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

+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)