增加飞牛论坛签到脚本
This commit is contained in:
parent
2e761c690b
commit
b8d08736b5
106
FnForum/README.md
Normal file
106
FnForum/README.md
Normal file
@ -0,0 +1,106 @@
|
||||
# 飞牛论坛自动签到脚本
|
||||
|
||||
## 脚本功能
|
||||
|
||||
飞牛论坛自动签到脚本用于自动完成飞牛论坛的每日签到任务,支持以下功能:
|
||||
|
||||
- 自动登录飞牛论坛账号
|
||||
- 自动识别验证码(通过百度OCR API)
|
||||
- 每日自动签到
|
||||
- 签到结果通过通知功能发送
|
||||
|
||||
## 依赖要求
|
||||
|
||||
### 软件依赖
|
||||
- Python 3.6+
|
||||
- 以下Python库:
|
||||
- requests
|
||||
- beautifulsoup4
|
||||
|
||||
### 安装命令
|
||||
```bash
|
||||
pip install requests beautifulsoup4
|
||||
```
|
||||
|
||||
## 配置方法
|
||||
|
||||
### 1. 环境变量配置
|
||||
|
||||
#### (1) 账号配置 - FN_CONFIG
|
||||
```json
|
||||
{
|
||||
"USERNAME": "你的飞牛论坛用户名",
|
||||
"PASSWORD": "你的飞牛论坛密码",
|
||||
"BASE_URL": "飞牛论坛基础URL(默认:https://club.fnnas.com/)"
|
||||
}
|
||||
```
|
||||
|
||||
#### (2) 百度OCR API配置 - BAIDU_API_CONFIG
|
||||
```json
|
||||
{
|
||||
"API_KEY": "你的百度OCR API Key",
|
||||
"SECRET_KEY": "你的百度OCR Secret Key"
|
||||
}
|
||||
```
|
||||
|
||||
### 2. 百度OCR API申请
|
||||
1. 注册百度智能云账号:https://cloud.baidu.com
|
||||
2. 创建文字识别应用,获取API Key和Secret Key
|
||||
3. 将获取的密钥填入环境变量
|
||||
|
||||
## 使用说明
|
||||
|
||||
### 运行方式
|
||||
```bash
|
||||
python3 sign_with_qinglong.py
|
||||
```
|
||||
|
||||
### 定时任务示例
|
||||
```bash
|
||||
0 8 * * * python3 sign_with_qinglong.py # 每天8点自动签到
|
||||
```
|
||||
|
||||
## 通知功能
|
||||
|
||||
签到结果将通过通知功能发送,示例内容:
|
||||
```
|
||||
📅 签到日期: 2023-05-10 08:30:45
|
||||
|
||||
👤 账号: your_username
|
||||
--------------------------------
|
||||
✅ 签到成功!
|
||||
|
||||
📊 签到数据:
|
||||
• 连续签到: 5天
|
||||
• 累计签到: 30天
|
||||
• 今日获得积分: 100
|
||||
|
||||
--------------------------------
|
||||
💻 脚本运行完成!
|
||||
🚀 下次见!
|
||||
```
|
||||
|
||||
## 常见问题
|
||||
|
||||
### 1. 验证码识别失败
|
||||
- 确认百度OCR API密钥正确
|
||||
- 检查验证码图片清晰度,可尝试更换OCR识别类型
|
||||
- 避免频繁请求触发API频率限制
|
||||
|
||||
### 2. 登录失败
|
||||
- 检查账号密码是否正确
|
||||
- 确认论坛登录页面结构是否变更
|
||||
- 尝试删除cookies.json后重新登录
|
||||
|
||||
### 3. 通知未收到
|
||||
- 确保通知模块路径正确
|
||||
- 手动测试通知功能:`python notify.py "测试通知" "测试内容"`
|
||||
|
||||
## 免责协议
|
||||
|
||||
1. 本脚本仅供个人学习和研究使用,请勿用于商业用途或其他非法目的。
|
||||
2. 脚本运行可能涉及论坛账号登录和数据获取,请严格遵守飞牛论坛的用户协议和相关法律法规。
|
||||
3. 作者不对脚本的可用性、准确性负责,使用本脚本产生的一切后果由使用者自行承担。
|
||||
4. 如因使用本脚本导致账号异常、封禁或其他损失,作者不承担任何责任。
|
||||
5. 若论坛调整页面结构或策略导致脚本失效,作者无义务持续维护更新。
|
||||
6. 使用本脚本即视为同意上述条款,请勿在未经允许的情况下将脚本用于他人账号。
|
731
FnForum/fn_signin.py
Normal file
731
FnForum/fn_signin.py
Normal file
@ -0,0 +1,731 @@
|
||||
"""
|
||||
任务名称
|
||||
name: 飞牛论坛签到
|
||||
定时规则
|
||||
cron: 0 0 8 * * ?
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import re
|
||||
import json
|
||||
import time
|
||||
import requests
|
||||
import base64
|
||||
import urllib.parse
|
||||
from bs4 import BeautifulSoup
|
||||
from datetime import datetime
|
||||
|
||||
# 添加青龙脚本根目录到Python路径
|
||||
QL_SCRIPTS_DIR = '/ql/scripts' # 青龙脚本默认目录
|
||||
sys.path.append(QL_SCRIPTS_DIR)
|
||||
|
||||
# 添加notify可能存在的其他路径
|
||||
POSSIBLE_PATHS = [
|
||||
'/ql', # 青龙根目录
|
||||
'/ql/data/scripts', # 新版青龙数据目录
|
||||
'/ql/scripts/notify', # 自定义通知目录
|
||||
os.path.dirname(__file__) # 当前脚本目录
|
||||
]
|
||||
|
||||
for path in POSSIBLE_PATHS:
|
||||
if os.path.exists(os.path.join(path, 'notify.py')):
|
||||
sys.path.append(path)
|
||||
break
|
||||
|
||||
try:
|
||||
from notify import send
|
||||
except ImportError:
|
||||
print("⚠️ 无法加载通知模块,请检查路径配置")
|
||||
send = lambda title, content: None # 创建空函数防止报错
|
||||
|
||||
class ConfigLoader:
|
||||
"""从青龙环境变量加载配置"""
|
||||
|
||||
@staticmethod
|
||||
def get_env_config():
|
||||
"""获取环境变量配置"""
|
||||
config = {}
|
||||
|
||||
# 从青龙环境变量获取配置
|
||||
fn_config = os.getenv('FN_CONFIG', '')
|
||||
baidu_api_config = os.getenv('BAIDU_API_CONFIG', '')
|
||||
|
||||
# 检查必要配置是否存在
|
||||
if not fn_config or not baidu_api_config:
|
||||
print("错误: 缺少必要的配置 - FN_CONFIG 和 BAIDU_API_CONFIG")
|
||||
return None
|
||||
|
||||
# 解析配置
|
||||
try:
|
||||
fn_config = json.loads(fn_config)
|
||||
baidu_api_config = json.loads(baidu_api_config)
|
||||
except json.JSONDecodeError as e:
|
||||
print(f"配置解析失败: {e}")
|
||||
return None
|
||||
|
||||
# 检查必要字段
|
||||
required_fn_fields = ['USERNAME', 'PASSWORD', 'BASE_URL']
|
||||
required_baidu_fields = ['API_KEY', 'SECRET_KEY']
|
||||
|
||||
missing_fn_fields = [field for field in required_fn_fields if field not in fn_config]
|
||||
missing_baidu_fields = [field for field in required_baidu_fields if field not in baidu_api_config]
|
||||
|
||||
if missing_fn_fields or missing_baidu_fields:
|
||||
missing_fields = missing_fn_fields + missing_baidu_fields
|
||||
print(f"错误: 缺少必要的配置字段: {', '.join(missing_fields)}")
|
||||
return None
|
||||
|
||||
# 设置配置
|
||||
config.update(fn_config)
|
||||
config.update(baidu_api_config)
|
||||
|
||||
# 设置其他配置
|
||||
config['LOGIN_URL'] = config['BASE_URL'] + 'member.php?mod=logging&action=login'
|
||||
config['SIGN_URL'] = config['BASE_URL'] + 'plugin.php?id=zqlj_sign'
|
||||
config['COOKIE_FILE'] = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'cookies.json')
|
||||
config['CAPTCHA_API_URL'] = "https://aip.baidubce.com/rest/2.0/ocr/v1/accurate_basic"
|
||||
config['MAX_RETRIES'] = int(os.getenv('FN_MAX_RETRIES', 3))
|
||||
config['RETRY_DELAY'] = int(os.getenv('FN_RETRY_DELAY', 2))
|
||||
config['TOKEN_CACHE_FILE'] = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'token_cache.json')
|
||||
|
||||
return config
|
||||
|
||||
# 配置信息
|
||||
class Config:
|
||||
# 从环境变量加载配置
|
||||
env_config = ConfigLoader.get_env_config()
|
||||
|
||||
if env_config:
|
||||
# 账号信息
|
||||
USERNAME = env_config['USERNAME']
|
||||
PASSWORD = env_config['PASSWORD']
|
||||
|
||||
# 网站URL
|
||||
BASE_URL = env_config['BASE_URL']
|
||||
LOGIN_URL = env_config['LOGIN_URL']
|
||||
SIGN_URL = env_config['SIGN_URL']
|
||||
|
||||
# Cookie文件路径
|
||||
COOKIE_FILE = env_config['COOKIE_FILE']
|
||||
|
||||
# 验证码识别API (百度OCR API)
|
||||
CAPTCHA_API_URL = env_config['CAPTCHA_API_URL']
|
||||
API_KEY = env_config['API_KEY']
|
||||
SECRET_KEY = env_config['SECRET_KEY']
|
||||
|
||||
# 重试设置
|
||||
MAX_RETRIES = env_config['MAX_RETRIES']
|
||||
RETRY_DELAY = env_config['RETRY_DELAY']
|
||||
|
||||
# Token缓存文件
|
||||
TOKEN_CACHE_FILE = env_config['TOKEN_CACHE_FILE']
|
||||
else:
|
||||
# 如果缺少必要配置,设置默认值(将导致运行失败)
|
||||
USERNAME = 'your_username'
|
||||
PASSWORD = 'your_password'
|
||||
API_KEY = 'your_api_key'
|
||||
SECRET_KEY = 'your_secret_key'
|
||||
BASE_URL = 'https://club.fnnas.com/'
|
||||
|
||||
class FNSignIn:
|
||||
def __init__(self):
|
||||
self.session = requests.Session()
|
||||
self.session.headers.update({
|
||||
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36',
|
||||
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8',
|
||||
'Accept-Language': 'zh-CN,zh;q=0.9,en;q=0.8'
|
||||
})
|
||||
self.load_cookies()
|
||||
|
||||
def load_cookies(self):
|
||||
"""从文件加载Cookie"""
|
||||
if os.path.exists(Config.COOKIE_FILE):
|
||||
try:
|
||||
with open(Config.COOKIE_FILE, 'r') as f:
|
||||
cookies_list = json.load(f)
|
||||
|
||||
# 检查是否为新格式的Cookie列表
|
||||
if isinstance(cookies_list, list) and len(cookies_list) > 0 and 'name' in cookies_list[0]:
|
||||
# 新格式:包含完整Cookie属性的列表
|
||||
for cookie_dict in cookies_list:
|
||||
self.session.cookies.set(
|
||||
cookie_dict['name'],
|
||||
cookie_dict['value'],
|
||||
domain=cookie_dict.get('domain'),
|
||||
path=cookie_dict.get('path')
|
||||
)
|
||||
else:
|
||||
# 旧格式:简单的名称-值字典
|
||||
self.session.cookies.update(cookies_list)
|
||||
|
||||
print("已从文件加载Cookie")
|
||||
return True
|
||||
except Exception as e:
|
||||
print(f"加载Cookie失败: {e}")
|
||||
return False
|
||||
|
||||
def save_cookies(self):
|
||||
"""保存Cookie到文件"""
|
||||
try:
|
||||
# 保存完整的Cookie信息,包括域名、路径等属性
|
||||
cookies_list = []
|
||||
for cookie in self.session.cookies:
|
||||
cookie_dict = {
|
||||
'name': cookie.name,
|
||||
'value': cookie.value,
|
||||
'domain': cookie.domain,
|
||||
'path': cookie.path,
|
||||
'expires': cookie.expires,
|
||||
'secure': cookie.secure
|
||||
}
|
||||
cookies_list.append(cookie_dict)
|
||||
|
||||
with open(Config.COOKIE_FILE, 'w') as f:
|
||||
json.dump(cookies_list, f)
|
||||
print("Cookie已保存到文件")
|
||||
return True
|
||||
except Exception as e:
|
||||
print(f"保存Cookie失败: {e}")
|
||||
return False
|
||||
|
||||
def check_login_status(self):
|
||||
"""检查登录状态"""
|
||||
try:
|
||||
response = self.session.get(Config.BASE_URL)
|
||||
soup = BeautifulSoup(response.text, 'html.parser')
|
||||
|
||||
# 检查是否存在登录链接,如果存在则表示未登录
|
||||
login_links = soup.select('a[href*="member.php?mod=logging&action=login"]')
|
||||
|
||||
# 检查页面内容是否包含用户名
|
||||
username_in_page = Config.USERNAME in response.text
|
||||
|
||||
# 检查是否有个人中心链接
|
||||
user_center_links = soup.select('a[href*="home.php?mod=space"]')
|
||||
|
||||
# 如果没有登录链接或者页面中包含用户名,则认为已登录
|
||||
if (len(login_links) == 0 or username_in_page) and len(user_center_links) > 0:
|
||||
print("Cookie有效,已登录状态")
|
||||
return True
|
||||
else:
|
||||
print("Cookie无效或已过期,需要重新登录")
|
||||
return False
|
||||
except Exception as e:
|
||||
print(f"检查登录状态失败: {e}")
|
||||
return False
|
||||
|
||||
def get_access_token(self):
|
||||
"""获取百度API的access_token,带缓存功能"""
|
||||
try:
|
||||
# 检查是否有缓存的token
|
||||
if os.path.exists(Config.TOKEN_CACHE_FILE):
|
||||
try:
|
||||
with open(Config.TOKEN_CACHE_FILE, 'r') as f:
|
||||
token_data = json.load(f)
|
||||
# 检查token是否过期(百度token有效期为30天)
|
||||
if token_data.get('expires_time', 0) > time.time():
|
||||
print("使用缓存的access_token")
|
||||
return token_data.get('access_token')
|
||||
else:
|
||||
print("缓存的access_token已过期,重新获取")
|
||||
except Exception as e:
|
||||
print(f"读取token缓存文件失败: {e}")
|
||||
|
||||
# 获取新token
|
||||
url = "https://aip.baidubce.com/oauth/2.0/token"
|
||||
params = {
|
||||
"grant_type": "client_credentials",
|
||||
"client_id": Config.API_KEY,
|
||||
"client_secret": Config.SECRET_KEY
|
||||
}
|
||||
|
||||
# 添加重试机制
|
||||
for retry in range(Config.MAX_RETRIES):
|
||||
try:
|
||||
response = requests.post(url, params=params)
|
||||
if response.status_code == 200:
|
||||
result = response.json()
|
||||
access_token = str(result.get("access_token"))
|
||||
expires_in = result.get("expires_in", 2592000) # 默认30天
|
||||
|
||||
# 缓存token
|
||||
token_cache = {
|
||||
'access_token': access_token,
|
||||
'expires_time': time.time() + expires_in - 86400 # 提前一天过期
|
||||
}
|
||||
try:
|
||||
with open(Config.TOKEN_CACHE_FILE, 'w') as f:
|
||||
json.dump(token_cache, f)
|
||||
print("access_token已缓存")
|
||||
except Exception as e:
|
||||
print(f"缓存access_token失败: {e}")
|
||||
|
||||
return access_token
|
||||
else:
|
||||
print(f"获取access_token失败,状态码: {response.status_code},重试({retry+1}/{Config.MAX_RETRIES})")
|
||||
if retry < Config.MAX_RETRIES - 1:
|
||||
time.sleep(Config.RETRY_DELAY)
|
||||
continue
|
||||
except Exception as e:
|
||||
print(f"获取access_token请求异常: {e},重试({retry+1}/{Config.MAX_RETRIES})")
|
||||
if retry < Config.MAX_RETRIES - 1:
|
||||
time.sleep(Config.RETRY_DELAY)
|
||||
continue
|
||||
|
||||
print(f"获取access_token失败,已达到最大重试次数({Config.MAX_RETRIES})")
|
||||
return None
|
||||
except Exception as e:
|
||||
print(f"获取access_token过程发生错误: {e}")
|
||||
return None
|
||||
|
||||
def recognize_captcha(self, captcha_url):
|
||||
"""识别验证码,带重试机制"""
|
||||
for retry in range(Config.MAX_RETRIES):
|
||||
try:
|
||||
# 下载验证码图片
|
||||
captcha_response = self.session.get(captcha_url)
|
||||
if captcha_response.status_code != 200:
|
||||
print(f"下载验证码图片失败,状态码: {captcha_response.status_code},重试({retry+1}/{Config.MAX_RETRIES})")
|
||||
if retry < Config.MAX_RETRIES - 1:
|
||||
time.sleep(Config.RETRY_DELAY)
|
||||
continue
|
||||
return None
|
||||
|
||||
# 将图片转换为Base64编码
|
||||
captcha_base64 = base64.b64encode(captcha_response.content).decode('utf-8')
|
||||
|
||||
# 获取access_token
|
||||
access_token = self.get_access_token()
|
||||
if not access_token:
|
||||
print(f"获取百度API access_token失败,重试({retry+1}/{Config.MAX_RETRIES})")
|
||||
if retry < Config.MAX_RETRIES - 1:
|
||||
time.sleep(Config.RETRY_DELAY)
|
||||
continue
|
||||
return None
|
||||
|
||||
# 构建API请求URL
|
||||
url = f"{Config.CAPTCHA_API_URL}?access_token={access_token}"
|
||||
|
||||
# 构建请求参数
|
||||
payload = f'image={urllib.parse.quote_plus(captcha_base64)}&detect_direction=false¶graph=false&probability=false'
|
||||
|
||||
# 设置请求头
|
||||
headers = {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
'Accept': 'application/json'
|
||||
}
|
||||
|
||||
# 发送请求
|
||||
api_response = requests.request("POST", url, headers=headers, data=payload.encode("utf-8"))
|
||||
|
||||
if api_response.status_code != 200:
|
||||
print(f"验证码识别API请求失败,状态码: {api_response.status_code},重试({retry+1}/{Config.MAX_RETRIES})")
|
||||
if retry < Config.MAX_RETRIES - 1:
|
||||
time.sleep(Config.RETRY_DELAY)
|
||||
continue
|
||||
return None
|
||||
|
||||
# 解析API响应
|
||||
result = api_response.json()
|
||||
if 'words_result' in result and len(result['words_result']) > 0:
|
||||
captcha_text = result['words_result'][0]['words']
|
||||
# 清理验证码文本,移除空格和特殊字符
|
||||
captcha_text = re.sub(r'[\s\W]+', '', captcha_text)
|
||||
print(f"验证码识别成功: {captcha_text}")
|
||||
return captcha_text
|
||||
elif 'error_code' in result:
|
||||
print(f"验证码识别API返回错误: {result.get('error_code')}, {result.get('error_msg')},重试({retry+1}/{Config.MAX_RETRIES})")
|
||||
if retry < Config.MAX_RETRIES - 1:
|
||||
time.sleep(Config.RETRY_DELAY)
|
||||
continue
|
||||
return None
|
||||
else:
|
||||
print(f"验证码识别API返回格式异常: {result},重试({retry+1}/{Config.MAX_RETRIES})")
|
||||
if retry < Config.MAX_RETRIES - 1:
|
||||
time.sleep(Config.RETRY_DELAY)
|
||||
continue
|
||||
return None
|
||||
except Exception as e:
|
||||
print(f"验证码识别过程发生错误: {e},重试({retry+1}/{Config.MAX_RETRIES})")
|
||||
if retry < Config.MAX_RETRIES - 1:
|
||||
time.sleep(Config.RETRY_DELAY)
|
||||
continue
|
||||
return None
|
||||
|
||||
print(f"验证码识别失败,已达到最大重试次数({Config.MAX_RETRIES})")
|
||||
return None
|
||||
|
||||
def login(self):
|
||||
"""使用账号密码登录,带重试机制"""
|
||||
for retry in range(Config.MAX_RETRIES):
|
||||
try:
|
||||
# 获取登录页面
|
||||
response = self.session.get(Config.LOGIN_URL)
|
||||
soup = BeautifulSoup(response.text, 'html.parser')
|
||||
|
||||
# 获取登录表单信息
|
||||
login_form = None
|
||||
for form in soup.find_all('form'):
|
||||
form_id = form.get('id', '')
|
||||
if form_id and ('loginform' in form_id or 'lsform' in form_id):
|
||||
login_form = form
|
||||
break
|
||||
elif form.get('name') == 'login':
|
||||
login_form = form
|
||||
break
|
||||
elif form.get('action') and 'logging' in form.get('action'):
|
||||
login_form = form
|
||||
break
|
||||
|
||||
if not login_form:
|
||||
# 尝试查找任何表单,可能是登录表单
|
||||
all_forms = soup.find_all('form')
|
||||
if all_forms:
|
||||
login_form = all_forms[0] # 使用第一个表单
|
||||
print(f"使用备选表单: ID={login_form.get('id')}, Action={login_form.get('action')}")
|
||||
|
||||
if not login_form:
|
||||
print(f"未找到登录表单,重试({retry+1}/{Config.MAX_RETRIES})")
|
||||
if retry < Config.MAX_RETRIES - 1:
|
||||
time.sleep(Config.RETRY_DELAY)
|
||||
continue
|
||||
return False
|
||||
|
||||
# 提取登录表单ID中的随机部分
|
||||
form_id = login_form.get('id', '')
|
||||
login_hash = form_id.split('_')[-1] if '_' in form_id else ''
|
||||
|
||||
# 获取登录表单的action属性
|
||||
form_action = login_form.get('action', '')
|
||||
print(f"找到登录表单: ID={form_id}, Action={form_action}")
|
||||
|
||||
# 获取表单字段
|
||||
formhash = soup.find('input', {'name': 'formhash'})
|
||||
if not formhash:
|
||||
print(f"未找到登录表单的formhash字段,重试({retry+1}/{Config.MAX_RETRIES})")
|
||||
if retry < Config.MAX_RETRIES - 1:
|
||||
time.sleep(Config.RETRY_DELAY)
|
||||
continue
|
||||
return False
|
||||
|
||||
# 获取表单字段
|
||||
formhash = formhash['value']
|
||||
|
||||
# 获取用户名输入框ID
|
||||
username_input = soup.find('input', {'name': 'username'})
|
||||
username_id = username_input.get('id', '') if username_input else ''
|
||||
|
||||
# 获取密码输入框ID
|
||||
password_input = soup.find('input', {'name': 'password'})
|
||||
password_id = password_input.get('id', '') if password_input else ''
|
||||
|
||||
print(f"找到用户名输入框ID: {username_id}")
|
||||
print(f"找到密码输入框ID: {password_id}")
|
||||
|
||||
# 构建登录数据
|
||||
login_data = {
|
||||
'formhash': formhash,
|
||||
'referer': Config.BASE_URL,
|
||||
'loginfield': 'username',
|
||||
'username': Config.USERNAME,
|
||||
'password': Config.PASSWORD,
|
||||
'questionid': '0',
|
||||
'answer': '',
|
||||
'cookietime': '2592000', # 保持登录状态30天
|
||||
'loginsubmit': 'true'
|
||||
}
|
||||
|
||||
# 添加特定的表单字段
|
||||
if username_id:
|
||||
login_data[username_id] = Config.USERNAME
|
||||
if password_id:
|
||||
login_data[password_id] = Config.PASSWORD
|
||||
|
||||
# 检查是否需要验证码
|
||||
seccodeverify = soup.find('input', {'name': 'seccodeverify'})
|
||||
if seccodeverify:
|
||||
print("检测到需要验证码,尝试自动识别验证码")
|
||||
|
||||
# 获取验证码ID
|
||||
seccode_id = seccodeverify.get('id', '').replace('seccodeverify_', '')
|
||||
|
||||
# 获取验证码图片URL
|
||||
captcha_img = soup.find('img', {'src': re.compile(r'misc\.php\?mod=seccode')})
|
||||
if not captcha_img:
|
||||
print(f"未找到验证码图片,重试({retry+1}/{Config.MAX_RETRIES})")
|
||||
if retry < Config.MAX_RETRIES - 1:
|
||||
time.sleep(Config.RETRY_DELAY)
|
||||
continue
|
||||
return False
|
||||
|
||||
captcha_url = Config.BASE_URL + captcha_img['src']
|
||||
print(f"验证码图片URL: {captcha_url}")
|
||||
|
||||
# 识别验证码
|
||||
captcha_text = self.recognize_captcha(captcha_url)
|
||||
if not captcha_text:
|
||||
print(f"验证码识别失败,重试({retry+1}/{Config.MAX_RETRIES})")
|
||||
if retry < Config.MAX_RETRIES - 1:
|
||||
time.sleep(Config.RETRY_DELAY)
|
||||
continue
|
||||
return False
|
||||
|
||||
# 添加验证码到登录数据
|
||||
login_data['seccodeverify'] = captcha_text
|
||||
login_data['seccodehash'] = seccode_id
|
||||
|
||||
# 更新请求头,模拟真实浏览器
|
||||
self.session.headers.update({
|
||||
'Origin': Config.BASE_URL.rstrip('/'),
|
||||
'Referer': Config.LOGIN_URL,
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
'Upgrade-Insecure-Requests': '1'
|
||||
})
|
||||
|
||||
# 构建登录URL
|
||||
login_url = f"{Config.LOGIN_URL}&loginsubmit=yes&inajax=1"
|
||||
|
||||
# 发送登录请求
|
||||
login_response = self.session.post(login_url, data=login_data, allow_redirects=True)
|
||||
|
||||
# 检查登录结果
|
||||
if '验证码' in login_response.text and '验证码错误' in login_response.text:
|
||||
print(f"验证码错误,登录失败,重试({retry+1}/{Config.MAX_RETRIES})")
|
||||
if retry < Config.MAX_RETRIES - 1:
|
||||
time.sleep(Config.RETRY_DELAY)
|
||||
continue
|
||||
return False
|
||||
|
||||
# 检查登录是否成功
|
||||
if 'succeedhandle_' in login_response.text or self.check_login_status():
|
||||
print(f"账号 {Config.USERNAME} 登录成功")
|
||||
self.save_cookies()
|
||||
return True
|
||||
else:
|
||||
print(f"登录失败,请检查账号密码,重试({retry+1}/{Config.MAX_RETRIES})")
|
||||
if retry < Config.MAX_RETRIES - 1:
|
||||
time.sleep(Config.RETRY_DELAY)
|
||||
continue
|
||||
return False
|
||||
except Exception as e:
|
||||
print(f"登录过程发生错误: {e},重试({retry+1}/{Config.MAX_RETRIES})")
|
||||
if retry < Config.MAX_RETRIES - 1:
|
||||
time.sleep(Config.RETRY_DELAY)
|
||||
continue
|
||||
return False
|
||||
|
||||
print(f"登录失败,已达到最大重试次数({Config.MAX_RETRIES})")
|
||||
return False
|
||||
|
||||
def check_sign_status(self):
|
||||
"""检查签到状态,带重试机制"""
|
||||
for retry in range(Config.MAX_RETRIES):
|
||||
try:
|
||||
response = self.session.get(Config.SIGN_URL)
|
||||
soup = BeautifulSoup(response.text, 'html.parser')
|
||||
|
||||
# 查找签到按钮
|
||||
sign_btn = soup.select_one('.signbtn .btna')
|
||||
if not sign_btn:
|
||||
print(f"未找到签到按钮,重试({retry+1}/{Config.MAX_RETRIES})")
|
||||
if retry < Config.MAX_RETRIES - 1:
|
||||
time.sleep(Config.RETRY_DELAY)
|
||||
continue
|
||||
return None, None
|
||||
|
||||
# 获取签到链接和状态
|
||||
sign_text = sign_btn.text.strip()
|
||||
sign_link = sign_btn.get('href')
|
||||
|
||||
# 提取sign参数
|
||||
sign_param = None
|
||||
if sign_link:
|
||||
match = re.search(r'sign=([^&]+)', sign_link)
|
||||
if match:
|
||||
sign_param = match.group(1)
|
||||
|
||||
return sign_text, sign_param
|
||||
except Exception as e:
|
||||
print(f"检查签到状态失败: {e},重试({retry+1}/{Config.MAX_RETRIES})")
|
||||
if retry < Config.MAX_RETRIES - 1:
|
||||
time.sleep(Config.RETRY_DELAY)
|
||||
continue
|
||||
return None, None
|
||||
|
||||
def do_sign(self, sign_param):
|
||||
"""执行签到,带重试机制"""
|
||||
for retry in range(Config.MAX_RETRIES):
|
||||
try:
|
||||
sign_url = f"{Config.SIGN_URL}&sign={sign_param}"
|
||||
response = self.session.get(sign_url)
|
||||
|
||||
# 检查签到结果
|
||||
if response.status_code == 200:
|
||||
# 再次检查签到状态
|
||||
sign_text, _ = self.check_sign_status()
|
||||
if sign_text == "今日已打卡":
|
||||
print("签到成功")
|
||||
return True
|
||||
else:
|
||||
print(f"签到请求已发送,但状态未更新,重试({retry+1}/{Config.MAX_RETRIES})")
|
||||
if retry < Config.MAX_RETRIES - 1:
|
||||
time.sleep(Config.RETRY_DELAY)
|
||||
continue
|
||||
return False
|
||||
else:
|
||||
print(f"签到请求失败,状态码: {response.status_code},重试({retry+1}/{Config.MAX_RETRIES})")
|
||||
if retry < Config.MAX_RETRIES - 1:
|
||||
time.sleep(Config.RETRY_DELAY)
|
||||
continue
|
||||
return False
|
||||
except Exception as e:
|
||||
print(f"签到过程发生错误: {e},重试({retry+1}/{Config.MAX_RETRIES})")
|
||||
if retry < Config.MAX_RETRIES - 1:
|
||||
time.sleep(Config.RETRY_DELAY)
|
||||
continue
|
||||
return False
|
||||
|
||||
def get_sign_info(self):
|
||||
"""获取签到信息,带重试机制"""
|
||||
for retry in range(Config.MAX_RETRIES):
|
||||
try:
|
||||
response = self.session.get(Config.SIGN_URL)
|
||||
soup = BeautifulSoup(response.text, 'html.parser')
|
||||
|
||||
# 查找签到信息区域
|
||||
sign_info_divs = soup.find_all('div', class_='bm')
|
||||
sign_info_div = None
|
||||
for div in sign_info_divs:
|
||||
header = div.find('div', class_='bm_h')
|
||||
if header and '我的打卡动态' in header.get_text():
|
||||
sign_info_div = div
|
||||
break
|
||||
|
||||
if not sign_info_div:
|
||||
print(f"未找到签到信息区域,重试({retry+1}/{Config.MAX_RETRIES})")
|
||||
if retry < Config.MAX_RETRIES - 1:
|
||||
time.sleep(Config.RETRY_DELAY)
|
||||
continue
|
||||
return {}
|
||||
|
||||
# 查找签到信息列表
|
||||
info_list = sign_info_div.find('div', class_='bm_c').find_all('li')
|
||||
|
||||
# 解析签到信息
|
||||
sign_info = {}
|
||||
for item in info_list:
|
||||
text = item.get_text(strip=True)
|
||||
if ':' in text:
|
||||
key, value = text.split(':', 1)
|
||||
sign_info[key] = value
|
||||
|
||||
return sign_info
|
||||
except Exception as e:
|
||||
print(f"获取签到信息失败: {e},重试({retry+1}/{Config.MAX_RETRIES})")
|
||||
if retry < Config.MAX_RETRIES - 1:
|
||||
time.sleep(Config.RETRY_DELAY)
|
||||
continue
|
||||
return {}
|
||||
|
||||
print(f"获取签到信息失败,已达到最大重试次数({Config.MAX_RETRIES})")
|
||||
return {}
|
||||
|
||||
def run(self):
|
||||
"""运行签到流程,带重试机制"""
|
||||
print("===== 开始运行签到脚本 =====")
|
||||
|
||||
# 检查登录状态
|
||||
if not self.check_login_status():
|
||||
# 如果未登录,尝试登录
|
||||
if not self.login():
|
||||
print("登录失败,签到流程终止")
|
||||
return False, "登录失败"
|
||||
|
||||
# 检查签到状态
|
||||
sign_text, sign_param = self.check_sign_status()
|
||||
if sign_text is None or sign_param is None:
|
||||
print("获取签到状态失败,签到流程终止")
|
||||
return False, "获取签到状态失败"
|
||||
|
||||
print(f"当前签到状态: {sign_text}")
|
||||
|
||||
# 初始化通知内容
|
||||
notify_title = f"飞牛论坛签到通知"
|
||||
notify_content = f"📅 签到日期: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n\n"
|
||||
notify_content += f"👤 账号: {Config.USERNAME}\n"
|
||||
notify_content += "--------------------------------\n"
|
||||
|
||||
# 如果未签到,执行签到
|
||||
if sign_text == "点击打卡":
|
||||
print("开始执行签到...")
|
||||
if self.do_sign(sign_param):
|
||||
# 获取并记录签到信息
|
||||
sign_info = self.get_sign_info()
|
||||
if sign_info:
|
||||
print("===== 签到信息 =====")
|
||||
notify_content += "✅ 签到成功!\n\n"
|
||||
notify_content += "📊 签到数据:\n"
|
||||
for key, value in sign_info.items():
|
||||
print(f"{key}: {value}")
|
||||
notify_content += f" • {key}: {value}\n"
|
||||
else:
|
||||
notify_content += "✅ 签到成功!\n"
|
||||
notify_content += " • 未能获取详细签到数据\n"
|
||||
else:
|
||||
notify_content += "❌ 签到失败!\n"
|
||||
notify_content += " • 请检查日志查看详细原因\n"
|
||||
print("签到失败")
|
||||
# 发送通知
|
||||
send(notify_title, notify_content)
|
||||
return False, "签到失败"
|
||||
elif sign_text == "今日已打卡":
|
||||
print("今日已签到,无需重复签到")
|
||||
# 获取并记录签到信息
|
||||
sign_info = self.get_sign_info()
|
||||
if sign_info:
|
||||
print("===== 签到信息 =====")
|
||||
notify_content += "✅ 今日已签到!\n\n"
|
||||
notify_content += "📊 签到数据:\n"
|
||||
for key, value in sign_info.items():
|
||||
print(f"{key}: {value}")
|
||||
notify_content += f" • {key}: {value}\n"
|
||||
else:
|
||||
notify_content += "✅ 今日已签到!\n"
|
||||
notify_content += " • 未能获取详细签到数据\n"
|
||||
else:
|
||||
notify_content += f"⚠️ 未知的签到状态: {sign_text},签到流程终止!\n"
|
||||
print(f"未知的签到状态: {sign_text},签到流程终止")
|
||||
# 发送通知
|
||||
send(notify_title, notify_content)
|
||||
return False, f"未知的签到状态: {sign_text}"
|
||||
|
||||
# 添加底部信息
|
||||
notify_content += "\n--------------------------------\n"
|
||||
notify_content += "💻 脚本运行完成!\n"
|
||||
notify_content += "🚀 下次见!"
|
||||
|
||||
# 发送通知
|
||||
send(notify_title, notify_content)
|
||||
|
||||
return True, "签到成功" if sign_text == "点击打卡" else "今日已签到"
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
try:
|
||||
# 创建签到实例并运行
|
||||
sign = FNSignIn()
|
||||
result, message = sign.run()
|
||||
|
||||
# 输出最终结果
|
||||
if result:
|
||||
print("===== 签到脚本执行成功 =====")
|
||||
else:
|
||||
print("===== 签到脚本执行失败 =====")
|
||||
except KeyboardInterrupt:
|
||||
print("脚本被用户中断")
|
||||
except Exception as e:
|
||||
print(f"脚本运行出错: {e}")
|
||||
# 输出详细的异常堆栈信息
|
||||
import traceback
|
||||
print(traceback.format_exc())
|
Loading…
Reference in New Issue
Block a user