雨云签到青龙脚本【多账号 + 过验证码】
@fatekey 大佬的方案是单独开一个容器,我想着反正我有青龙面板在跑脚本,站在巨人的肩膀上顺手写了一个,把多账户也写进来了
一、运行效果
先看实际运行的效果吧
【前两个账号运行已签到,仅测试多账户切换功能,临时注册的第三个号用于测试签到功能】
## 开始执行... 2026-01-23 16:57:53
2026-01-23 16:57:54.116950139 [W:onnxruntime:Default, cpuid_info.cc:91 LogEarlyWarning] Unknown CPU vendor. cpuinfo_vendor value: 16
2026-01-23 16:57:54,191 - INFO - --------------------------------------------------------------------------------
2026-01-23 16:57:54,191 - INFO - 雨云签到工具 by SerendipityR ~
2026-01-23 16:57:54,191 - INFO - Github发布页: https://github.com/SerendipityR-2022/Rainyun-Qiandao
2026-01-23 16:57:54,192 - INFO - --------------------------------------------------------------------------------
2026-01-23 16:57:54,192 - INFO - 雨云签到工具容器版 by fatekey ~
2026-01-23 16:57:54,192 - INFO - Github发布页: https://github.com/fatekey/Rainyun-Qiandao
2026-01-23 16:57:54,192 - INFO - --------------------------------------------------------------------------------
2026-01-23 16:57:54,192 - INFO - 项目为二次开发青龙脚本化运行
2026-01-23 16:57:54,192 - INFO - 本项目基于上述项目开发
2026-01-23 16:57:54,192 - INFO - 本项目仅作为学习参考,请勿用于其他用途
2026-01-23 16:57:54,192 - INFO - --------------------------------------------------------------------------------
2026-01-23 16:57:54,192 - INFO - ✅ 成功解析3个账号
2026-01-23 16:57:54,192 - INFO -
================= 处理第1个账号 ==================
2026-01-23 16:57:54,192 - INFO -
========== 开始处理账号:TACGN ==========
2026-01-23 16:57:54,192 - INFO - ⏳ 随机延时 0 分钟 35 秒
2026-01-23 16:58:29,794 - INFO - ✅ Selenium驱动初始化成功,路径:/usr/bin/chromedriver
2026-01-23 16:58:29,809 - INFO - ✅ 已注入stealth.min.js反检测脚本
2026-01-23 16:58:29,809 - INFO - ⏳ 发起登录请求
2026-01-23 16:58:29,810 - INFO - 🌐 访问雨云登录页
2026-01-23 16:58:31,258 - INFO - 页面标题:登录 | 雨云
2026-01-23 16:58:31,258 - INFO - ⏳ 等待登录表单元素加载...
2026-01-23 16:58:31,376 - INFO - 📝 输入账号密码
2026-01-23 16:58:31,775 - INFO - ⏳ 正在登录中,耗时较长请稍等……
2026-01-23 16:58:54,899 - INFO - ✅ 未触发登录验证码
2026-01-23 16:58:59,906 - INFO - 当前页面: https://app.rainyun.com/dashboard
2026-01-23 16:58:59,910 - INFO - 页面标题: 总览 | 雨云
2026-01-23 16:58:59,940 - INFO - ✅ 账号登录成功:TACGN
2026-01-23 16:58:59,940 - INFO - 🌐 访问赚取积分页
2026-01-23 16:59:00,958 - INFO - 当前页面: https://app.rainyun.com/account/reward/earn
2026-01-23 16:59:00,962 - INFO - 页面标题: 赚取积分 | 雨云
2026-01-23 16:59:00,962 - INFO - 🔍 查找每日签到按钮
2026-01-23 16:59:01,016 - INFO - 📌 签到状态:已完成,无需重复签到
2026-01-23 16:59:01,047 - INFO - 💰 当前积分:700(约0.35元)
2026-01-23 16:59:01,131 - INFO - ✅ 账号TACGN浏览器已关闭
2026-01-23 16:59:01,131 - INFO - ✅ 临时文件清理完成
2026-01-23 16:59:01,131 - INFO -
========== 账号TACGN处理完成 ==========
2026-01-23 16:59:05,996 - INFO -
================= 处理第2个账号 ==================
2026-01-23 16:59:05,996 - INFO -
========== 开始处理账号:ACGN_T ==========
2026-01-23 16:59:05,996 - INFO - ⏳ 随机延时 4 分钟 3 秒
2026-01-23 17:03:09,549 - INFO - ✅ Selenium驱动初始化成功,路径:/usr/bin/chromedriver
2026-01-23 17:03:09,564 - INFO - ✅ 已注入stealth.min.js反检测脚本
2026-01-23 17:03:09,564 - INFO - ⏳ 发起登录请求
2026-01-23 17:03:09,564 - INFO - 🌐 访问雨云登录页
2026-01-23 17:03:11,015 - INFO - 页面标题:登录 | 雨云
2026-01-23 17:03:11,016 - INFO - ⏳ 等待登录表单元素加载...
2026-01-23 17:03:11,155 - INFO - 📝 输入账号密码
2026-01-23 17:03:11,584 - INFO - ⏳ 正在登录中,耗时较长请稍等……
2026-01-23 17:03:34,692 - INFO - ✅ 未触发登录验证码
2026-01-23 17:03:39,699 - INFO - 当前页面: https://app.rainyun.com/dashboard
2026-01-23 17:03:39,703 - INFO - 页面标题: 总览 | 雨云
2026-01-23 17:03:39,733 - INFO - ✅ 账号登录成功:ACGN_T
2026-01-23 17:03:39,733 - INFO - 🌐 访问赚取积分页
2026-01-23 17:03:40,777 - INFO - 当前页面: https://app.rainyun.com/account/reward/earn
2026-01-23 17:03:40,782 - INFO - 页面标题: 赚取积分 | 雨云
2026-01-23 17:03:40,783 - INFO - 🔍 查找每日签到按钮
2026-01-23 17:03:40,858 - INFO - 📌 签到状态:已完成,无需重复签到
2026-01-23 17:03:40,881 - INFO - 💰 当前积分:4684(约2.34元)
2026-01-23 17:03:40,966 - INFO - ✅ 账号ACGN_T浏览器已关闭
2026-01-23 17:03:40,966 - INFO - ✅ 临时文件清理完成
2026-01-23 17:03:40,966 - INFO -
========== 账号ACGN_T处理完成 ==========
2026-01-23 17:03:43,883 - INFO -
================= 处理第3个账号 ==================
2026-01-23 17:03:43,883 - INFO -
========== 开始处理账号:ACGN ==========
2026-01-23 17:03:43,883 - INFO - ⏳ 随机延时 4 分钟 50 秒
2026-01-23 17:08:34,684 - INFO - ✅ Selenium驱动初始化成功,路径:/usr/bin/chromedriver
2026-01-23 17:08:34,698 - INFO - ✅ 已注入stealth.min.js反检测脚本
2026-01-23 17:08:34,699 - INFO - ⏳ 发起登录请求
2026-01-23 17:08:34,699 - INFO - 🌐 访问雨云登录页
2026-01-23 17:08:36,143 - INFO - 页面标题:登录 | 雨云
2026-01-23 17:08:36,143 - INFO - ⏳ 等待登录表单元素加载...
2026-01-23 17:08:36,285 - INFO - 📝 输入账号密码
2026-01-23 17:08:36,657 - INFO - ⏳ 正在登录中,耗时较长请稍等……
2026-01-23 17:08:59,802 - INFO - ✅ 未触发登录验证码
2026-01-23 17:09:04,809 - INFO - 当前页面: https://app.rainyun.com/dashboard
2026-01-23 17:09:04,813 - INFO - 页面标题: 总览 | 雨云
2026-01-23 17:09:04,842 - INFO - ✅ 账号登录成功:ACGN
2026-01-23 17:09:04,842 - INFO - 🌐 访问赚取积分页
2026-01-23 17:09:05,841 - INFO - 当前页面: https://app.rainyun.com/account/reward/earn
2026-01-23 17:09:05,871 - INFO - 页面标题: 赚取积分 | 雨云
2026-01-23 17:09:05,871 - INFO - 🔍 查找每日签到按钮
2026-01-23 17:09:05,940 - INFO - 📌 签到状态:领取奖励,开始领取
2026-01-23 17:09:06,072 - INFO - ⚠️ 触发签到验证码
2026-01-23 17:09:06,253 - INFO - 🔄 验证码处理第1次尝试(最大10次)
2026-01-23 17:09:06,810 - INFO - 开始下载验证码图片(1):https://turing.captcha.qcloud.com/cap_union_new_getcapbysig?img_index=1&image=02680900003d283800000015123b75d53fed&sess=s0HtD0kpY6pcWGI6FzFpt6ZiszTr2EhH-VfdHwPwxIdqv34Z-7I44K0-_RhKCQ_D1pczn56AhHTy7TzWXqVjayAnecALMlUWYf152tXUBM_URxYIPDxvoDD7jXbk7mwSIeJKDAUtmTTnuuaRcoqdw3DlBpEXv3Xc4RbCewuRGJZUAkZPrzzB8njktvXIOPrqAhs4UafKm96GgAUPJExW9_2PkkRGBKSTS43H1uLzB9el3g70xLMDYSd2TywoxM5Ps2idtfMPBn_aMw93gVXYLGwpX0Iztn4QG1vFv9VJj6NgCvOU2YSfCmTrGyEXxdzPnGglGJAKJFB0FfuxP4bM3-0O4DQt4l-5NsT52KR_8WcG7rvohxQXZy1sRw9MY84c31oFqKfUyPsa49v1VdtmranaOtiaDLX6SjgI6rJPvt2_kSelSHRNUWtA**
2026-01-23 17:09:07,077 - INFO - 开始下载验证码图片(2):https://turing.captcha.qcloud.com/cap_union_new_getcapbysig?img_index=0&image=02680900003d283800000015123b75d53fed&sess=s0HtD0kpY6pcWGI6FzFpt6ZiszTr2EhH-VfdHwPwxIdqv34Z-7I44K0-_RhKCQ_D1pczn56AhHTy7TzWXqVjayAnecALMlUWYf152tXUBM_URxYIPDxvoDD7jXbk7mwSIeJKDAUtmTTnuuaRcoqdw3DlBpEXv3Xc4RbCewuRGJZUAkZPrzzB8njktvXIOPrqAhs4UafKm96GgAUPJExW9_2PkkRGBKSTS43H1uLzB9el3g70xLMDYSd2TywoxM5Ps2idtfMPBn_aMw93gVXYLGwpX0Iztn4QG1vFv9VJj6NgCvOU2YSfCmTrGyEXxdzPnGglGJAKJFB0FfuxP4bM3-0O4DQt4l-5NsT52KR_8WcG7rvohxQXZy1sRw9MY84c31oFqKfUyPsa49v1VdtmranaOtiaDLX6SjgI6rJPvt2_kSelSHRNUWtA**
2026-01-23 17:09:07,375 - ERROR - ⚠️ 图案2识别率0.0000低于阈值0.4
2026-01-23 17:09:07,376 - ERROR - ❌ 验证码坐标重复,答案无效
2026-01-23 17:09:07,376 - ERROR - ❌ 验证码处理失败:验证码答案无效
2026-01-23 17:09:07,376 - ERROR - ⏳ 刷新验证码中,稍后重试……
2026-01-23 17:09:13,495 - INFO - 🔄 验证码处理第2次尝试(最大10次)
2026-01-23 17:09:13,518 - INFO - 开始下载验证码图片(1):https://turing.captcha.qcloud.com/cap_union_new_getcapbysig?img_index=1&image=0268090000946c2b0000000bb5e61fd63312&sess=s0_hrS7I5bVMRdivCWNVX_5xijZd5qBztok8b_H7bwMciiNFNIe3KMmj4IPktJO-cbs-8dl7upCI40ZosuxWWRjpXlIbF-P3ZWNoFjjg5G9dMFSybpTUgmgQO1lGEy1QSjGIghi44ITJTpCGcF4ym8wD4iU0xLCVakXfJvTvPiJbxl055LMVFM8W1FM1TtThPXpkg5h9JgYXRHols_wYhIgOI_dRxdgl3r_h-dSKI109RxypesTYee-w0m-Lw_41AM1etin4G_Iamp3lveRUaOtNV1JT4ssYxJ3DR1NZ8SEfN3yxvn9Z-_dxfifqGBxz8hkBmv4vsmx4M9imY60mxrr32HJt0K1ODVgIkzXKA0mgcq1DsSXM0AlcE765_pI_-NP9BgOPXEivjsEDpnxrS-nUFA1DJEz6urpWBwjgZN80OGgAAIs1XL1A**
2026-01-23 17:09:13,731 - INFO - 开始下载验证码图片(2):https://turing.captcha.qcloud.com/cap_union_new_getcapbysig?img_index=0&image=0268090000946c2b0000000bb5e61fd63312&sess=s0_hrS7I5bVMRdivCWNVX_5xijZd5qBztok8b_H7bwMciiNFNIe3KMmj4IPktJO-cbs-8dl7upCI40ZosuxWWRjpXlIbF-P3ZWNoFjjg5G9dMFSybpTUgmgQO1lGEy1QSjGIghi44ITJTpCGcF4ym8wD4iU0xLCVakXfJvTvPiJbxl055LMVFM8W1FM1TtThPXpkg5h9JgYXRHols_wYhIgOI_dRxdgl3r_h-dSKI109RxypesTYee-w0m-Lw_41AM1etin4G_Iamp3lveRUaOtNV1JT4ssYxJ3DR1NZ8SEfN3yxvn9Z-_dxfifqGBxz8hkBmv4vsmx4M9imY60mxrr32HJt0K1ODVgIkzXKA0mgcq1DsSXM0AlcE765_pI_-NP9BgOPXEivjsEDpnxrS-nUFA1DJEz6urpWBwjgZN80OGgAAIs1XL1A**
2026-01-23 17:09:14,002 - ERROR - ⚠️ 图案2识别率0.1515低于阈值0.4
2026-01-23 17:09:14,003 - ERROR - ❌ 验证码坐标重复,答案无效
2026-01-23 17:09:14,003 - ERROR - ❌ 验证码处理失败:验证码答案无效
2026-01-23 17:09:14,003 - ERROR - ⏳ 刷新验证码中,稍后重试……
2026-01-23 17:09:23,108 - INFO - 🔄 验证码处理第3次尝试(最大10次)
2026-01-23 17:09:23,131 - INFO - 开始下载验证码图片(1):https://turing.captcha.qcloud.com/cap_union_new_getcapbysig?img_index=1&image=0268090000f13d2300000015123b75d53f2f&sess=s02qPcN6ye2H2TPQfQ9ghy_0L3jB722YFRMCmx-rWnjm4UgxUo3F4WLoUzz5JczVgNJMtQwRWLFRo4OvXls1zjaajvPXch4RMoo6YZOavScFvdGaB-9B-ecxWvfcPx7ZTEb03-5MTmG-P2LipAwhLGAYKO0JOK6Rb6z3KYkAy9pxHIXYP9FaLlwdRvLsEDbqKWJZKCP4IHJ9mav4XH2EoTFfWGYMR-sA53gKcavXkSbzg2J_3ntSL6rszaLREZi9ZCSn1bPIDt16NYUXhHhlPFCJmBzIh41fG-nFTtpB-A8_i_vPaNo3mwlxJ9KojhSP37q7CfeASWq8-DTtI3OnT-mZbyVzoDHvBgQOiiQu5o0_VxQtxzWD9vNmVbErvVsP1VxQEVv0GCFywapI0H-R2DimaJI87vvVkVIzVce2MZQJ_lxWICubZ-RA**
2026-01-23 17:09:23,352 - INFO - 开始下载验证码图片(2):https://turing.captcha.qcloud.com/cap_union_new_getcapbysig?img_index=0&image=0268090000f13d2300000015123b75d53f2f&sess=s02qPcN6ye2H2TPQfQ9ghy_0L3jB722YFRMCmx-rWnjm4UgxUo3F4WLoUzz5JczVgNJMtQwRWLFRo4OvXls1zjaajvPXch4RMoo6YZOavScFvdGaB-9B-ecxWvfcPx7ZTEb03-5MTmG-P2LipAwhLGAYKO0JOK6Rb6z3KYkAy9pxHIXYP9FaLlwdRvLsEDbqKWJZKCP4IHJ9mav4XH2EoTFfWGYMR-sA53gKcavXkSbzg2J_3ntSL6rszaLREZi9ZCSn1bPIDt16NYUXhHhlPFCJmBzIh41fG-nFTtpB-A8_i_vPaNo3mwlxJ9KojhSP37q7CfeASWq8-DTtI3OnT-mZbyVzoDHvBgQOiiQu5o0_VxQtxzWD9vNmVbErvVsP1VxQEVv0GCFywapI0H-R2DimaJI87vvVkVIzVce2MZQJ_lxWICubZ-RA**
2026-01-23 17:09:23,677 - INFO - 🎯 图案 1 坐标(37,277),匹配率:0.6552
2026-01-23 17:09:23,677 - INFO - 🎯 图案 2 坐标(598,114),匹配率:0.5641
2026-01-23 17:09:23,677 - INFO - 🎯 图案 3 坐标(437,197),匹配率:0.6207
2026-01-23 17:09:26,517 - INFO - 📤 提交验证码
2026-01-23 17:09:31,670 - INFO - ✅ 验证码验证通过
2026-01-23 17:09:36,676 - INFO - ✅ 签到奖励领取成功
2026-01-23 17:09:36,699 - INFO - 💰 当前积分:700(约0.35元)
2026-01-23 17:09:36,788 - INFO - ✅ 账号ACGN浏览器已关闭
2026-01-23 17:09:36,788 - INFO - ✅ 临时文件清理完成
2026-01-23 17:09:36,788 - INFO -
========== 账号ACGN处理完成 ==========
2026-01-23 17:09:40,305 - INFO -
🎉 所有账号处理完成!
## 执行结束... 2026-01-23 17:09:40 耗时 707 秒
二、前置条件
青龙面板:我是直接用的 1panel 应用商店里的青龙面板
雨云账号密码:自个注册去
三、准备工作
青龙面板安装依赖:安装不上的,不会的请自行搜索教程或者请教 AI 了哦
—— NodeJs:chromium
—— Python3:selenium
—— Linux:chromium-driver
青龙面板配置环境变量:想设置多少自己设置就好了
—— RAINYUN_ACCOUNT
—— [[“账号 1”,“账号 1 密码”],[“账号 2”,“账号 2 密码”]]
四、创建文件
同目录下创建以下两个文件
stealth.min.js
stealth.min.js.txt
rainyun.py
rainyun.py.txt
删掉 [.txt] 后缀上传就好了,顺便贴出 rainyun.py 的代码如下
import json
import logging
import os
import random
import re
import sys
import time
from typing import Tuple, Optional, List
import cv2
import ddddocr
import requests
from selenium import webdriver
from selenium.common import TimeoutException, WebDriverException, NoSuchElementException
from selenium.webdriver import ActionChains
from selenium.webdriver.chrome.options import Options
from selenium.webdriver.chrome.service import Service
from selenium.webdriver.chrome.webdriver import WebDriver
from selenium.webdriver.common.by import By
from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.support.wait import WebDriverWait
# ===================== 青龙面板专属配置(常量不抽离) =====================
CONFIG = {
"timeout": 20, # 青龙面板网络可能不稳定,延长超时时间
"max_delay": 5, # 最大随机等待分钟数
"max_captcha_retry": 10, # 验证码最大重试次数(防止递归栈溢出)
"similarity_threshold": 0.4, # 降低阈值提升识别率
"script_path": os.path.dirname(os.path.abspath(__file__)), # 青龙脚本所在目录
"temp_path": os.path.join(os.path.dirname(os.path.abspath(__file__)), "temp"), # 临时文件路径
"rainyun_login_url": "https://app.rainyun.com/auth/login",
"rainyun_earn_url": "https://app.rainyun.com/account/reward/earn"
}
# 全局日志对象(仅日志全局化,核心变量均函数内初始化)
logger = logging.getLogger(__name__)
def init_logger():
"""初始化青龙面板日志格式(增强版)"""
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(levelname)s - %(message)s',
handlers=[logging.StreamHandler(sys.stdout)]
)
# 打印项目信息
logger.info("-"*80)
logger.info("雨云签到工具 by SerendipityR ~")
logger.info("Github发布页: https://github.com/SerendipityR-2022/Rainyun-Qiandao")
logger.info("-"*80)
logger.info("雨云签到工具容器版 by fatekey ~")
logger.info("Github发布页: https://github.com/fatekey/Rainyun-Qiandao")
logger.info("-"*80)
logger.info(" 项目为二次开发青龙脚本化运行")
logger.info(" 本项目基于上述项目开发")
logger.info(" 本项目仅作为学习参考,请勿用于其他用途")
logger.info("-"*80)
def init_selenium() -> WebDriver:
"""初始化青龙面板专用Selenium驱动(每次调用新建实例,避免缓存污染)"""
ops = Options()
# 容器环境必需配置
ops.add_argument("--no-sandbox")
ops.add_argument("--disable-dev-shm-usage")
ops.add_argument("--headless=new")
ops.add_argument("--disable-gpu")
ops.add_argument("--window-size=1920,1080")
ops.add_argument("--user-agent=Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36")
# 反爬配置
ops.add_experimental_option("excludeSwitches", ["enable-automation"])
ops.add_experimental_option('useAutomationExtension', False)
ops.add_argument("--disable-blink-features=AutomationControlled")
# 青龙面板固定驱动路径校验
driver_path = "/usr/bin/chromedriver"
if not os.path.exists(driver_path) or not os.access(driver_path, os.X_OK):
raise FileNotFoundError(
f"青龙面板未安装chromium-driver!\n"
f"请在青龙终端执行:apt update && apt install -y chromium-driver"
)
try:
service = Service(executable_path=driver_path)
driver = webdriver.Chrome(service=service, options=ops)
# 清空缓存(双重保障)
driver.delete_all_cookies()
logger.info(f"✅ Selenium驱动初始化成功,路径:{driver_path}")
return driver
except WebDriverException as e:
logger.error(f"❌ 驱动启动失败:{str(e)}")
raise
def check_stealth_js() -> str:
"""检查青龙脚本目录下的stealth.min.js"""
js_path = os.path.join(CONFIG["script_path"], "stealth.min.js")
if not os.path.exists(js_path):
logger.error(f"❌ 未找到stealth.min.js!请将文件上传到青龙脚本目录:{CONFIG['script_path']}")
logger.info("📥 下载地址:https://raw.githubusercontent.com/berstend/puppeteer-extra/master/packages/puppeteer-extra-plugin-stealth/evasions/stealth.min.js")
sys.exit(1)
return js_path
def inject_stealth_js(driver: WebDriver):
"""注入反检测脚本(传入driver实例,解耦全局变量)"""
js_path = check_stealth_js()
with open(js_path, "r", encoding="utf-8") as f:
js = f.read()
driver.execute_cdp_cmd("Page.addScriptToEvaluateOnNewDocument", {"source": js})
logger.info("✅ 已注入stealth.min.js反检测脚本")
def download_image(url: str, filename: str, img_index: int) -> bool:
"""下载图片(带URL日志+青龙面板加请求头防拦截)"""
os.makedirs(CONFIG["temp_path"], exist_ok=True)
try:
logger.info(f"开始下载验证码图片({img_index}):{url}")
headers = {
"User-Agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
"Referer": "https://app.rainyun.com/"
}
response = requests.get(url, headers=headers, timeout=10)
response.raise_for_status()
path = os.path.join(CONFIG["temp_path"], filename)
with open(path, "wb") as f:
f.write(response.content)
return True
except Exception as e:
logger.error(f"❌ 下载图片失败 {url}:{str(e)}")
return False
# ========== 工具函数(精简+健壮性优化) ==========
def get_url_from_style(style: str) -> Optional[str]:
"""从style属性提取URL"""
try:
match = re.search(r'url\(["\']?(.*?)["\']?\)', style)
return match.group(1) if match else None
except Exception:
return None
def get_width_from_style(style: str) -> str:
"""从style属性提取宽度"""
match = re.search(r'width:\s*([\d.]+)px', style)
return match.group(1) if match else "300"
def get_height_from_style(style: str) -> str:
"""从style属性提取高度"""
match = re.search(r'height:\s*([\d.]+)px', style)
return match.group(1) if match else "150"
def compute_similarity(img1_path: str, img2_path: str) -> Tuple[float, int]:
"""青龙面板适配:SIFT不可用时用ORB(增加异常兜底)"""
try:
img1 = cv2.imread(img1_path, cv2.IMREAD_GRAYSCALE)
img2 = cv2.imread(img2_path, cv2.IMREAD_GRAYSCALE)
if img1 is None or img2 is None:
logger.warning(f"❌ 图片读取失败:{img1_path} 或 {img2_path}")
return 0.0, 0
# 优先SIFT,降级ORB
try:
sift = cv2.SIFT_create()
norm = cv2.NORM_L2
except AttributeError:
sift = cv2.ORB_create()
norm = cv2.NORM_HAMMING
logger.warning("⚠️ SIFT不可用,使用ORB匹配")
kp1, des1 = sift.detectAndCompute(img1, None)
kp2, des2 = sift.detectAndCompute(img2, None)
if des1 is None or des2 is None:
return 0.0, 0
bf = cv2.BFMatcher(norm, crossCheck=False)
matches = bf.knnMatch(des1, des2, k=2)
good = [m for m, n in matches if m.distance < 0.8 * n.distance]
similarity = len(good) / len(matches) if matches else 0.0
return similarity, len(good)
except Exception as e:
logger.error(f"❌ 相似度计算失败:{str(e)}")
return 0.0, 0
def download_captcha_img(driver: WebDriver, wait: WebDriverWait) -> bool:
"""下载并分割验证码图片(解耦全局变量)"""
try:
# 清空旧临时文件
if os.path.exists(CONFIG["temp_path"]):
for f in os.listdir(CONFIG["temp_path"]):
os.remove(os.path.join(CONFIG["temp_path"], f))
# 定位验证码背景图
slideBg = wait.until(EC.visibility_of_element_located((By.XPATH, '//*[@id="slideBg"]')))
img1_url = get_url_from_style(slideBg.get_attribute("style"))
if not img1_url or not download_image(img1_url, "captcha.jpg", 1):
return False
# 定位验证码碎片图
sprite = wait.until(EC.visibility_of_element_located((By.XPATH, '//*[@id="instruction"]/div/img')))
if not download_image(sprite.get_attribute("src"), "sprite.jpg", 2):
return False
# 分割碎片图
raw = cv2.imread(os.path.join(CONFIG["temp_path"], "sprite.jpg"))
if raw is None:
logger.error("❌ 验证码碎片图读取失败")
return False
w = raw.shape[1]
for i in range(3):
cv2.imwrite(
os.path.join(CONFIG["temp_path"], f"sprite_{i+1}.jpg"),
raw[:, w//3*i : w//3*(i+1)]
)
return True
except TimeoutException:
logger.error("❌ 验证码图片加载超时")
return False
except Exception as e:
logger.error(f"❌ 验证码图片处理失败:{str(e)}")
return False
def check_answer(result: dict) -> bool:
"""检查验证码答案有效性(带识别率日志)"""
valid = True
for i in range(3):
sim = float(result.get(f"sprite_{i+1}.similarity", 0))
if sim < CONFIG["similarity_threshold"]:
logger.error(f"⚠️ 图案{i+1}识别率{sim:.4f}低于阈值{CONFIG['similarity_threshold']}")
valid = False
break
# 检查坐标唯一性
positions = [result.get(f"sprite_{i+1}.position") for i in range(3)]
if len(set(positions)) != 3:
logger.error("❌ 验证码坐标重复,答案无效")
valid = False
return valid
def process_captcha(driver: WebDriver, wait: WebDriverWait) -> bool:
"""处理验证码(改递归为循环,提升健壮性,解耦全局变量)"""
captcha_retry_count = 0
ocr = ddddocr.DdddOcr(ocr=True, show_ad=False)
det = ddddocr.DdddOcr(det=True, show_ad=False)
while captcha_retry_count < CONFIG["max_captcha_retry"]:
captcha_retry_count += 1
logger.info(f"🔄 验证码处理第{captcha_retry_count}次尝试(最大{CONFIG['max_captcha_retry']}次)")
try:
# 下载验证码图片
if not download_captcha_img(driver, wait):
raise Exception("验证码图片下载失败")
# 校验验证码有效性
valid = True
for i in range(3):
sprite_path = os.path.join(CONFIG["temp_path"], f"sprite_{i+1}.jpg")
with open(sprite_path, "rb") as f:
if ocr.classification(f.read()) in ["0", "1"]:
valid = False
break
if not valid:
raise Exception("验证码碎片无效")
# 识别验证码
captcha = cv2.imread(os.path.join(CONFIG["temp_path"], "captcha.jpg"))
if captcha is None:
raise Exception("验证码背景图读取失败")
with open(os.path.join(CONFIG["temp_path"], "captcha.jpg"), "rb") as f:
bboxes = det.detection(f.read())
if not bboxes:
raise Exception("未检测到验证码图案")
# 匹配碎片与背景图
result = {}
for i, (x1, y1, x2, y2) in enumerate(bboxes):
# 裁剪背景图中的图案
cv2.imwrite(os.path.join(CONFIG["temp_path"], f"spec_{i+1}.jpg"), captcha[y1:y2, x1:x2])
# 计算与每个碎片的相似度
for j in range(3):
sim, _ = compute_similarity(
os.path.join(CONFIG["temp_path"], f"sprite_{j+1}.jpg"),
os.path.join(CONFIG["temp_path"], f"spec_{i+1}.jpg")
)
key_sim = f"sprite_{j+1}.similarity"
key_pos = f"sprite_{j+1}.position"
if sim > float(result.get(key_sim, 0)):
result[key_sim] = sim
result[key_pos] = f"{int((x1+x2)/2)},{int((y1+y2)/2)}"
# 校验答案
if not check_answer(result):
raise Exception("验证码答案无效")
# 打印匹配结果
for i in range(3):
pos = result[f"sprite_{i+1}.position"]
sim = result[f"sprite_{i+1}.similarity"]
x, y = pos.split(",")
logger.info(f"🎯 图案 {i+1} 坐标({x},{y}),匹配率:{sim:.4f}")
# 点击验证码图案
slideBg = wait.until(EC.visibility_of_element_located((By.XPATH, '//*[@id="slideBg"]')))
style = slideBg.get_attribute("style")
width, height = float(get_width_from_style(style)), float(get_height_from_style(style))
width_raw, height_raw = captcha.shape[1], captcha.shape[0]
for i in range(3):
pos = result[f"sprite_{i+1}.position"]
x, y = map(int, pos.split(","))
# 计算实际点击坐标(适配页面缩放)
final_x = int(-width/2 + x/width_raw * width) + random.randint(-1, 1)
final_y = int(-height/2 + y/height_raw * height) + random.randint(-1, 1)
ActionChains(driver).move_to_element_with_offset(slideBg, final_x, final_y).click().perform()
time.sleep(random.uniform(0.5, 1))
# 提交验证码
logger.info("📤 提交验证码")
confirm = wait.until(EC.element_to_be_clickable((By.XPATH, '//*[@id="tcStatus"]/div[2]/div[2]/div/div')))
confirm.click()
time.sleep(5)
# 校验验证码结果
tc_operation = wait.until(EC.visibility_of_element_located((By.XPATH, '//*[@id="tcOperation"]')))
if tc_operation.get_attribute("class") == "tc-opera pointer show-success":
logger.info("✅ 验证码验证通过")
return True
else:
raise Exception("验证码验证失败")
except Exception as e:
logger.error(f"❌ 验证码处理失败:{str(e)}")
# 刷新验证码重试
try:
logger.error("⏳ 刷新验证码中,稍后重试……")
reload = driver.find_element(By.XPATH, '//*[@id="reload"]')
time.sleep(3)
reload.click()
time.sleep(min(3 * (2 ** (captcha_retry_count - 1)), 30)) # 指数退避重试间隔,上限30秒
except NoSuchElementException:
logger.error("❌ 验证码刷新按钮未找到,重试失败")
return False
logger.error(f"❌ 验证码重试{CONFIG['max_captcha_retry']}次仍失败,放弃")
return False
def clean_temp():
"""清理青龙面板临时文件(增加容错)"""
try:
if os.path.exists(CONFIG["temp_path"]):
for f in os.listdir(CONFIG["temp_path"]):
file_path = os.path.join(CONFIG["temp_path"], f)
try:
os.remove(file_path)
except Exception as e:
logger.warning(f"⚠️ 删除临时文件{file_path}失败:{str(e)}")
os.rmdir(CONFIG["temp_path"])
logger.info("✅ 临时文件清理完成")
except Exception as e:
logger.warning(f"⚠️ 清理临时文件失败:{str(e)}")
def parse_accounts() -> List[List[str]]:
"""解析青龙面板RAINYUN_ACCOUNT环境变量"""
account_str = os.getenv("RAINYUN_ACCOUNT")
if not account_str:
logger.error("❌ 未配置RAINYUN_ACCOUNT环境变量!格式应为[[账号1,密码1],[账号2,密码2]]")
sys.exit(1)
try:
# 解析JSON格式的账号列表(兼容单引号/双引号)
account_str = account_str.replace("'", "\"") # 统一为双引号
accounts = json.loads(account_str)
# 校验格式
if not isinstance(accounts, list):
raise ValueError("环境变量值不是列表类型")
for idx, account in enumerate(accounts):
if not isinstance(account, list) or len(account) != 2:
raise ValueError(f"第{idx+1}个账号格式错误,应为[账号,密码]")
if not account[0] or not account[1]:
raise ValueError(f"第{idx+1}个账号/密码为空")
logger.info(f"✅ 成功解析{len(accounts)}个账号")
return accounts
except json.JSONDecodeError as e:
logger.error(f"❌ RAINYUN_ACCOUNT格式解析失败:{str(e)},请检查格式是否为合法JSON")
sys.exit(1)
except ValueError as e:
logger.error(f"❌ RAINYUN_ACCOUNT格式错误:{str(e)}")
sys.exit(1)
def sign_in_rainyun(username: str, password: str):
"""单账号签到核心逻辑(独立封装,支持多账号循环调用)"""
driver = None
try:
logger.info(f"\n========== 开始处理账号:{username} ==========")
# 随机延时(可选)
delay = random.randint(0, CONFIG["max_delay"])
delay_sec = random.randint(0, 60)
logger.info(f"⏳ 随机延时 {delay} 分钟 {delay_sec} 秒")
time.sleep(delay * 60 + delay_sec)
# 初始化Selenium(每次新建实例,清空缓存)
driver = init_selenium()
inject_stealth_js(driver)
wait = WebDriverWait(driver, CONFIG["timeout"])
# 访问登录页
logger.info("⏳ 发起登录请求")
logger.info("🌐 访问雨云登录页")
driver.get(CONFIG["rainyun_login_url"])
logger.info(f"页面标题:{driver.title}")
# 输入账号密码
logger.info("⏳ 等待登录表单元素加载...")
username_elem = wait.until(EC.visibility_of_element_located((By.NAME, "login-field")))
password_elem = wait.until(EC.visibility_of_element_located((By.NAME, "login-password")))
login_btn = wait.until(EC.element_to_be_clickable((By.XPATH, '//*[@id="app"]/div[1]/div[1]/div/div[2]/fade/div/div/span/form/button')))
logger.info("📝 输入账号密码")
username_elem.send_keys(username)
password_elem.send_keys(password)
login_btn.click()
logger.info("⏳ 正在登录中,耗时较长请稍等……")
time.sleep(3)
# 处理登录验证码
try:
wait.until(EC.visibility_of_element_located((By.ID, "tcaptcha_iframe_dy")))
logger.warning("⚠️ 触发登录验证码")
driver.switch_to.frame("tcaptcha_iframe_dy")
if not process_captcha(driver, wait):
raise Exception("登录验证码验证失败")
except TimeoutException:
logger.info("✅ 未触发登录验证码")
# 校验登录状态
time.sleep(5)
driver.switch_to.default_content()
logger.info(f"当前页面: {driver.current_url}")
logger.info(f"页面标题: {driver.title}")
if driver.current_url != "https://app.rainyun.com/dashboard":
raise Exception("登录失败!请检查账号密码或网络")
user_name = driver.find_element(By.XPATH, '//*[@id="app"]/div[1]/nav/div[1]/ul/div[6]/li/a/div/div/p').text.strip()
logger.info(f"✅ 账号登录成功:{user_name}")
# 访问签到页
logger.info("🌐 访问赚取积分页")
driver.get(CONFIG["rainyun_earn_url"])
driver.implicitly_wait(5)
logger.info(f"当前页面: {driver.current_url}")
logger.info(f"页面标题: {driver.title}")
# 查找并点击签到按钮
logger.info("🔍 查找每日签到按钮")
earn_btn_qddiv = driver.find_element(By.XPATH, '//*[@id="app"]/div[1]/div[3]/div[2]/div/div/div[2]/div[2]/div/div/div/div[1]/div')
earn_btn_qd = earn_btn_qddiv.find_element(By.XPATH, './/span[contains(text(),"每日签到")]')
status_elem = earn_btn_qd.find_element(By.XPATH, './following-sibling::span[1]')
status_text = status_elem.text.strip()
if status_text == "领取奖励":
earn_btn = status_elem.find_element(By.XPATH, './a')
logger.info(f"📌 签到状态:{status_text},开始领取")
earn_btn.click()
# 处理签到验证码
logger.info("⚠️ 触发签到验证码")
driver.switch_to.frame("tcaptcha_iframe_dy")
if not process_captcha(driver, wait):
raise Exception("签到验证码验证失败")
driver.switch_to.default_content()
# 校验签到结果
time.sleep(5)
logger.info("✅ 签到奖励领取成功")
else:
logger.info(f"📌 签到状态:{status_text},无需重复签到")
# 获取当前积分
try:
points_elem = driver.find_element(By.XPATH, '//*[@id="app"]/div[1]/div[3]/div[2]/div/div/div[2]/div[1]/div[1]/div/p/div/h3')
current_points = int(''.join(re.findall(r'\d+', points_elem.text)))
logger.info(f"💰 当前积分:{current_points}(约{current_points/2000:.2f}元)")
except Exception as e:
logger.warning(f"⚠️ 积分获取失败:{str(e)}")
except Exception as e:
logger.error(f"❌ 账号{username}处理失败:{str(e)}", exc_info=True)
finally:
# 关闭浏览器,彻底清空缓存
if driver:
try:
driver.quit()
logger.info(f"✅ 账号{username}浏览器已关闭")
except Exception as e:
logger.warning(f"⚠️ 关闭浏览器失败:{str(e)}")
# 清理临时文件
clean_temp()
logger.info(f"\n========== 账号{username}处理完成 ==========\n")
def main():
"""主函数:解析多账号,依次执行签到"""
init_logger()
# 解析账号列表
accounts = parse_accounts()
# 依次处理每个账号
for idx, (username, password) in enumerate(accounts, 1):
logger.info(f"\n================= 处理第{idx}个账号 ==================")
sign_in_rainyun(username, password)
# 账号间间隔(可选)
time.sleep(random.uniform(2, 5))
logger.info("\n🎉 所有账号处理完成!")
if __name__ == "__main__":
main()
五、最后
后面的设置定时任务之类的就懒得写啦,顺便我也懒得写通知之类的了,感谢前人的开发让我站于巨人肩膀上,要是有后来者优化了就更好了
哦,对了,懒得上传 github 了,就丢 linux.do 里好了















