inti
This commit is contained in:
@@ -0,0 +1,2 @@
|
||||
.venv
|
||||
*/__pycache__
|
||||
@@ -0,0 +1,126 @@
|
||||
# 斗鱼自动登录工具 - Python版
|
||||
|
||||
全自动登录斗鱼账号,获取Cookie。
|
||||
|
||||
## 功能特性
|
||||
|
||||
- ✅ 自动过极验v3滑块验证
|
||||
- ✅ 自动获取邮箱验证码(IMAP)
|
||||
- ✅ 批量账号登录
|
||||
- ✅ Cookie持久化存储
|
||||
- ✅ 代理支持
|
||||
|
||||
## 项目结构
|
||||
|
||||
```
|
||||
douyu_login_py/
|
||||
├── main.py # 主入口
|
||||
├── config.yaml # 配置文件
|
||||
├── requirements.txt # 依赖
|
||||
├── douyu/ # 斗鱼登录模块
|
||||
│ ├── config.py # 配置管理
|
||||
│ ├── crypto.py # 加密工具
|
||||
│ ├── email_verifier.py # 邮箱验证
|
||||
│ └── login.py # 登录核心
|
||||
├── geetest/ # 极验滑块破解(复用geetest-v3-silde-crack)
|
||||
│ ├── solver.py
|
||||
│ ├── network.py
|
||||
│ ├── crypto.py
|
||||
│ ├── imaging.py
|
||||
│ └── trajectory.py
|
||||
├── utils/ # 工具模块
|
||||
│ ├── logger.py
|
||||
│ └── helpers.py
|
||||
└── data/
|
||||
├── accounts.json # 账号配置
|
||||
└── cookies/ # Cookie存储
|
||||
```
|
||||
|
||||
## 安装依赖
|
||||
|
||||
```bash
|
||||
pip install -r requirements.txt
|
||||
```
|
||||
|
||||
## 配置
|
||||
|
||||
编辑 `config.yaml`:
|
||||
|
||||
```yaml
|
||||
accounts:
|
||||
- username: "your_username"
|
||||
password: "your_password"
|
||||
email: "your_email@bdhg.xyz"
|
||||
email_password: "your_email_password"
|
||||
email_imap_server: "mail.bdhg.xyz"
|
||||
email_imap_port: 993
|
||||
```
|
||||
|
||||
## 使用方法
|
||||
|
||||
### 单账号登录
|
||||
|
||||
```bash
|
||||
# 登录第一个账号
|
||||
python main.py
|
||||
|
||||
# 登录指定索引的账号
|
||||
python main.py -i 0
|
||||
```
|
||||
|
||||
### 批量登录
|
||||
|
||||
```bash
|
||||
python main.py --batch
|
||||
```
|
||||
|
||||
### 详细日志
|
||||
|
||||
```bash
|
||||
python main.py -v
|
||||
```
|
||||
|
||||
## 登录流程
|
||||
|
||||
```
|
||||
1️⃣ 第一次登录 → 获取极验参数
|
||||
2️⃣ 极验滑块验证 → 自动识别缺口位置
|
||||
3️⃣ 第二次登录 → 获取邮箱验证code
|
||||
4️⃣ 发送邮箱验证邮件
|
||||
5️⃣ IMAP获取验证码
|
||||
6️⃣ 提交验证码
|
||||
7️⃣ 完成登录获取Cookie
|
||||
```
|
||||
|
||||
## Cookie使用
|
||||
|
||||
登录成功后,Cookie会保存到 `data/cookies/` 目录。
|
||||
|
||||
可以使用以下方式读取Cookie:
|
||||
|
||||
```python
|
||||
from utils.helpers import load_cookie
|
||||
|
||||
cookie = load_cookie("your_username")
|
||||
print(cookie)
|
||||
```
|
||||
|
||||
## 极验滑块破解
|
||||
|
||||
本项目复用了 `geetest-v3-silde-crack` 的滑块破解方案,包含:
|
||||
|
||||
- RSA/AES加密
|
||||
- 轨迹生成
|
||||
- 图像识别(OpenCV)
|
||||
- 性能数据伪造
|
||||
|
||||
## 注意事项
|
||||
|
||||
1. 请确保邮箱支持IMAP协议
|
||||
2. 邮箱需要开启IMAP访问权限
|
||||
3. 部分邮箱需要使用应用专用密码
|
||||
4. 建议使用代理避免IP限制
|
||||
|
||||
## 许可证
|
||||
|
||||
MIT License
|
||||
Binary file not shown.
+24
@@ -0,0 +1,24 @@
|
||||
# 斗鱼自动登录配置
|
||||
|
||||
# 账号配置
|
||||
accounts:
|
||||
- username: "9006787894"
|
||||
password: "a778899"
|
||||
email: "jtmjcm@bdhg.xyz"
|
||||
email_password: "www123"
|
||||
email_imap_server: "mail.bdhg.xyz"
|
||||
email_imap_port: 993
|
||||
|
||||
# 代理配置(可选)
|
||||
proxy:
|
||||
enabled: false
|
||||
http: ""
|
||||
https: ""
|
||||
|
||||
# 日志配置
|
||||
log:
|
||||
level: "DEBUG"
|
||||
file: "logs/douyu_login.log"
|
||||
|
||||
# Cookie存储
|
||||
cookie_dir: "data/cookies"
|
||||
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"accounts": [
|
||||
{
|
||||
"username": "your_username",
|
||||
"password": "your_password",
|
||||
"email": "your_email@bdhg.xyz",
|
||||
"email_password": "your_email_password",
|
||||
"email_imap_server": "mail.bdhg.xyz",
|
||||
"email_imap_port": 993
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
"""斗鱼登录模块"""
|
||||
|
||||
from .login import DouyuLogin
|
||||
from .email_verifier import EmailVerifier
|
||||
from .config import Config
|
||||
|
||||
__all__ = ["DouyuLogin", "EmailVerifier", "Config"]
|
||||
@@ -0,0 +1,72 @@
|
||||
"""配置管理模块"""
|
||||
|
||||
import yaml
|
||||
from pathlib import Path
|
||||
from dataclasses import dataclass
|
||||
from typing import List, Optional
|
||||
|
||||
|
||||
@dataclass
|
||||
class Account:
|
||||
"""账号配置"""
|
||||
username: str
|
||||
password: str
|
||||
email: str
|
||||
email_password: str
|
||||
email_imap_server: str
|
||||
email_imap_port: int = 993
|
||||
|
||||
|
||||
@dataclass
|
||||
class ProxyConfig:
|
||||
"""代理配置"""
|
||||
enabled: bool = False
|
||||
http: str = ""
|
||||
https: str = ""
|
||||
|
||||
|
||||
class Config:
|
||||
"""配置管理器"""
|
||||
|
||||
def __init__(self, config_path: str = "config.yaml"):
|
||||
self.config_path = Path(config_path)
|
||||
self._config = self._load_config()
|
||||
|
||||
def _load_config(self) -> dict:
|
||||
"""加载配置文件"""
|
||||
if not self.config_path.exists():
|
||||
raise FileNotFoundError(f"配置文件不存在: {self.config_path}")
|
||||
|
||||
with open(self.config_path, 'r', encoding='utf-8') as f:
|
||||
return yaml.safe_load(f)
|
||||
|
||||
def get_accounts(self) -> List[Account]:
|
||||
"""获取账号列表"""
|
||||
accounts = []
|
||||
for acc in self._config.get('accounts', []):
|
||||
accounts.append(Account(
|
||||
username=acc['username'],
|
||||
password=acc['password'],
|
||||
email=acc['email'],
|
||||
email_password=acc['email_password'],
|
||||
email_imap_server=acc.get('email_imap_server', ''),
|
||||
email_imap_port=acc.get('email_imap_port', 993),
|
||||
))
|
||||
return accounts
|
||||
|
||||
def get_proxy(self) -> ProxyConfig:
|
||||
"""获取代理配置"""
|
||||
proxy = self._config.get('proxy', {})
|
||||
return ProxyConfig(
|
||||
enabled=proxy.get('enabled', False),
|
||||
http=proxy.get('http', ''),
|
||||
https=proxy.get('https', ''),
|
||||
)
|
||||
|
||||
def get_cookie_dir(self) -> str:
|
||||
"""获取Cookie存储目录"""
|
||||
return self._config.get('cookie_dir', 'data/cookies')
|
||||
|
||||
def get_log_config(self) -> dict:
|
||||
"""获取日志配置"""
|
||||
return self._config.get('log', {'level': 'INFO', 'file': 'logs/douyu_login.log'})
|
||||
@@ -0,0 +1,63 @@
|
||||
"""斗鱼专用加密模块 - 用户名密码加密"""
|
||||
|
||||
import base64
|
||||
import hashlib
|
||||
from Crypto.PublicKey import RSA
|
||||
from Crypto.Cipher import PKCS1_v1_5
|
||||
from Crypto.Cipher import AES
|
||||
|
||||
|
||||
# 斗鱼RSA公钥(从JS中提取)
|
||||
DOUYU_RSA_PUBLIC_KEY = """-----BEGIN PUBLIC KEY-----
|
||||
MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDHfGXRkF+RiLA71KAHOFBaWGBy
|
||||
J7M6D3MDAsFHo2JMBDm2Kfj6V3GFMI7B2JQ3qGl0jCk6ILT1jQ+IFhLvLR3cXPaC
|
||||
HT5xYa0hzJpMNO3bLSuJhzY5jQNqRMWfbcV4FLB2JBaFfWcY7RWQ2pCE6jjDnMHM
|
||||
o2kz+dJoGnZM0b99VwIDAQAB
|
||||
-----END PUBLIC KEY-----"""
|
||||
|
||||
# 斗鱼AES密钥(16位)
|
||||
DOUYU_AES_KEY = "1234567890abcdef"
|
||||
|
||||
|
||||
def md5(text: str) -> str:
|
||||
"""MD5加密"""
|
||||
return hashlib.md5(text.encode('utf-8')).hexdigest()
|
||||
|
||||
|
||||
def rsa_encrypt(text: str, public_key: str = DOUYU_RSA_PUBLIC_KEY) -> str:
|
||||
"""RSA加密"""
|
||||
key = RSA.import_key(public_key)
|
||||
cipher = PKCS1_v1_5.new(key)
|
||||
encrypted = cipher.encrypt(text.encode('utf-8'))
|
||||
return base64.b64encode(encrypted).decode('utf-8')
|
||||
|
||||
|
||||
def aes_encrypt(text: str, key: str = DOUYU_AES_KEY) -> str:
|
||||
"""AES加密"""
|
||||
key_bytes = key.encode('utf-8')
|
||||
text_bytes = text.encode('utf-8')
|
||||
|
||||
# 填充到16的倍数
|
||||
padding_len = 16 - (len(text_bytes) % 16)
|
||||
text_bytes += bytes([padding_len] * padding_len)
|
||||
|
||||
cipher = AES.new(key_bytes, AES.MODE_ECB)
|
||||
encrypted = cipher.encrypt(text_bytes)
|
||||
return base64.b64encode(encrypted).decode('utf-8')
|
||||
|
||||
|
||||
def encrypt_username(username: str) -> str:
|
||||
"""加密用户名(斗鱼使用RSA加密)"""
|
||||
return rsa_encrypt(username)
|
||||
|
||||
|
||||
def encrypt_password(password: str) -> str:
|
||||
"""加密密码(斗鱼使用MD5)"""
|
||||
return md5(password)
|
||||
|
||||
|
||||
def encrypt_nickname_or_phone(text: str) -> str:
|
||||
"""加密昵称或手机号"""
|
||||
# 斗鱼使用特殊的Base64编码
|
||||
encoded = base64.b64encode(text.encode('utf-8')).decode('utf-8')
|
||||
return encoded
|
||||
@@ -0,0 +1,231 @@
|
||||
"""邮箱验证模块 - IMAP获取验证码"""
|
||||
|
||||
import imaplib
|
||||
import email
|
||||
import re
|
||||
import time
|
||||
from datetime import datetime, timedelta
|
||||
from email.header import decode_header
|
||||
from typing import Optional
|
||||
from loguru import logger
|
||||
|
||||
|
||||
class EmailVerifier:
|
||||
"""邮箱验证器"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
imap_server: str,
|
||||
imap_port: int,
|
||||
username: str,
|
||||
password: str,
|
||||
timeout: float = 12,
|
||||
):
|
||||
self.imap_server = imap_server
|
||||
self.imap_port = imap_port
|
||||
self.username = username
|
||||
self.password = password
|
||||
self.timeout = timeout
|
||||
self._connection: Optional[imaplib.IMAP4_SSL] = None
|
||||
|
||||
def connect(self) -> None:
|
||||
"""连接IMAP服务器"""
|
||||
try:
|
||||
logger.info(f"连接IMAP服务器: {self.imap_server}:{self.imap_port}")
|
||||
self._connection = imaplib.IMAP4_SSL(
|
||||
self.imap_server,
|
||||
self.imap_port,
|
||||
timeout=self.timeout,
|
||||
)
|
||||
self._connection.login(self.username, self.password)
|
||||
logger.info("IMAP连接成功")
|
||||
except Exception as e:
|
||||
logger.error(f"IMAP连接失败: {e}")
|
||||
raise
|
||||
|
||||
def disconnect(self) -> None:
|
||||
"""断开IMAP连接"""
|
||||
if self._connection:
|
||||
try:
|
||||
self._connection.logout()
|
||||
except:
|
||||
pass
|
||||
self._connection = None
|
||||
|
||||
def get_verification_code(self, max_wait: int = 60, interval: int = 2) -> str:
|
||||
"""
|
||||
获取斗鱼验证码
|
||||
|
||||
Args:
|
||||
max_wait: 最大等待时间(秒)
|
||||
interval: 轮询间隔(秒)
|
||||
|
||||
Returns:
|
||||
6位验证码
|
||||
"""
|
||||
logger.info(f"等待斗鱼验证码邮件,最大等待 {max_wait} 秒...")
|
||||
|
||||
self.connect()
|
||||
|
||||
try:
|
||||
start_time = time.time()
|
||||
while time.time() - start_time < max_wait:
|
||||
code = self._fetch_latest_code()
|
||||
if code:
|
||||
logger.success(f"获取到验证码: {code}")
|
||||
return code
|
||||
|
||||
logger.debug("未找到验证码,等待中...")
|
||||
time.sleep(interval)
|
||||
|
||||
raise TimeoutError("等待验证码超时")
|
||||
finally:
|
||||
self.disconnect()
|
||||
|
||||
def _fetch_latest_code(self) -> Optional[str]:
|
||||
"""从IMAP获取最新验证码"""
|
||||
try:
|
||||
self._connection.select('INBOX')
|
||||
|
||||
# 搜索最近5分钟的邮件
|
||||
since = (datetime.now() - timedelta(minutes=5)).strftime("%d-%b-%Y")
|
||||
status, messages = self._connection.search(
|
||||
None,
|
||||
f'(OR (FROM "douyu") (FROM "斗鱼") (SUBJECT "验证码")) SINCE {since}'
|
||||
)
|
||||
|
||||
if status != 'OK' or not messages[0]:
|
||||
return None
|
||||
|
||||
# 获取最新的一封邮件
|
||||
latest_id = messages[0].split()[-1]
|
||||
status, msg_data = self._connection.fetch(latest_id, '(RFC822)')
|
||||
|
||||
if status != 'OK':
|
||||
return None
|
||||
|
||||
msg = email.message_from_bytes(msg_data[0][1])
|
||||
|
||||
# 检查是否是斗鱼的邮件
|
||||
subject = self._decode_subject(msg.get('Subject', ''))
|
||||
if not self._is_douyu_email(subject, msg.get('From', '')):
|
||||
return None
|
||||
|
||||
# 提取验证码
|
||||
body = self._get_email_body(msg)
|
||||
code = self._extract_verification_code(body)
|
||||
|
||||
if code:
|
||||
logger.info(f"从邮件中提取到验证码: {code}")
|
||||
|
||||
return code
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"获取邮件失败: {e}")
|
||||
return None
|
||||
|
||||
def _is_douyu_email(self, subject: str, from_addr: str) -> bool:
|
||||
"""判断是否是斗鱼的邮件"""
|
||||
douyu_keywords = ['斗鱼', 'douyu', '验证码', '安全验证']
|
||||
subject_lower = subject.lower()
|
||||
from_lower = from_addr.lower()
|
||||
|
||||
return any(keyword in subject_lower or keyword in from_lower
|
||||
for keyword in douyu_keywords)
|
||||
|
||||
def _decode_subject(self, subject: str) -> str:
|
||||
"""解码邮件主题"""
|
||||
if not subject:
|
||||
return ""
|
||||
|
||||
decoded_parts = decode_header(subject)
|
||||
result = []
|
||||
for part, charset in decoded_parts:
|
||||
if isinstance(part, bytes):
|
||||
result.append(part.decode(charset or 'utf-8', errors='ignore'))
|
||||
else:
|
||||
result.append(part)
|
||||
return ' '.join(result)
|
||||
|
||||
def _get_email_body(self, msg: email.message.Message) -> str:
|
||||
"""获取邮件正文"""
|
||||
body = ""
|
||||
|
||||
if msg.is_multipart():
|
||||
for part in msg.walk():
|
||||
content_type = part.get_content_type()
|
||||
if content_type == 'text/plain' or content_type == 'text/html':
|
||||
try:
|
||||
payload = part.get_payload(decode=True)
|
||||
charset = part.get_content_charset() or 'utf-8'
|
||||
body += payload.decode(charset, errors='ignore')
|
||||
except:
|
||||
pass
|
||||
else:
|
||||
try:
|
||||
payload = msg.get_payload(decode=True)
|
||||
charset = msg.get_content_charset() or 'utf-8'
|
||||
body = payload.decode(charset, errors='ignore')
|
||||
except:
|
||||
pass
|
||||
|
||||
return body
|
||||
|
||||
def _extract_verification_code(self, text: str) -> Optional[str]:
|
||||
"""从文本中提取验证码"""
|
||||
# 清理HTML标签
|
||||
text = re.sub(r'<[^>]+>', ' ', text)
|
||||
|
||||
# 查找6位数字验证码
|
||||
# 注意:中文冒号和英文冒号需要分别处理
|
||||
patterns = [
|
||||
r'验证码[::]\s*(\d{6})', # 验证码:123456 或 验证码:123456
|
||||
r'验证码\s+(\d{6})', # 验证码 123456
|
||||
r'(\d{6})\s*是您的验证码', # 123456是您的验证码
|
||||
r'您的验证码[是为]\s*[::]\s*(\d{6})', # 您的验证码是:123456
|
||||
r'您的验证码[是为]\s*(\d{6})', # 您的验证码是123456
|
||||
r'verification code[::]\s*(\d{6})',
|
||||
r'code[::]\s*(\d{6})',
|
||||
r'(\d{6})', # 最后尝试匹配任何6位数字
|
||||
]
|
||||
|
||||
for pattern in patterns:
|
||||
match = re.search(pattern, text, re.IGNORECASE)
|
||||
if match:
|
||||
code = match.group(1)
|
||||
# 验证码有效性检查(简单过滤明显不是验证码的数字)
|
||||
if not self._is_likely_code(code):
|
||||
continue
|
||||
return code
|
||||
|
||||
return None
|
||||
|
||||
def _is_likely_code(self, code: str) -> bool:
|
||||
"""判断是否是有效的验证码"""
|
||||
# 过滤掉一些明显不是验证码的数字
|
||||
invalid_patterns = [
|
||||
r'^(\d)\1{5}$', # 全部相同:111111, 222222
|
||||
r'^123456$', # 顺序数字
|
||||
r'^654321$', # 逆序数字
|
||||
]
|
||||
|
||||
for pattern in invalid_patterns:
|
||||
if re.match(pattern, code):
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def get_email_config_for_account(email_address: str) -> dict:
|
||||
"""根据邮箱地址自动配置IMAP服务器"""
|
||||
configs = {
|
||||
'qq.com': {'server': 'imap.qq.com', 'port': 993},
|
||||
'163.com': {'server': 'imap.163.com', 'port': 993},
|
||||
'126.com': {'server': 'imap.126.com', 'port': 993},
|
||||
'gmail.com': {'server': 'imap.gmail.com', 'port': 993},
|
||||
'outlook.com': {'server': 'outlook.office365.com', 'port': 993},
|
||||
'hotmail.com': {'server': 'outlook.office365.com', 'port': 993},
|
||||
}
|
||||
|
||||
domain = email_address.split('@')[-1].lower()
|
||||
return configs.get(domain, {'server': f'imap.{domain}', 'port': 993})
|
||||
+458
@@ -0,0 +1,458 @@
|
||||
"""斗鱼登录核心模块"""
|
||||
|
||||
import re
|
||||
import json
|
||||
import time
|
||||
import requests
|
||||
from typing import Mapping, Optional, Tuple
|
||||
from pathlib import Path
|
||||
from urllib.parse import urlsplit, urlunsplit
|
||||
from loguru import logger
|
||||
|
||||
from .config import Account
|
||||
from .crypto import encrypt_password, encrypt_nickname_or_phone
|
||||
from .email_verifier import EmailVerifier
|
||||
|
||||
# 导入geetest滑块模块
|
||||
import sys
|
||||
sys.path.insert(0, str(Path(__file__).parent.parent))
|
||||
from geetest import run_solver
|
||||
from geetest.solver import (
|
||||
_generate_seed, get_w1, get_w2, get_w3,
|
||||
)
|
||||
from geetest.network import get_js_address, get_c_s, req_slide, get_picture, req_end
|
||||
from geetest.imaging import download_picture
|
||||
|
||||
|
||||
class LoginResult:
|
||||
"""登录结果"""
|
||||
|
||||
def __init__(self, success: bool, cookie: str = "", message: str = "", code: str = ""):
|
||||
self.success = success
|
||||
self.cookie = cookie
|
||||
self.message = message
|
||||
self.code = code # 需要验证时的code
|
||||
|
||||
|
||||
class DouyuLogin:
|
||||
"""斗鱼登录器"""
|
||||
|
||||
# 斗鱼API地址
|
||||
LOGIN_API = "https://passport.douyu.com/wgapi/member/passport/login"
|
||||
SEND_EMAIL_API = "https://passport.douyu.com/wgapi/member/passport/remotelogin/sendemail"
|
||||
VERIFY_API = "https://passport.douyu.com/wgapi/member/passport/remotelogin/verify"
|
||||
LOGIN_CALLBACK_API = "https://www.douyu.com/api/passport/login"
|
||||
WEBLOGIN_API = "https://msg.douyu.com/webLogin"
|
||||
REQUEST_TIMEOUT = (3.05, 12)
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
account: Account,
|
||||
proxy: Optional[str | Mapping[str, str]] = None,
|
||||
timeout: tuple[float, float] = REQUEST_TIMEOUT,
|
||||
):
|
||||
self.account = account
|
||||
self.proxy = proxy
|
||||
self.timeout = timeout
|
||||
self.session = requests.Session()
|
||||
self._setup_session()
|
||||
|
||||
def _setup_session(self) -> None:
|
||||
"""配置Session"""
|
||||
# 只使用配置文件里显式传入的代理,避免系统环境变量悄悄影响请求。
|
||||
self.session.trust_env = False
|
||||
self.session.headers.update({
|
||||
'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/149.0.0.0 Safari/537.36',
|
||||
'Accept': 'application/json, text/javascript, */*; q=0.01',
|
||||
'Accept-Language': 'zh-CN,zh;q=0.9',
|
||||
'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8',
|
||||
'Referer': 'https://www.douyu.com/',
|
||||
'Origin': 'https://www.douyu.com',
|
||||
'X-Requested-With': 'XMLHttpRequest',
|
||||
})
|
||||
|
||||
if self.proxy:
|
||||
if isinstance(self.proxy, str):
|
||||
self.session.proxies = {
|
||||
'http': self.proxy,
|
||||
'https': self.proxy,
|
||||
}
|
||||
else:
|
||||
self.session.proxies = {
|
||||
scheme: url
|
||||
for scheme, url in self.proxy.items()
|
||||
if url
|
||||
}
|
||||
|
||||
def _safe_url(self, url: str) -> str:
|
||||
"""隐藏查询参数,避免日志泄露登录回调 code。"""
|
||||
parsed = urlsplit(url)
|
||||
return urlunsplit((parsed.scheme, parsed.netloc, parsed.path, "", ""))
|
||||
|
||||
def _request(self, method: str, url: str, **kwargs) -> requests.Response:
|
||||
"""统一发送请求,附带分段超时和更明确的错误信息。"""
|
||||
timeout = kwargs.pop('timeout', self.timeout)
|
||||
safe_url = self._safe_url(url)
|
||||
started = time.monotonic()
|
||||
|
||||
try:
|
||||
response = self.session.request(method, url, timeout=timeout, **kwargs)
|
||||
except requests.Timeout as exc:
|
||||
elapsed = time.monotonic() - started
|
||||
raise TimeoutError(
|
||||
f"{method.upper()} {safe_url} 超时,耗时 {elapsed:.1f}s,"
|
||||
f"timeout={timeout}"
|
||||
) from exc
|
||||
except requests.RequestException as exc:
|
||||
elapsed = time.monotonic() - started
|
||||
raise ConnectionError(
|
||||
f"{method.upper()} {safe_url} 请求失败,耗时 {elapsed:.1f}s: {exc}"
|
||||
) from exc
|
||||
|
||||
elapsed = time.monotonic() - started
|
||||
logger.debug(
|
||||
f"{method.upper()} {safe_url} -> {response.status_code} "
|
||||
f"({elapsed:.2f}s)"
|
||||
)
|
||||
return response
|
||||
|
||||
def _request_json(self, method: str, url: str, source: str, **kwargs) -> dict:
|
||||
"""请求 JSON 接口,并在响应异常时输出可定位的信息。"""
|
||||
response = self._request(method, url, **kwargs)
|
||||
response.raise_for_status()
|
||||
|
||||
body = response.text.strip()
|
||||
if not body:
|
||||
raise ValueError(f"{source} 返回为空,无法解析 JSON")
|
||||
|
||||
try:
|
||||
return response.json()
|
||||
except json.JSONDecodeError as exc:
|
||||
preview = body[:200].replace("\n", "\\n")
|
||||
raise ValueError(f"{source} 返回的不是有效 JSON: {preview}") from exc
|
||||
|
||||
def login(self) -> LoginResult:
|
||||
"""
|
||||
完整登录流程
|
||||
|
||||
Returns:
|
||||
LoginResult: 登录结果,包含cookie
|
||||
"""
|
||||
logger.info(f"开始登录账号: {self.account.username}")
|
||||
|
||||
try:
|
||||
# 1️⃣ 第一次登录(获取极验参数)
|
||||
logger.info("步骤1: 第一次登录,获取极验参数...")
|
||||
gt, challenge, code_token, initial_cookies = self._first_login()
|
||||
|
||||
# 2️⃣ 极验滑块验证
|
||||
logger.info("步骤2: 极验滑块验证...")
|
||||
validate, seccode = self._solve_geetest(gt, challenge)
|
||||
|
||||
# 3️⃣ 第二次登录(带极验)
|
||||
logger.info("步骤3: 第二次登录(带极验验证)...")
|
||||
remote_code = self._second_login(gt, challenge, validate, seccode, code_token)
|
||||
|
||||
# 4️⃣ 发送邮箱验证
|
||||
logger.info("步骤4: 发送邮箱验证...")
|
||||
self._send_email_verify(remote_code)
|
||||
|
||||
# 5️⃣ IMAP获取验证码
|
||||
logger.info("步骤5: 获取邮箱验证码...")
|
||||
verify_code = self._get_email_code()
|
||||
|
||||
# 6️⃣ 提交验证码
|
||||
logger.info("步骤6: 提交验证码...")
|
||||
login_url = self._submit_verify_code(remote_code, verify_code)
|
||||
|
||||
# 7️⃣ 完成登录获取Cookie
|
||||
logger.info("步骤7: 完成登录,获取Cookie...")
|
||||
cookie = self._complete_login(login_url)
|
||||
|
||||
logger.success(f"登录成功! Cookie长度: {len(cookie)}")
|
||||
return LoginResult(success=True, cookie=cookie, message="登录成功")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"登录失败: {e}")
|
||||
return LoginResult(success=False, message=str(e))
|
||||
|
||||
def _first_login(self) -> Tuple[str, str, str, dict]:
|
||||
"""
|
||||
第一次登录,获取极验参数
|
||||
|
||||
Returns:
|
||||
(gt, challenge, code_token, cookies)
|
||||
"""
|
||||
# 加密用户名和密码
|
||||
encrypted_username = encrypt_nickname_or_phone(self.account.username)
|
||||
encrypted_password = encrypt_password(self.account.password)
|
||||
|
||||
data = {
|
||||
'type': '1',
|
||||
'nicknameOrPhoneEncrypt': encrypted_username,
|
||||
'password': encrypted_password,
|
||||
'biz_type': '1',
|
||||
'room_id': '0',
|
||||
'redirect_url': 'https://www.douyu.com/',
|
||||
}
|
||||
|
||||
payload = self._request_json(
|
||||
'post',
|
||||
self.LOGIN_API,
|
||||
'第一次登录接口',
|
||||
data=data,
|
||||
)
|
||||
|
||||
logger.debug(f"第一次登录响应: {json.dumps(payload, ensure_ascii=False)[:200]}")
|
||||
|
||||
if payload.get('error') != 81:
|
||||
error_msg = payload.get('msg', '未知错误')
|
||||
raise ValueError(f"第一次登录失败: {error_msg}")
|
||||
|
||||
# 提取极验参数
|
||||
geetest_data = payload.get('data', {}).get('geetest', {})
|
||||
code_data = geetest_data.get('code_data', {})
|
||||
code_token = geetest_data.get('code_token', '')
|
||||
|
||||
gt = code_data.get('gt', '')
|
||||
challenge = code_data.get('challenge', '')
|
||||
|
||||
if not gt or not challenge:
|
||||
raise ValueError("获取极验参数失败")
|
||||
|
||||
logger.info(f"获取极验参数成功: gt={gt[:10]}..., challenge={challenge[:10]}...")
|
||||
|
||||
return gt, challenge, code_token, self.session.cookies.get_dict()
|
||||
|
||||
def _solve_geetest(self, gt: str, challenge: str) -> Tuple[str, str]:
|
||||
"""
|
||||
解决极验滑块验证
|
||||
|
||||
Args:
|
||||
gt: 极验gt参数
|
||||
challenge: 极验challenge参数
|
||||
|
||||
Returns:
|
||||
(validate, seccode)
|
||||
"""
|
||||
logger.info("开始极验滑块验证...")
|
||||
|
||||
# 使用geetest-v3-silde-crack的完整流程
|
||||
str_16 = _generate_seed()
|
||||
|
||||
# 获取JS地址
|
||||
get_js_address(gt)
|
||||
|
||||
# 获取第一个w值
|
||||
w1 = get_w1(gt, challenge, str_16)
|
||||
|
||||
# 获取c和s
|
||||
c, s = get_c_s(gt, challenge, w1)
|
||||
|
||||
# 获取第二个w值
|
||||
w2 = get_w2(gt, challenge, c, s, str_16)
|
||||
|
||||
# 请求滑块
|
||||
req_slide(gt, challenge, w2)
|
||||
|
||||
# 获取图片
|
||||
bg, fullbg, c, s, slice, challenge_new = get_picture(gt, challenge)
|
||||
|
||||
# 识别缺口位置
|
||||
hkjl = download_picture(bg, fullbg, slice)
|
||||
|
||||
# 获取第三个w值
|
||||
w3 = get_w3(str_16, challenge_new, hkjl, c, s, gt)
|
||||
|
||||
# 最后验证
|
||||
result = req_end(gt, challenge_new, w3)
|
||||
|
||||
# 从req_end的响应中提取validate
|
||||
# req_end返回的是验证结果,成功时包含validate
|
||||
if isinstance(result, dict):
|
||||
validate = result.get('validate', '')
|
||||
else:
|
||||
# 如果返回的是字符串,可能是直接的validate值
|
||||
validate = str(result)
|
||||
|
||||
if not validate:
|
||||
raise ValueError("极验验证失败,未获取到validate")
|
||||
|
||||
seccode = f"{validate}|jordan"
|
||||
|
||||
logger.success(f"极验滑块验证成功! validate={validate[:20]}...")
|
||||
return validate, seccode
|
||||
|
||||
def _second_login(self, gt: str, challenge: str, validate: str,
|
||||
seccode: str, code_token: str) -> str:
|
||||
"""
|
||||
第二次登录(带极验验证)
|
||||
|
||||
Returns:
|
||||
remote_code: 用于邮箱验证的code
|
||||
"""
|
||||
encrypted_username = encrypt_nickname_or_phone(self.account.username)
|
||||
encrypted_password = encrypt_password(self.account.password)
|
||||
|
||||
# 参考HAR文件中的完整参数
|
||||
data = {
|
||||
'type': '1',
|
||||
'nicknameOrPhoneEncrypt': encrypted_username,
|
||||
'password': encrypted_password,
|
||||
'room_id': '0',
|
||||
'code_type': '1',
|
||||
'code_token': code_token,
|
||||
'gt_version': 'v3',
|
||||
'geetest_challenge': challenge,
|
||||
'geetest_validate': validate,
|
||||
'geetest_seccode': seccode,
|
||||
'code_data[geetest_challenge]': challenge,
|
||||
'code_data[geetest_validate]': validate,
|
||||
'code_data[geetest_seccode]': seccode,
|
||||
'code_data[gt_version]': 'v3',
|
||||
'code_data[code]': '',
|
||||
'redirect_url': 'https://www.douyu.com/',
|
||||
't': str(int(time.time() * 1000)),
|
||||
'client_id': '1',
|
||||
'did': '',
|
||||
'lang': '',
|
||||
'isMultiAccount': '0',
|
||||
'biz_type': '1',
|
||||
}
|
||||
|
||||
payload = self._request_json(
|
||||
'post',
|
||||
self.LOGIN_API,
|
||||
'第二次登录接口',
|
||||
data=data,
|
||||
)
|
||||
|
||||
logger.debug(f"第二次登录响应: {json.dumps(payload, ensure_ascii=False)[:200]}")
|
||||
|
||||
if payload.get('error') != 130014:
|
||||
error_msg = payload.get('msg', '未知错误')
|
||||
raise ValueError(f"第二次登录失败: {error_msg}")
|
||||
|
||||
# 提取remote_code
|
||||
remote_code = payload.get('data', {}).get('remoteLogin', {}).get('code', '')
|
||||
|
||||
if not remote_code:
|
||||
raise ValueError("获取remote_code失败")
|
||||
|
||||
logger.info(f"获取remote_code成功: {remote_code[:20]}...")
|
||||
|
||||
return remote_code
|
||||
|
||||
def _send_email_verify(self, remote_code: str) -> None:
|
||||
"""发送邮箱验证"""
|
||||
data = {
|
||||
'code': remote_code,
|
||||
'client_id': '1',
|
||||
}
|
||||
|
||||
payload = self._request_json(
|
||||
'post',
|
||||
self.SEND_EMAIL_API,
|
||||
'发送验证邮件接口',
|
||||
data=data,
|
||||
)
|
||||
|
||||
if payload.get('error') != 0:
|
||||
raise ValueError(f"发送验证邮件失败: {payload.get('msg')}")
|
||||
|
||||
logger.info("验证邮件已发送")
|
||||
|
||||
def _get_email_code(self) -> str:
|
||||
"""获取邮箱验证码"""
|
||||
verifier = EmailVerifier(
|
||||
imap_server=self.account.email_imap_server,
|
||||
imap_port=self.account.email_imap_port,
|
||||
username=self.account.email,
|
||||
password=self.account.email_password,
|
||||
)
|
||||
|
||||
return verifier.get_verification_code(max_wait=60)
|
||||
|
||||
def _submit_verify_code(self, remote_code: str, verify_code: str) -> str:
|
||||
"""
|
||||
提交验证码
|
||||
|
||||
Returns:
|
||||
login_url: 登录回调URL
|
||||
"""
|
||||
data = {
|
||||
'verify_type': '2',
|
||||
'captcha': verify_code,
|
||||
'isMultiAccount': '0',
|
||||
'code': remote_code,
|
||||
'client_id': '1',
|
||||
'redirect_url': '//www.douyu.com/api/passport/login',
|
||||
}
|
||||
|
||||
payload = self._request_json(
|
||||
'post',
|
||||
self.VERIFY_API,
|
||||
'提交验证码接口',
|
||||
data=data,
|
||||
)
|
||||
|
||||
if payload.get('error') != 0:
|
||||
raise ValueError(f"提交验证码失败: {payload.get('msg')}")
|
||||
|
||||
login_url = payload.get('data', {}).get('url', '')
|
||||
|
||||
if not login_url:
|
||||
raise ValueError("获取登录URL失败")
|
||||
|
||||
# 补全URL
|
||||
if login_url.startswith('//'):
|
||||
login_url = 'https:' + login_url
|
||||
|
||||
logger.info(f"获取登录URL成功: {login_url[:50]}...")
|
||||
|
||||
return login_url
|
||||
|
||||
def _complete_login(self, login_url: str) -> str:
|
||||
"""
|
||||
完成登录,获取Cookie
|
||||
|
||||
Returns:
|
||||
cookie: 完整的Cookie字符串
|
||||
"""
|
||||
# 访问登录URL
|
||||
response = self._request('get', login_url)
|
||||
response.raise_for_status()
|
||||
|
||||
# 尝试访问webLogin获取用户信息
|
||||
try:
|
||||
code_match = re.search(r'code=([^&]+)', login_url)
|
||||
if code_match:
|
||||
code = code_match.group(1)
|
||||
weblogin_url = f"{self.WEBLOGIN_API}?code={code}"
|
||||
response2 = self._request('get', weblogin_url)
|
||||
|
||||
if response2.status_code == 200:
|
||||
logger.info("WebLogin成功")
|
||||
except Exception as e:
|
||||
logger.warning(f"WebLogin请求失败(不影响登录): {e}")
|
||||
|
||||
# 收集所有Cookie
|
||||
cookies = self.session.cookies.get_dict()
|
||||
|
||||
# 格式化Cookie字符串
|
||||
cookie_str = '; '.join([f"{k}={v}" for k, v in cookies.items()])
|
||||
|
||||
return cookie_str
|
||||
|
||||
def save_cookie(self, cookie: str, filepath: str) -> None:
|
||||
"""保存Cookie到文件"""
|
||||
Path(filepath).parent.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
with open(filepath, 'w', encoding='utf-8') as f:
|
||||
json.dump({
|
||||
'username': self.account.username,
|
||||
'cookie': cookie,
|
||||
'timestamp': int(time.time()),
|
||||
}, f, ensure_ascii=False, indent=2)
|
||||
|
||||
logger.info(f"Cookie已保存到: {filepath}")
|
||||
@@ -0,0 +1,5 @@
|
||||
"""Geetest slider captcha solver package."""
|
||||
|
||||
from .solver import run_solver
|
||||
|
||||
__all__ = ["run_solver"]
|
||||
@@ -0,0 +1,246 @@
|
||||
import random
|
||||
import hashlib
|
||||
from typing import Any
|
||||
from Crypto.PublicKey import RSA
|
||||
from Crypto.Cipher import PKCS1_v1_5, AES
|
||||
|
||||
# 随机产生4个字符组成的字符串
|
||||
def four_random_chart() -> str:
|
||||
return hex(int(65536 * (1 + random.random())))[2:][1:]
|
||||
|
||||
# PKCS#1 v1.5 填充 + RSA 加密
|
||||
def parse_jsbn_bigint(n_obj:dict[Any, int]) -> int:
|
||||
DB = 28
|
||||
DV = 1 << DB
|
||||
t = n_obj['t']
|
||||
|
||||
result = 0
|
||||
for i in range(t):
|
||||
result += n_obj[i] * (DV ** i)
|
||||
|
||||
return result
|
||||
|
||||
def encrypt_data(plaintext:str) -> str:
|
||||
cipher = PKCS1_v1_5.new(public_key)
|
||||
encrypted = cipher.encrypt(plaintext.encode('utf-8'))
|
||||
# 转换为十六进制,确保偶数长度
|
||||
hex_result = encrypted.hex()
|
||||
if len(hex_result) % 2 == 1:
|
||||
hex_result = '0' + hex_result
|
||||
return hex_result
|
||||
|
||||
def RSA_jiami_r(str_16:str) -> str:
|
||||
global public_key
|
||||
# 你的数据
|
||||
n_data = {
|
||||
0: 134982529, 1: 254232810, 2: 164556709, 3: 234907349,
|
||||
4: 134685994, 5: 35463984, 6: 258277946, 7: 12518857,
|
||||
8: 44638621, 9: 93783641, 10: 212253739, 11: 62792472,
|
||||
12: 186688352, 13: 109500232, 14: 182488077, 15: 261196188,
|
||||
16: 26354094, 17: 103248217, 18: 106891695, 19: 165771045,
|
||||
20: 41530993, 21: 263704736, 22: 111785174, 23: 12753611,
|
||||
24: 232116673, 25: 155524985, 26: 218291229, 27: 122452343,
|
||||
28: 248250238, 29: 118739550, 30: 251169095, 31: 129059733,
|
||||
32: 149835464, 33: 5498868, 34: 71719731, 35: 154456417,
|
||||
36: 49635,
|
||||
't': 37, 's': 0
|
||||
}
|
||||
|
||||
e = 65537
|
||||
n = parse_jsbn_bigint(n_data)
|
||||
|
||||
# 构造公钥
|
||||
public_key = RSA.construct((n, e))
|
||||
encrypted = encrypt_data(str_16)
|
||||
return encrypted
|
||||
|
||||
# AES加密
|
||||
# 加密模式: AES-CBC
|
||||
# 密钥长度: 128位
|
||||
# IV: 固定为 "0000000000000000"
|
||||
def parse_string_to_wordarray(text:str) -> list[int]:
|
||||
"""将字符串转换为 WordArray 格式"""
|
||||
length = len(text)
|
||||
words = []
|
||||
|
||||
for i in range(length):
|
||||
# 计算在 words 数组中的索引
|
||||
word_index = i >> 2 # 相当于 i // 4
|
||||
|
||||
# 确保 words 数组足够长
|
||||
while len(words) <= word_index:
|
||||
words.append(0)
|
||||
|
||||
# 获取字符的 ASCII 码
|
||||
char_code = ord(text[i]) & 0xFF
|
||||
|
||||
# 计算位移量
|
||||
shift = 24 - (i % 4) * 8
|
||||
|
||||
# 将字符添加到对应的 word 中
|
||||
words[word_index] |= char_code << shift
|
||||
|
||||
return words
|
||||
def AES_O(plaintext:str, str_16:str) -> list[int]:
|
||||
# 密钥
|
||||
key_words = parse_string_to_wordarray(str_16)
|
||||
key = b''.join(w.to_bytes(4, 'big') for w in key_words)
|
||||
|
||||
# IV
|
||||
iv = b'0000' * 4 # "0000000000000000"
|
||||
|
||||
# 填充(PKCS7)
|
||||
pad_len = 16 - len(plaintext) % 16
|
||||
plaintext_padded = plaintext.encode() + bytes([pad_len] * pad_len)
|
||||
|
||||
# 加密
|
||||
cipher = AES.new(key, AES.MODE_CBC, iv)
|
||||
ciphertext = cipher.encrypt(plaintext_padded)
|
||||
|
||||
# 结果是字节数组
|
||||
return list(ciphertext)
|
||||
|
||||
# 自定义base64编码
|
||||
def geetest_base64_encode(data:list[int]) -> dict[str, Any]:
|
||||
"""极验自定义Base64编码"""
|
||||
|
||||
# 配置
|
||||
charset = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789()'
|
||||
pad_char = '.'
|
||||
|
||||
# 位掩码 (这些是打乱的)
|
||||
masks = [7274496, 9483264, 19220, 235]
|
||||
mask_bits = 24 # 总位数
|
||||
|
||||
def extract_bits(value, mask):
|
||||
"""根据掩码提取位"""
|
||||
result = 0
|
||||
# 从高位到低位遍历
|
||||
for i in range(mask_bits - 1, -1, -1):
|
||||
# 如果掩码的第i位是1
|
||||
if (mask >> i) & 1:
|
||||
# 提取value的第i位,添加到结果
|
||||
result = (result << 1) | ((value >> i) & 1)
|
||||
return result
|
||||
|
||||
encoded = ""
|
||||
padding = ""
|
||||
length = len(data)
|
||||
|
||||
# 每3字节一组
|
||||
i = 0
|
||||
while i < length:
|
||||
if i + 2 < length:
|
||||
# 完整3字节
|
||||
block = (data[i] << 16) | (data[i + 1] << 8) | data[i + 2]
|
||||
|
||||
# 使用4个掩码提取
|
||||
encoded += charset[extract_bits(block, masks[0])]
|
||||
encoded += charset[extract_bits(block, masks[1])]
|
||||
encoded += charset[extract_bits(block, masks[2])]
|
||||
encoded += charset[extract_bits(block, masks[3])]
|
||||
|
||||
i += 3
|
||||
else:
|
||||
# 处理剩余
|
||||
remainder = length - i
|
||||
|
||||
if remainder == 2:
|
||||
block = (data[i] << 16) | (data[i + 1] << 8)
|
||||
encoded += charset[extract_bits(block, masks[0])]
|
||||
encoded += charset[extract_bits(block, masks[1])]
|
||||
encoded += charset[extract_bits(block, masks[2])]
|
||||
padding = pad_char
|
||||
elif remainder == 1:
|
||||
block = data[i] << 16
|
||||
encoded += charset[extract_bits(block, masks[0])]
|
||||
encoded += charset[extract_bits(block, masks[1])]
|
||||
padding = pad_char + pad_char
|
||||
|
||||
break
|
||||
|
||||
return {
|
||||
"res": encoded,
|
||||
"end": padding
|
||||
}
|
||||
|
||||
|
||||
def encrypt_string(e:str, t:list[int], n:str) -> str:
|
||||
"""
|
||||
JS加密函数的Python实现
|
||||
|
||||
参数:
|
||||
e: 原始字符串
|
||||
t: 加密参数数组
|
||||
n: 十六进制字符串
|
||||
"""
|
||||
if not t or not n:
|
||||
return e
|
||||
|
||||
o = 0 # 偏移量
|
||||
i = e # 结果字符串
|
||||
s = t[0] # 12
|
||||
a = t[2] # 98
|
||||
_ = t[4] # 43
|
||||
|
||||
# 每次读取2个字符(十六进制)
|
||||
while o < len(n):
|
||||
r = n[o:o + 2] # 取2个字符
|
||||
if len(r) < 2:
|
||||
break
|
||||
o += 2
|
||||
|
||||
# 解析十六进制
|
||||
c = int(r, 16)
|
||||
|
||||
# 转换为字符
|
||||
l = chr(c)
|
||||
|
||||
# 计算插入位置: (s * c^2 + a * c + _) % len(e)
|
||||
u = (s * c * c + a * c + _) % len(e)
|
||||
|
||||
# 在位置u插入字符
|
||||
i = i[:u] + l + i[u:]
|
||||
|
||||
return i
|
||||
|
||||
|
||||
def simple_md5(message:str) -> str:
|
||||
"""
|
||||
简化版MD5实现,结构更清晰
|
||||
"""
|
||||
|
||||
# 使用内置hashlib验证结果
|
||||
def verify_result(msg):
|
||||
return hashlib.md5(msg.encode()).hexdigest()
|
||||
|
||||
# 轮移位常量
|
||||
shifts = [
|
||||
7, 12, 17, 22, 7, 12, 17, 22, 7, 12, 17, 22, 7, 12, 17, 22,
|
||||
5, 9, 14, 20, 5, 9, 14, 20, 5, 9, 14, 20, 5, 9, 14, 20,
|
||||
4, 11, 16, 23, 4, 11, 16, 23, 4, 11, 16, 23, 4, 11, 16, 23,
|
||||
6, 10, 15, 21, 6, 10, 15, 21, 6, 10, 15, 21, 6, 10, 15, 21
|
||||
]
|
||||
|
||||
# K常数(与JavaScript版本中的常数对应)
|
||||
K = [
|
||||
0xd76aa478, 0xe8c7b756, 0x242070db, 0xc1bdceee,
|
||||
0xf57c0faf, 0x4787c62a, 0xa8304613, 0xfd469501,
|
||||
0x698098d8, 0x8b44f7af, 0xffff5bb1, 0x895cd7be,
|
||||
0x6b901122, 0xfd987193, 0xa679438e, 0x49b40821,
|
||||
0xf61e2562, 0xc040b340, 0x265e5a51, 0xe9b6c7aa,
|
||||
0xd62f105d, 0x02441453, 0xd8a1e681, 0xe7d3fbc8,
|
||||
0x21e1cde6, 0xc33707d6, 0xf4d50d87, 0x455a14ed,
|
||||
0xa9e3e905, 0xfcefa3f8, 0x676f02d9, 0x8d2a4c8a,
|
||||
0xfffa3942, 0x8771f681, 0x6d9d6122, 0xfde5380c,
|
||||
0xa4beea44, 0x4bdecfa9, 0xf6bb4b60, 0xbebfbc70,
|
||||
0x289b7ec6, 0xeaa127fa, 0xd4ef3085, 0x04881d05,
|
||||
0xd9d4d039, 0xe6db99e5, 0x1fa27cf8, 0xc4ac5665,
|
||||
0xf4292244, 0x432aff97, 0xab9423a7, 0xfc93a039,
|
||||
0x655b59c3, 0x8f0ccc92, 0xffeff47d, 0x85845dd1,
|
||||
0x6fa87e4f, 0xfe2ce6e0, 0xa3014314, 0x4e0811a1,
|
||||
0xf7537e82, 0xbd3af235, 0x2ad7d2bb, 0xeb86d391
|
||||
]
|
||||
|
||||
# 实际实现...
|
||||
return verify_result(message)
|
||||
@@ -0,0 +1,89 @@
|
||||
import requests
|
||||
import cv2
|
||||
import numpy as np
|
||||
from PIL import Image
|
||||
|
||||
REQUEST_TIMEOUT = (3.05, 12)
|
||||
|
||||
|
||||
# 将 Image 转换为 Mat,通过 flag 可以控制颜色
|
||||
def pilImgToCv2(img: Image.Image, flag=cv2.COLOR_RGB2BGR):
|
||||
return cv2.cvtColor(np.asarray(img), flag)
|
||||
|
||||
# 识别图片缺口返回滑块距离
|
||||
def shibie(img: Image.Image, slice: Image.Image):
|
||||
# 通过 pilImgToCv2 将图片置灰
|
||||
# 背景图和滑块图都需要做相同处理
|
||||
grayImg = pilImgToCv2(img, cv2.COLOR_BGR2GRAY)
|
||||
# showImg(grayImg) # 可以通过它来看处理后的图片效果
|
||||
graySlice = pilImgToCv2(slice, cv2.COLOR_BGR2GRAY)
|
||||
# 做边缘检测进一步降低干扰,阈值可以自行调整
|
||||
grayImg = cv2.Canny(grayImg, 255, 255)
|
||||
# showImg(grayImg) # 可以通过它来看处理后的图片效果
|
||||
graySlice = cv2.Canny(graySlice, 255, 255)
|
||||
# 通过模板匹配两张图片,找出缺口的位置
|
||||
result = cv2.matchTemplate(grayImg, graySlice, cv2.TM_CCOEFF_NORMED)
|
||||
maxLoc = cv2.minMaxLoc(result)[3]
|
||||
# 匹配出来的滑动距离
|
||||
distance = maxLoc[0]
|
||||
# 下面的逻辑是在图片画出一个矩形框来标记匹配到的位置,可以直观的看到匹配结果,去掉也可以的
|
||||
sliceHeight, sliceWidth = graySlice.shape[:2]
|
||||
# 左上角
|
||||
x, y = maxLoc
|
||||
# 右下角
|
||||
x2, y2 = x + sliceWidth, y + sliceHeight
|
||||
resultBg = pilImgToCv2(img, cv2.COLOR_RGB2BGR)
|
||||
cv2.rectangle(resultBg, (x, y), (x2, y2), (0, 0, 255), 2)
|
||||
# showImg(resultBg) # 可以通过它来看处理后的图片效果
|
||||
return distance
|
||||
|
||||
# 还原图片
|
||||
def restore_geetest_image(input_path:str, output_path:str) -> None:
|
||||
"""
|
||||
还原极验打乱的验证码图像
|
||||
"""
|
||||
Ut = [
|
||||
39, 38, 48, 49, 41, 40, 46, 47, 35, 34, 50, 51, 33, 32, 28, 29,
|
||||
27, 26, 36, 37, 31, 30, 44, 45, 43, 42, 12, 13, 23, 22, 14, 15,
|
||||
21, 20, 8, 9, 25, 24, 6, 7, 3, 2, 0, 1, 11, 10, 4, 5, 19, 18, 16, 17
|
||||
]
|
||||
|
||||
# 打开混淆图像
|
||||
img = Image.open(input_path)
|
||||
new_img = Image.new("RGB", (260,160))
|
||||
r = 160
|
||||
for _ in range(len(Ut)):
|
||||
a = r / 2
|
||||
c = Ut[_] % 26 * 12 + 1
|
||||
u = 80 if Ut[_] > 25 else 0
|
||||
l = img.crop(box=(c, u, c + 10, u + 80))
|
||||
new_img.paste(l, box=(_ % 26 * 10, 80 if _ > 25 else 0))
|
||||
|
||||
new_img.save(output_path)
|
||||
print(f"图像已还原并保存到: {output_path}")
|
||||
|
||||
# 下载图片
|
||||
def download_picture(bg:str, fullbg:str, slice:str) -> int:
|
||||
for i in range(3):
|
||||
if i == 0:
|
||||
url = "https://static.geetest.com/"+bg
|
||||
response = requests.get(url, timeout=REQUEST_TIMEOUT)
|
||||
response.raise_for_status()
|
||||
with open("bg.jpg", "wb") as f:
|
||||
f.write(response.content)
|
||||
restore_geetest_image("bg.jpg", "bg.jpg")
|
||||
else:
|
||||
if i == 1:
|
||||
url = "https://static.geetest.com/" + fullbg
|
||||
response = requests.get(url, timeout=REQUEST_TIMEOUT)
|
||||
response.raise_for_status()
|
||||
with open("fullbg.jpg", "wb") as f:
|
||||
f.write(response.content)
|
||||
restore_geetest_image("fullbg.jpg", "fullbg.jpg")
|
||||
else:
|
||||
url = "https://static.geetest.com/" + slice
|
||||
response = requests.get(url, timeout=REQUEST_TIMEOUT)
|
||||
response.raise_for_status()
|
||||
with open("slice.jpg", "wb") as f:
|
||||
f.write(response.content)
|
||||
return shibie(Image.open('fullbg.jpg'), Image.open('slice.jpg'))
|
||||
@@ -0,0 +1,250 @@
|
||||
import time
|
||||
import requests
|
||||
import json
|
||||
import re
|
||||
from typing import Tuple
|
||||
|
||||
REQUEST_TIMEOUT = (3.05, 12)
|
||||
|
||||
|
||||
def _parse_json_response(response: requests.Response, source: str) -> dict:
|
||||
response.raise_for_status()
|
||||
|
||||
body = response.text.strip()
|
||||
if not body:
|
||||
raise ValueError(f"{source} 返回为空,无法解析 JSON")
|
||||
|
||||
try:
|
||||
return response.json()
|
||||
except json.JSONDecodeError as exc:
|
||||
preview = body[:200].replace("\n", "\\n")
|
||||
raise ValueError(f"{source} 返回的不是有效 JSON: {preview}") from exc
|
||||
|
||||
|
||||
def get_challenge_gt_bak() -> Tuple[str, str]:
|
||||
headers = {
|
||||
'accept': 'application/json, text/javascript, */*; q=0.01',
|
||||
'accept-language': 'zh-CN,zh;q=0.9',
|
||||
'priority': 'u=1, i',
|
||||
'referer': 'https://demos.geetest.com/slide-float.html',
|
||||
'sec-ch-ua': '"Chromium";v="140", "Not=A?Brand";v="24", "Google Chrome";v="140"',
|
||||
'sec-ch-ua-mobile': '?0',
|
||||
'sec-ch-ua-platform': '"Windows"',
|
||||
'sec-fetch-dest': 'empty',
|
||||
'sec-fetch-mode': 'cors',
|
||||
'sec-fetch-site': 'same-origin',
|
||||
'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.0.0 Safari/537.36',
|
||||
'x-requested-with': 'XMLHttpRequest',
|
||||
}
|
||||
|
||||
params = {
|
||||
't': str(int(round(time.time() * 1000))),
|
||||
}
|
||||
|
||||
response = requests.get(
|
||||
'https://demos.geetest.com/gt/register-slide',
|
||||
params=params,
|
||||
headers=headers,
|
||||
timeout=10,
|
||||
)
|
||||
data = _parse_json_response(response, "极验 demo register-slide")
|
||||
return data["gt"], data["challenge"]
|
||||
|
||||
|
||||
def get_challenge_gt() -> Tuple[str, str]:
|
||||
headers = {
|
||||
'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/146.0.0.0 Safari/537.36',
|
||||
'Accept': 'application/json, text/javascript, */*; q=0.01',
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
'referer': 'https://passport.douyu.com/member/login?state=https%3A%2F%2Fwww.douyu.com%2Fmember%2FcpSecurity%2Fcheck_geetest_status',
|
||||
}
|
||||
|
||||
data = {
|
||||
'type': '1',
|
||||
'nicknameOrPhoneEncrypt': '03ILaBwtmmCm0A==',
|
||||
'password': '57219dddec71c31b7647683fa5306103',
|
||||
'biz_type': '1',
|
||||
}
|
||||
|
||||
response = requests.post(
|
||||
'https://passport.douyu.com/wgapi/member/passport/login',
|
||||
headers=headers,
|
||||
data=data,
|
||||
timeout=10,
|
||||
)
|
||||
payload = _parse_json_response(response, "斗鱼登录接口")
|
||||
|
||||
try:
|
||||
code_data = payload["data"]["geetest"]["code_data"]
|
||||
return code_data["gt"], code_data["challenge"]
|
||||
except KeyError as exc:
|
||||
preview = json.dumps(payload, ensure_ascii=False)[:300]
|
||||
raise ValueError(f"斗鱼登录接口返回中缺少极验参数: {preview}") from exc
|
||||
|
||||
|
||||
|
||||
def get_js_address(gt:str) -> dict:
|
||||
headers = {
|
||||
'accept': '*/*',
|
||||
'accept-language': 'zh-CN,zh;q=0.9',
|
||||
'referer': 'https://demos.geetest.com/',
|
||||
'sec-ch-ua': '"Chromium";v="140", "Not=A?Brand";v="24", "Google Chrome";v="140"',
|
||||
'sec-ch-ua-mobile': '?0',
|
||||
'sec-ch-ua-platform': '"Windows"',
|
||||
'sec-fetch-dest': 'script',
|
||||
'sec-fetch-mode': 'no-cors',
|
||||
'sec-fetch-site': 'same-site',
|
||||
'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.0.0 Safari/537.36',
|
||||
}
|
||||
|
||||
params = {
|
||||
'gt': gt,
|
||||
'callback': 'geetest_' + str(int(round(time.time() * 1000))),
|
||||
}
|
||||
|
||||
response = requests.get(
|
||||
'https://apiv6.geetest.com/gettype.php',
|
||||
params=params,
|
||||
headers=headers,
|
||||
timeout=REQUEST_TIMEOUT,
|
||||
)
|
||||
response.raise_for_status()
|
||||
|
||||
match = re.search(r'\((.*)\)$', response.text)
|
||||
if match:
|
||||
json_str = match.group(1)
|
||||
return json.loads(json_str)
|
||||
else:
|
||||
raise ValueError("无法解析 JSONP 响应")
|
||||
|
||||
def get_c_s(gt:str, challenge:str, w:str) -> Tuple[list[int],str]:
|
||||
headers = {
|
||||
'accept': '*/*',
|
||||
'accept-language': 'zh-CN,zh;q=0.9',
|
||||
'referer': 'https://demos.geetest.com/',
|
||||
'sec-ch-ua': '"Chromium";v="140", "Not=A?Brand";v="24", "Google Chrome";v="140"',
|
||||
'sec-ch-ua-mobile': '?0',
|
||||
'sec-ch-ua-platform': '"Windows"',
|
||||
'sec-fetch-dest': 'script',
|
||||
'sec-fetch-mode': 'no-cors',
|
||||
'sec-fetch-site': 'same-site',
|
||||
'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.0.0 Safari/537.36',
|
||||
}
|
||||
|
||||
response = requests.get(
|
||||
'https://apiv6.geetest.com/get.php?gt=' + gt + '&challenge=' + challenge + '&lang=zh-cn&pt=0&client_type=web&w=' + w + '&callback=geetest_' + str(
|
||||
int(round(time.time() * 1000))),
|
||||
headers=headers,
|
||||
timeout=REQUEST_TIMEOUT,
|
||||
)
|
||||
response.raise_for_status()
|
||||
match = re.search(r'\((.*)\)$', response.text)
|
||||
if match:
|
||||
json_str = match.group(1)
|
||||
return json.loads(json_str)['data']['c'], json.loads(json_str)['data']['s']
|
||||
else:
|
||||
raise ValueError("无法解析 JSONP 响应")
|
||||
|
||||
def req_slide(gt:str, challenge:str, w2:str) -> None:
|
||||
headers = {
|
||||
'Accept': '*/*',
|
||||
'Accept-Language': 'zh-CN,zh;q=0.9',
|
||||
'Connection': 'keep-alive',
|
||||
'Referer': 'https://demos.geetest.com/',
|
||||
'Sec-Fetch-Dest': 'script',
|
||||
'Sec-Fetch-Mode': 'no-cors',
|
||||
'Sec-Fetch-Site': 'cross-site',
|
||||
'Sec-Fetch-Storage-Access': 'active',
|
||||
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.0.0 Safari/537.36',
|
||||
'sec-ch-ua': '"Chromium";v="140", "Not=A?Brand";v="24", "Google Chrome";v="140"',
|
||||
'sec-ch-ua-mobile': '?0',
|
||||
'sec-ch-ua-platform': '"Windows"',
|
||||
}
|
||||
|
||||
response = requests.get(
|
||||
'https://api.geevisit.com/ajax.php?gt=' + gt + '&challenge=' + challenge + '&lang=zh-cn&pt=0&client_type=web&w=' + w2 + '&callback=geetest_' + str(
|
||||
int(round(time.time() * 1000))),
|
||||
headers=headers,
|
||||
timeout=REQUEST_TIMEOUT,
|
||||
)
|
||||
response.raise_for_status()
|
||||
|
||||
def get_picture(gt:str, challenge:str) -> tuple[str, str, list[int], str, str, str]:
|
||||
headers = {
|
||||
'Accept': '*/*',
|
||||
'Accept-Language': 'zh-CN,zh;q=0.9',
|
||||
'Connection': 'keep-alive',
|
||||
'Referer': 'https://demos.geetest.com/',
|
||||
'Sec-Fetch-Dest': 'script',
|
||||
'Sec-Fetch-Mode': 'no-cors',
|
||||
'Sec-Fetch-Site': 'cross-site',
|
||||
'Sec-Fetch-Storage-Access': 'active',
|
||||
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.0.0 Safari/537.36',
|
||||
'sec-ch-ua': '"Chromium";v="140", "Not=A?Brand";v="24", "Google Chrome";v="140"',
|
||||
'sec-ch-ua-mobile': '?0',
|
||||
'sec-ch-ua-platform': '"Windows"',
|
||||
}
|
||||
params = {
|
||||
'is_next': 'true',
|
||||
'type': 'slide3',
|
||||
'gt': gt,
|
||||
'challenge': challenge,
|
||||
'lang': 'zh-cn',
|
||||
'https': 'true',
|
||||
'protocol': 'https://',
|
||||
'offline': 'false',
|
||||
'product': 'embed',
|
||||
'api_server': 'api.geevisit.com',
|
||||
'isPC': 'true',
|
||||
'autoReset': 'true',
|
||||
'width': '100%',
|
||||
'callback': 'geetest_'+str(int(round(time.time() * 1000))),
|
||||
}
|
||||
|
||||
response = requests.get(
|
||||
'https://api.geevisit.com/get.php',
|
||||
params=params,
|
||||
headers=headers,
|
||||
timeout=REQUEST_TIMEOUT,
|
||||
)
|
||||
response.raise_for_status()
|
||||
print(response.text)
|
||||
match = re.search(r'\((.*)\)$', response.text)
|
||||
if match:
|
||||
json_str = match.group(1)
|
||||
return json.loads(json_str)['bg'], json.loads(json_str)['fullbg'], json.loads(json_str)['c'], \
|
||||
json.loads(json_str)['s'], json.loads(json_str)['slice'], json.loads(json_str)['challenge']
|
||||
else:
|
||||
raise ValueError("无法解析 JSONP 响应")
|
||||
|
||||
def req_end(gt:str, challenge:str, w:str) -> dict:
|
||||
|
||||
headers = {
|
||||
'Accept': '*/*',
|
||||
'Accept-Language': 'zh-CN,zh;q=0.9',
|
||||
'Connection': 'keep-alive',
|
||||
'Referer': 'https://demos.geetest.com/',
|
||||
'Sec-Fetch-Dest': 'script',
|
||||
'Sec-Fetch-Mode': 'no-cors',
|
||||
'Sec-Fetch-Site': 'cross-site',
|
||||
'Sec-Fetch-Storage-Access': 'active',
|
||||
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.0.0 Safari/537.36',
|
||||
'sec-ch-ua': '"Chromium";v="140", "Not=A?Brand";v="24", "Google Chrome";v="140"',
|
||||
'sec-ch-ua-mobile': '?0',
|
||||
'sec-ch-ua-platform': '"Windows"',
|
||||
}
|
||||
|
||||
response = requests.get(
|
||||
'https://api.geevisit.com/ajax.php?gt='+gt+'&challenge='+challenge+'&lang=zh-cn&%24_BCm=0&client_type=web&w='+w+'&callback=geetest_'+str(int(round(time.time() * 1000))),
|
||||
headers=headers,
|
||||
timeout=REQUEST_TIMEOUT,
|
||||
)
|
||||
response.raise_for_status()
|
||||
print(response.text)
|
||||
match = re.search(r'\((.*)\)$', response.text)
|
||||
if match:
|
||||
json_str = match.group(1)
|
||||
data = json.loads(json_str)
|
||||
return data
|
||||
else:
|
||||
raise ValueError("无法解析 JSONP 响应")
|
||||
@@ -0,0 +1,113 @@
|
||||
import time
|
||||
import random
|
||||
from typing import Optional
|
||||
|
||||
|
||||
def generate_fake_performance_timing(base_time: Optional[int] = None) -> dict[str, int]:
|
||||
"""
|
||||
生成伪造的浏览器性能时间戳数据
|
||||
|
||||
Args:
|
||||
base_time: 基准时间戳(毫秒),默认使用当前时间
|
||||
|
||||
Returns:
|
||||
dict: 包含所有性能时间戳的字典
|
||||
"""
|
||||
if base_time is None:
|
||||
base_time = int(time.time() * 1000)
|
||||
|
||||
# 定义合理的时间间隔范围(毫秒)
|
||||
intervals = {
|
||||
'fetch': random.randint(1, 2),
|
||||
'domain_lookup_start': random.randint(3, 5),
|
||||
'domain_lookup': random.randint(5, 15),
|
||||
'connect': random.randint(50, 150),
|
||||
'ssl_offset': random.randint(30, 50),
|
||||
'request': random.randint(1, 5),
|
||||
'response': random.randint(20, 100),
|
||||
'response_end': random.randint(1, 3),
|
||||
'unload_start': random.randint(1, 3),
|
||||
'unload': random.randint(1, 5),
|
||||
'dom_loading': random.randint(1, 3),
|
||||
'dom_interactive': random.randint(50, 200),
|
||||
'dom_content_loaded': random.randint(1, 3),
|
||||
'load_event': random.randint(0, 5)
|
||||
}
|
||||
|
||||
timing = {}
|
||||
|
||||
# 按照时间顺序构建
|
||||
timing['navigationStart'] = base_time
|
||||
timing['fetchStart'] = timing['navigationStart'] + intervals['fetch']
|
||||
timing['domainLookupStart'] = timing['fetchStart'] + intervals['domain_lookup_start']
|
||||
timing['domainLookupEnd'] = timing['domainLookupStart'] + intervals['domain_lookup']
|
||||
|
||||
timing['connectStart'] = timing['domainLookupEnd']
|
||||
timing['secureConnectionStart'] = timing['connectStart'] + intervals['ssl_offset']
|
||||
timing['connectEnd'] = timing['connectStart'] + intervals['connect']
|
||||
|
||||
timing['requestStart'] = timing['connectEnd'] + intervals['request']
|
||||
timing['responseStart'] = timing['requestStart'] + intervals['response']
|
||||
timing['responseEnd'] = timing['responseStart'] + intervals['response_end']
|
||||
|
||||
timing['unloadEventStart'] = timing['responseEnd'] + intervals['unload_start']
|
||||
timing['unloadEventEnd'] = timing['unloadEventStart'] + intervals['unload']
|
||||
|
||||
timing['domLoading'] = timing['unloadEventEnd'] + intervals['dom_loading']
|
||||
timing['domInteractive'] = timing['domLoading'] + intervals['dom_interactive']
|
||||
timing['domContentLoadedEventStart'] = timing['domInteractive']
|
||||
timing['domContentLoadedEventEnd'] = timing['domInteractive'] + intervals['dom_content_loaded']
|
||||
timing['domComplete'] = timing['domContentLoadedEventEnd']
|
||||
timing['loadEventStart'] = timing['domComplete']
|
||||
timing['loadEventEnd'] = timing['loadEventStart'] + intervals['load_event']
|
||||
|
||||
# 无重定向的情况
|
||||
timing['redirectStart'] = 0
|
||||
timing['redirectEnd'] = 0
|
||||
|
||||
return timing
|
||||
|
||||
def __ease_out_expo(sep):
|
||||
'''
|
||||
轨迹相关操作
|
||||
'''
|
||||
if sep == 1:
|
||||
return 1
|
||||
else:
|
||||
return 1 - pow(2, -10 * sep)
|
||||
def get_slide_track(distance):
|
||||
"""
|
||||
根据滑动距离生成滑动轨迹
|
||||
:param distance: 需要滑动的距离
|
||||
:return: 滑动轨迹<type 'list'>: [[x,y,t], ...]
|
||||
x: 已滑动的横向距离
|
||||
y: 已滑动的纵向距离, 除起点外, 均为0
|
||||
t: 滑动过程消耗的时间, 单位: 毫秒
|
||||
"""
|
||||
|
||||
if not isinstance(distance, int) or distance < 0:
|
||||
raise ValueError(f"distance类型必须是大于等于0的整数: distance: {distance}, type: {type(distance)}")
|
||||
# 初始化轨迹列表
|
||||
slide_track = [
|
||||
[random.randint(-50, -10), random.randint(-50, -10), 0],
|
||||
[0, 0, 0],
|
||||
]
|
||||
# 共记录count次滑块位置信息
|
||||
count = 10 + int(distance / 2)
|
||||
# 初始化滑动时间
|
||||
t = random.randint(50, 100)
|
||||
# 记录上一次滑动的距离
|
||||
_x = 0
|
||||
_y = 0
|
||||
for i in range(count):
|
||||
# 已滑动的横向距离
|
||||
x = round(__ease_out_expo(i / count) * distance)
|
||||
# y = round(__ease_out_expo(i / count) * 14)
|
||||
# 滑动过程消耗的时间
|
||||
t += random.randint(10, 50)
|
||||
if x == _x:
|
||||
continue
|
||||
slide_track.append([x, _y, t])
|
||||
_x = x
|
||||
slide_track.append(slide_track[-1])
|
||||
return slide_track, slide_track[-1][2]
|
||||
@@ -0,0 +1,159 @@
|
||||
import time
|
||||
import random
|
||||
import json
|
||||
from .trajectory import generate_realistic_trajectory, process_mouse_trajectory, compress_trajectory, TrajectoryEncoder, \
|
||||
H
|
||||
from .crypto import four_random_chart, RSA_jiami_r, AES_O, geetest_base64_encode, encrypt_string, simple_md5
|
||||
from .imaging import download_picture
|
||||
from .network import get_challenge_gt, get_js_address, get_c_s, req_slide, get_picture, req_end
|
||||
from .performance import generate_fake_performance_timing, get_slide_track
|
||||
|
||||
|
||||
def _generate_seed() -> str:
|
||||
return four_random_chart() + four_random_chart() + four_random_chart() + four_random_chart()
|
||||
|
||||
def get_w1(gt:str, challenge:str, str_16:str) -> str:
|
||||
r = RSA_jiami_r(str_16)
|
||||
plaintext = '{"gt":"' + gt + '","challenge":"' + challenge + '","offline":false,"new_captcha":true,"product":"float","width":"300px","https":true,"api_server":"apiv6.geetest.com","protocol":"https://","type":"fullpage","static_servers":["static.geetest.com/","static.geevisit.com/"],"voice":"/static/js/voice.1.2.6.js","click":"/static/js/click.3.1.2.js","beeline":"/static/js/beeline.1.0.1.js","fullpage":"/static/js/fullpage.9.2.0-guwyxh.js","slide":"/static/js/slide.7.9.3.js","geetest":"/static/js/geetest.6.0.9.js","aspect_radio":{"slide":103,"click":128,"voice":128,"beeline":50},"cc":16,"ww":true,"i":"-1!!-1!!-1!!-1!!-1!!-1!!-1!!-1!!-1!!-1!!-1!!-1!!-1!!-1!!-1!!-1!!-1!!-1!!-1!!-1!!-1!!-1!!-1!!-1!!-1!!-1!!-1!!-1!!-1!!-1!!-1!!-1!!-1!!-1!!-1!!-1!!-1!!-1!!-1!!-1!!-1!!-1!!-1!!-1!!-1!!-1!!-1!!-1!!-1!!-1!!-1!!-1!!-1!!-1!!-1!!-1!!-1!!-1!!-1!!-1!!-1!!-1!!-1!!-1!!-1!!-1!!-1!!-1!!-1!!-1!!-1!!-1!!-1!!-1"}'
|
||||
o = AES_O(plaintext, str_16)
|
||||
i = geetest_base64_encode(o)
|
||||
return i['res'] + i['end'] + r
|
||||
|
||||
def get_w2(gt:str, challenge:str, c:list[int], s:str,str_16:str) -> str:
|
||||
# 伪造浏览器性能数据
|
||||
fake_timing = generate_fake_performance_timing()
|
||||
# 映射
|
||||
web_load_time = {
|
||||
"a": fake_timing["navigationStart"],
|
||||
"b": fake_timing["unloadEventStart"],
|
||||
"c": fake_timing["unloadEventEnd"],
|
||||
"d": fake_timing["redirectStart"],
|
||||
"e": fake_timing["redirectEnd"],
|
||||
"f": fake_timing["fetchStart"],
|
||||
"g": fake_timing["domainLookupStart"],
|
||||
"h": fake_timing["domainLookupEnd"],
|
||||
"i": fake_timing["connectStart"],
|
||||
"j": fake_timing["connectEnd"],
|
||||
"k": fake_timing["secureConnectionStart"],
|
||||
"l": fake_timing["requestStart"],
|
||||
"m": fake_timing["responseStart"],
|
||||
"n": fake_timing["responseEnd"],
|
||||
"o": fake_timing["domLoading"],
|
||||
"p": fake_timing["domInteractive"],
|
||||
"q": fake_timing["domContentLoadedEventStart"],
|
||||
"r": fake_timing["domContentLoadedEventEnd"],
|
||||
"s": fake_timing["domComplete"],
|
||||
"t": fake_timing["loadEventStart"],
|
||||
"u": fake_timing["loadEventEnd"]
|
||||
}
|
||||
|
||||
first_time = int(round(time.time() * 1000)) # 伪造脚本开始运行时间
|
||||
|
||||
guiji_yuanshu_shuzu = generate_realistic_trajectory(
|
||||
start_x=random.randint(400, 600), # 起始位置随机
|
||||
start_y=random.randint(400, 500),
|
||||
end_x=853,
|
||||
end_y=288,
|
||||
start_time=first_time
|
||||
)
|
||||
|
||||
|
||||
trajectory = process_mouse_trajectory(guiji_yuanshu_shuzu)["data"]
|
||||
compressed = compress_trajectory(trajectory)
|
||||
tt = encrypt_string(compressed, c, s)
|
||||
|
||||
passtime = str(int(round(time.time() * 1000)) - first_time)
|
||||
|
||||
rp = simple_md5(gt + challenge + passtime)
|
||||
|
||||
plaintext = '{"lang":"zh-cn","type":"fullpage","tt":"'+tt+'","light":"DIV_0","s":"c7c3e21112fe4f741921cb3e4ff9f7cb","h":"321f9af1e098233dbd03f250fd2b5e21","hh":"39bd9cad9e425c3a8f51610fd506e3b3","hi":"09eb21b3ae9542a9bc1e8b63b3d9a467","vip_order":-1,"ct":-1,"ep":{"v":"9.2.0-guwyxh","te":false,"$_BBn":true,"ven":"Google Inc. (AMD)","ren":"ANGLE (AMD, AMD Radeon RX 6750 GRE 12GB (0x000073DF) Direct3D11 vs_5_0 ps_5_0, D3D11)","fp":'+json.dumps(guiji_yuanshu_shuzu[0], separators=(',', ':'))+',"lp":'+json.dumps(guiji_yuanshu_shuzu[-1], separators=(',', ':'))+',"em":{"ph":0,"cp":0,"ek":"11","wd":1,"nt":0,"si":0,"sc":0},"tm":'+json.dumps(web_load_time, separators=(',', ':'))+',"dnf":"dnf","by":0},"passtime":'+passtime+',"rp":"'+rp+'","captcha_token":"112439067","tsfq":"xovrayel"}'
|
||||
|
||||
|
||||
result = geetest_base64_encode(AES_O(plaintext, str_16))
|
||||
|
||||
return result['res']+result['end']+result['end']
|
||||
|
||||
def get_w3(str_16:str, challenge:str, hkjl:int, c:list[int], s:str, gt:str) -> str:
|
||||
encoder = TrajectoryEncoder()
|
||||
u = RSA_jiami_r(str_16)
|
||||
# 伪造浏览器性能数据
|
||||
fake_timing = generate_fake_performance_timing()
|
||||
# 映射
|
||||
web_load_time = {
|
||||
"a": fake_timing["navigationStart"],
|
||||
"b": fake_timing["unloadEventStart"],
|
||||
"c": fake_timing["unloadEventEnd"],
|
||||
"d": fake_timing["redirectStart"],
|
||||
"e": fake_timing["redirectEnd"],
|
||||
"f": fake_timing["fetchStart"],
|
||||
"g": fake_timing["domainLookupStart"],
|
||||
"h": fake_timing["domainLookupEnd"],
|
||||
"i": fake_timing["connectStart"],
|
||||
"j": fake_timing["connectEnd"],
|
||||
"k": fake_timing["secureConnectionStart"],
|
||||
"l": fake_timing["requestStart"],
|
||||
"m": fake_timing["responseStart"],
|
||||
"n": fake_timing["responseEnd"],
|
||||
"o": fake_timing["domLoading"],
|
||||
"p": fake_timing["domInteractive"],
|
||||
"q": fake_timing["domContentLoadedEventStart"],
|
||||
"r": fake_timing["domContentLoadedEventEnd"],
|
||||
"s": fake_timing["domComplete"],
|
||||
"t": fake_timing["loadEventStart"],
|
||||
"u": fake_timing["loadEventEnd"]
|
||||
}
|
||||
|
||||
|
||||
|
||||
trajectory = get_slide_track(hkjl)[0]
|
||||
print(trajectory)
|
||||
|
||||
# trajectory = generator.generate(target_x)
|
||||
userresponse = H(trajectory[-1][0], challenge)
|
||||
|
||||
result1 = encoder.encode(trajectory)
|
||||
aa = encoder.encrypt_string(result1, c, s)
|
||||
|
||||
passtime = str(trajectory[-1][2])
|
||||
|
||||
|
||||
rp = simple_md5(gt + challenge[:32] + passtime)
|
||||
|
||||
plaintext = '{"lang":"zh-cn","userresponse":"'+userresponse+'","passtime":'+passtime+',"imgload":50,"aa":"'+aa+'","ep":{"v":"7.9.3","$_BIT":false,"me":true,"tm":'+json.dumps(web_load_time, separators=(',', ':'))+',"td":-1},"h9s9":"1816378497","rp":"'+rp+'"}'
|
||||
|
||||
h = geetest_base64_encode(AES_O(plaintext, str_16))['res']
|
||||
return h+u
|
||||
|
||||
def run_solver() -> None:
|
||||
# 16位字符串
|
||||
str_16 = _generate_seed()
|
||||
|
||||
# 访问第一个链接获取gt challeneg
|
||||
gt, challenge = get_challenge_gt()
|
||||
|
||||
# 访问第二个链接获取js文件地址(后续需要用到)
|
||||
get_js_address(gt)
|
||||
|
||||
# 获取第一个w值
|
||||
w1 = get_w1(gt, challenge, str_16)
|
||||
|
||||
# 访问第三个链接获取数组c与字符串s
|
||||
c, s = get_c_s(gt, challenge, w1)
|
||||
|
||||
# 获取第二个w值
|
||||
w2 = get_w2(gt, challenge, c, s, str_16)
|
||||
|
||||
# 访问第四个链接确认进入滑动
|
||||
req_slide(gt, challenge, w2)
|
||||
|
||||
# 访问第五个链接获取最后一个w值所需要的加密参数
|
||||
bg, fullbg, c, s, slice, challenge = get_picture(gt, challenge)
|
||||
|
||||
# 识别缺口位置得到滑块距离
|
||||
hkjl = download_picture(bg, fullbg, slice)
|
||||
|
||||
# 获取第三个w值
|
||||
w3 = get_w3(str_16,challenge,hkjl,c,s,gt)
|
||||
|
||||
# 最后的验证
|
||||
message = req_end(gt, challenge, w3)
|
||||
@@ -0,0 +1,628 @@
|
||||
import random
|
||||
import math
|
||||
from typing import Any, Optional
|
||||
|
||||
|
||||
# 生成类人的鼠标轨迹
|
||||
def generate_realistic_trajectory(start_x:int, start_y:int, end_x:int, end_y:int, start_time:int) -> list[Any]:
|
||||
"""生成类人的鼠标轨迹"""
|
||||
trajectory = []
|
||||
|
||||
# 计算距离和步数
|
||||
distance = ((end_x - start_x) ** 2 + (end_y - start_y) ** 2) ** 0.5
|
||||
steps = int(distance / 2) + random.randint(5, 15) # 根据距离动态调整步数
|
||||
|
||||
current_time = start_time
|
||||
current_x, current_y = start_x, start_y
|
||||
|
||||
for i in range(steps):
|
||||
# 使用贝塞尔曲线模拟自然移动
|
||||
progress = i / steps
|
||||
|
||||
# 添加缓动函数(开始快,中间慢,结束快)
|
||||
if progress < 0.3:
|
||||
eased = progress / 0.3 * 0.2
|
||||
elif progress < 0.7:
|
||||
eased = 0.2 + (progress - 0.3) / 0.4 * 0.5
|
||||
else:
|
||||
eased = 0.7 + (progress - 0.7) / 0.3 * 0.3
|
||||
|
||||
# 计算目标位置(添加随机抖动)
|
||||
target_x = start_x + (end_x - start_x) * eased
|
||||
target_y = start_y + (end_y - start_y) * eased
|
||||
|
||||
# 添加微小的随机偏移(模拟手抖)
|
||||
jitter_x = random.uniform(-0.5, 0.5)
|
||||
jitter_y = random.uniform(-0.5, 0.5)
|
||||
|
||||
current_x = int(target_x + jitter_x)
|
||||
current_y = int(target_y + jitter_y)
|
||||
|
||||
# 随机时间间隔(3-25ms,符合人类反应)
|
||||
time_delta = random.choices(
|
||||
[3, 4, 5, 6, 7, 8, 10, 12, 15, 20, 24],
|
||||
weights=[5, 8, 10, 12, 10, 8, 5, 3, 2, 1, 1]
|
||||
)[0]
|
||||
current_time += time_delta
|
||||
|
||||
trajectory.append([
|
||||
"move",
|
||||
current_x,
|
||||
current_y,
|
||||
current_time,
|
||||
"pointermove"
|
||||
])
|
||||
|
||||
# 偶尔在同一位置停留(模拟视觉确认)
|
||||
if random.random() < 0.15:
|
||||
trajectory.append([
|
||||
"move",
|
||||
current_x,
|
||||
current_y,
|
||||
current_time + random.randint(5, 15),
|
||||
"pointermove"
|
||||
])
|
||||
current_time += random.randint(5, 15)
|
||||
|
||||
# 到达目标后的悬停
|
||||
hover_time = random.randint(50, 150)
|
||||
for _ in range(random.randint(2, 5)):
|
||||
current_time += random.randint(8, 25)
|
||||
trajectory.append([
|
||||
"move",
|
||||
end_x + random.randint(-1, 1),
|
||||
end_y + random.randint(-1, 1),
|
||||
current_time,
|
||||
"pointermove"
|
||||
])
|
||||
|
||||
current_time += hover_time
|
||||
|
||||
# 点击事件
|
||||
trajectory.append([
|
||||
"down",
|
||||
end_x,
|
||||
end_y,
|
||||
current_time,
|
||||
"pointerdown"
|
||||
])
|
||||
|
||||
trajectory.append([
|
||||
"focus",
|
||||
current_time + 1
|
||||
])
|
||||
|
||||
click_duration = random.randint(80, 130)
|
||||
trajectory.append([
|
||||
"up",
|
||||
end_x,
|
||||
end_y,
|
||||
current_time + click_duration,
|
||||
"pointerup"
|
||||
])
|
||||
|
||||
return trajectory
|
||||
|
||||
# 处理原始轨迹数组
|
||||
def process_mouse_trajectory(events:list[Any], max_records: Optional[int] = None) -> dict[str,Any]:
|
||||
"""
|
||||
处理鼠标/触摸轨迹数据,将绝对坐标转换为相对坐标和时间差
|
||||
|
||||
参数:
|
||||
events: 原始事件数据列表
|
||||
max_records: 最大保留记录数(None表示保留全部)
|
||||
|
||||
返回:
|
||||
处理后的事件列表
|
||||
"""
|
||||
if not events or len(events) == 0:
|
||||
return []
|
||||
|
||||
# 初始化变量
|
||||
prev_x = 0 # 上一个X坐标
|
||||
prev_y = 0 # 上一个Y坐标
|
||||
prev_time = 0 # 上一个时间戳
|
||||
result = [] # 结果数组
|
||||
first_event = None # 第一个事件
|
||||
last_event = None # 最后一个事件
|
||||
|
||||
# 移动类事件(包含坐标信息)
|
||||
MOVE_EVENTS = ["move", "mousemove", "touchmove", "pointermove"]
|
||||
|
||||
# 点击类事件(仅时间信息)
|
||||
CLICK_EVENTS = ["down", "up", "click", "mousedown", "mouseup",
|
||||
"touchstart", "touchend", "pointerdown", "pointerup"]
|
||||
|
||||
# 特殊事件(仅时间信息)
|
||||
TIME_ONLY_EVENTS = ["focus", "blur", "keydown", "keyup"]
|
||||
|
||||
# 如果设置了最大记录数,只处理最后N条
|
||||
start_index = 0
|
||||
if max_records and len(events) > max_records:
|
||||
start_index = len(events) - max_records
|
||||
|
||||
# 遍历事件
|
||||
for i in range(start_index, len(events)):
|
||||
event = events[i]
|
||||
event_type = event[0]
|
||||
|
||||
# 处理移动类事件(包含X, Y坐标)
|
||||
if event_type in MOVE_EVENTS:
|
||||
x = event[1]
|
||||
y = event[2]
|
||||
timestamp = event[3]
|
||||
|
||||
# 记录第一个和最后一个事件
|
||||
if first_event is None:
|
||||
first_event = event
|
||||
last_event = event
|
||||
|
||||
# 计算相对坐标差值
|
||||
delta_x = x - prev_x
|
||||
delta_y = y - prev_y
|
||||
|
||||
# 计算时间差
|
||||
if prev_time == 0:
|
||||
time_diff = 0 # 第一个事件时间差为0
|
||||
else:
|
||||
time_diff = timestamp - prev_time
|
||||
|
||||
# 添加到结果数组
|
||||
result.append([
|
||||
event_type,
|
||||
[delta_x, delta_y],
|
||||
time_diff
|
||||
])
|
||||
|
||||
# 更新上一次的值
|
||||
prev_x = x
|
||||
prev_y = y
|
||||
prev_time = timestamp
|
||||
|
||||
# 处理点击类事件(包含坐标但只记录时间差)
|
||||
elif event_type in CLICK_EVENTS:
|
||||
timestamp = event[3] if len(event) > 3 else event[1]
|
||||
|
||||
# 计算时间差
|
||||
if prev_time == 0:
|
||||
time_diff = 0
|
||||
else:
|
||||
time_diff = timestamp - prev_time
|
||||
|
||||
# 添加到结果数组(坐标差为[0,0])
|
||||
result.append([
|
||||
event_type,
|
||||
[0, 0],
|
||||
time_diff
|
||||
])
|
||||
|
||||
prev_time = timestamp
|
||||
|
||||
# 处理仅时间类事件(如focus)
|
||||
elif event_type in TIME_ONLY_EVENTS:
|
||||
timestamp = event[1]
|
||||
|
||||
# 计算时间差
|
||||
if prev_time == 0:
|
||||
time_diff = 0
|
||||
else:
|
||||
time_diff = timestamp - prev_time
|
||||
|
||||
# 添加到结果数组(仅包含时间差)
|
||||
result.append([
|
||||
event_type,
|
||||
time_diff
|
||||
])
|
||||
|
||||
prev_time = timestamp
|
||||
|
||||
return {
|
||||
"data": result,
|
||||
"first_event": first_event,
|
||||
"last_event": last_event,
|
||||
"total_events": len(result)
|
||||
}
|
||||
|
||||
def compress_trajectory(e:list[Any]) -> str:
|
||||
"""
|
||||
压缩轨迹数据的完整实现
|
||||
|
||||
Args:
|
||||
e: 轨迹数据列表
|
||||
|
||||
Returns:
|
||||
压缩后的 Base64 编码字符串
|
||||
"""
|
||||
# 事件类型映射
|
||||
p = {
|
||||
"move": 0,
|
||||
"down": 1,
|
||||
"up": 2,
|
||||
"scroll": 3,
|
||||
"focus": 4,
|
||||
"blur": 5,
|
||||
"unload": 6,
|
||||
"unknown": 7
|
||||
}
|
||||
|
||||
def h(e, t):
|
||||
"""
|
||||
填充二进制字符串
|
||||
|
||||
Args:
|
||||
e: 数值
|
||||
t: 目标长度
|
||||
|
||||
Returns:
|
||||
填充后的二进制字符串
|
||||
"""
|
||||
n = bin(e)[2:] # 转为二进制并去掉 '0b' 前缀
|
||||
r = ""
|
||||
o = len(n) + 1
|
||||
while o <= t:
|
||||
r += "0"
|
||||
o += 1
|
||||
return r + n
|
||||
|
||||
def f(e):
|
||||
"""
|
||||
压缩事件类型数组
|
||||
|
||||
Args:
|
||||
e: 事件类型列表
|
||||
|
||||
Returns:
|
||||
压缩后的二进制字符串
|
||||
"""
|
||||
t = []
|
||||
n = len(e)
|
||||
r = 0
|
||||
|
||||
# 游程编码(Run-Length Encoding)
|
||||
while r < n:
|
||||
o = e[r]
|
||||
i = 0
|
||||
while True:
|
||||
if 16 <= i:
|
||||
break
|
||||
s = r + i + 1
|
||||
if n <= s:
|
||||
break
|
||||
if e[s] != o:
|
||||
break
|
||||
i += 1
|
||||
|
||||
r = r + 1 + i
|
||||
a = p[o]
|
||||
if i != 0:
|
||||
t.append(8 | a) # 设置重复标志位
|
||||
t.append(i - 1)
|
||||
else:
|
||||
t.append(a)
|
||||
|
||||
# 编码长度信息
|
||||
_ = h(32768 | n, 16)
|
||||
c = ""
|
||||
for l in range(len(t)):
|
||||
c += h(t[l], 4)
|
||||
|
||||
return _ + c
|
||||
|
||||
def c(e, t):
|
||||
"""
|
||||
对数组每个元素应用函数
|
||||
|
||||
Args:
|
||||
e: 输入数组
|
||||
t: 转换函数
|
||||
|
||||
Returns:
|
||||
转换后的数组
|
||||
"""
|
||||
n = []
|
||||
for r in range(len(e)):
|
||||
n.append(t(e[r]))
|
||||
return n
|
||||
|
||||
def d(e, t):
|
||||
"""
|
||||
压缩数值数组(游程编码 + 变长编码)
|
||||
|
||||
Args:
|
||||
e: 数值数组
|
||||
t: 是否为坐标数据(需要过滤符号位)
|
||||
|
||||
Returns:
|
||||
压缩后的二进制字符串
|
||||
"""
|
||||
|
||||
# 第一步:限制数值范围到 [-32767, 32767]
|
||||
def limit_value(val):
|
||||
limit = 32767
|
||||
return max(-limit, min(limit, val))
|
||||
|
||||
e = c(e, limit_value)
|
||||
|
||||
# 第二步:游程编码(Run-Length Encoding)
|
||||
n = len(e)
|
||||
r = 0
|
||||
o = []
|
||||
|
||||
while r < n:
|
||||
i = 1
|
||||
s = e[r]
|
||||
a = abs(s)
|
||||
|
||||
# 统计连续相同的值
|
||||
while r + i < n and e[r + i] == s and a < 127 and i < 127:
|
||||
i += 1
|
||||
|
||||
if i > 1:
|
||||
# 重复值编码格式:
|
||||
# 位15: 符号标志 (1=负数49152, 0=正数32768)
|
||||
# 位14-7: 重复次数 (i)
|
||||
# 位6-0: 绝对值 (a)
|
||||
o.append((49152 if s < 0 else 32768) | (i << 7) | a)
|
||||
else:
|
||||
o.append(s)
|
||||
|
||||
r += i
|
||||
|
||||
e = o
|
||||
|
||||
# 第三步:变长编码
|
||||
r = [] # 存储每个数字的十六进制位数
|
||||
o = [] # 存储实际数值
|
||||
|
||||
for val in e:
|
||||
# 计算需要多少个十六进制位(每位4 bit)
|
||||
if val == 0:
|
||||
bits = 1
|
||||
else:
|
||||
# 方法1:使用对数(与原JS一致)
|
||||
bits = math.ceil(math.log(abs(val) + 1) / math.log(16))
|
||||
# 方法2:直接计算(更快)
|
||||
# bits = len(format(abs(val), 'x'))
|
||||
|
||||
bits = max(1, bits)
|
||||
|
||||
r.append(h(bits - 1, 2)) # 2位十六进制存储位数信息
|
||||
o.append(h(abs(val), 4 * bits)) # 实际值
|
||||
|
||||
i = "".join(r) # 元数据
|
||||
s = "".join(o) # 数据
|
||||
|
||||
# 第四步:符号位编码(仅用于坐标数据)
|
||||
if t:
|
||||
# 过滤掉:0值 和 已编码符号的压缩值(位15=1)
|
||||
filtered = [x for x in e if x != 0 and (x >> 15) != 1]
|
||||
n = "".join(["1" if x < 0 else "0" for x in filtered])
|
||||
else:
|
||||
n = ""
|
||||
|
||||
# 最终格式:[头部16位][元数据][数据][符号位]
|
||||
# 头部:最高位置1 + 数组长度
|
||||
return h(32768 | len(e), 16) + i + s + n
|
||||
|
||||
# 主函数:数据分离
|
||||
t = [] # 事件类型
|
||||
n = [] # 时间差
|
||||
r = [] # X坐标
|
||||
o = [] # Y坐标
|
||||
|
||||
for i in range(len(e)):
|
||||
a = e[i]
|
||||
length = len(a)
|
||||
|
||||
t.append(a[0])
|
||||
n.append(a[1] if length == 2 else a[2])
|
||||
|
||||
if length == 3:
|
||||
r.append(a[1][0])
|
||||
o.append(a[1][1])
|
||||
|
||||
# 压缩各部分
|
||||
c_str = f(t) + d(n, False) + d(r, True) + d(o, True)
|
||||
|
||||
# 填充到6的倍数
|
||||
l = len(c_str)
|
||||
if l % 6 != 0:
|
||||
c_str += h(0, 6 - l % 6)
|
||||
|
||||
# Base64编码
|
||||
def u(e):
|
||||
"""Base64编码"""
|
||||
t = ""
|
||||
n = len(e) // 6
|
||||
base64_chars = "()*,-./0123456789:?@ABCDEFGHIJKLMNOPQRSTUVWXYZ_abcdefghijklmnopqrstuvwxyz~"
|
||||
|
||||
for r in range(n):
|
||||
# 每次取6位二进制
|
||||
binary_str = e[6 * r: 6 * (r + 1)]
|
||||
index = int(binary_str, 2)
|
||||
t += base64_chars[index]
|
||||
|
||||
return t
|
||||
|
||||
return u(c_str)
|
||||
|
||||
class TrajectoryEncoder:
|
||||
def __init__(self) -> None:
|
||||
self.CHARSET = "()*,-./0123456789:?@ABCDEFGHIJKLMNOPQRSTUVWXYZ_abcdefghijklmnopqr"
|
||||
self.BASE = len(self.CHARSET) # 64
|
||||
self.DIRECTION_CHARS = "stuvwxyz~"
|
||||
self.DIRECTION_PATTERNS = [
|
||||
[1, 0], # s
|
||||
[2, 0], # t
|
||||
[1, -1], # u
|
||||
[1, 1], # v
|
||||
[0, 1], # w
|
||||
[0, -1], # x
|
||||
[3, 0], # y
|
||||
[2, -1], # z
|
||||
[2, 1] # ~
|
||||
]
|
||||
|
||||
def encode_number(self, num):
|
||||
"""编码单个数值为64进制"""
|
||||
abs_num = abs(num)
|
||||
high_index = abs_num // self.BASE
|
||||
low_index = abs_num % self.BASE
|
||||
|
||||
result = ""
|
||||
|
||||
# 负数标记
|
||||
if num < 0:
|
||||
result += "!"
|
||||
|
||||
# 高位(当值>=64时)
|
||||
if high_index > 0 and high_index < self.BASE:
|
||||
result += "$"
|
||||
result += self.CHARSET[high_index]
|
||||
|
||||
# 低位
|
||||
result += self.CHARSET[low_index]
|
||||
|
||||
return result
|
||||
|
||||
def compress_trajectory(self, points):
|
||||
"""压缩轨迹:计算相邻点差值"""
|
||||
compressed = []
|
||||
time_accumulator = 0
|
||||
|
||||
for i in range(len(points) - 1):
|
||||
dx = points[i + 1][0] - points[i][0] # 不要用abs
|
||||
dy = points[i + 1][1] - points[i][1] # 不要用abs
|
||||
dt = abs(points[i + 1][2] - points[i][2]) # 时间可以用abs
|
||||
|
||||
# 跳过完全相同的点
|
||||
if dx == 0 and dy == 0 and dt == 0:
|
||||
continue
|
||||
|
||||
# 位置不变只累积时间
|
||||
if dx == 0 and dy == 0:
|
||||
time_accumulator += dt
|
||||
else:
|
||||
compressed.append([dx, dy, dt + time_accumulator])
|
||||
time_accumulator = 0
|
||||
|
||||
# 处理剩余时间
|
||||
if time_accumulator != 0:
|
||||
compressed.append([0, 0, time_accumulator])
|
||||
|
||||
return compressed
|
||||
|
||||
def get_direction_code(self, dx, dy):
|
||||
"""识别是否匹配方向模式"""
|
||||
for i, pattern in enumerate(self.DIRECTION_PATTERNS):
|
||||
if dx == pattern[0] and dy == pattern[1]:
|
||||
return self.DIRECTION_CHARS[i]
|
||||
return None
|
||||
|
||||
def encode(self, trajectory):
|
||||
"""
|
||||
编码轨迹
|
||||
trajectory: [[x, y, timestamp], ...]
|
||||
返回: 编码后的字符串
|
||||
"""
|
||||
compressed = self.compress_trajectory(trajectory)
|
||||
|
||||
x_encoded = []
|
||||
y_encoded = []
|
||||
t_encoded = []
|
||||
|
||||
for dx, dy, dt in compressed:
|
||||
direction_code = self.get_direction_code(dx, dy)
|
||||
|
||||
if direction_code:
|
||||
# 匹配到方向模式,只记录y
|
||||
y_encoded.append(direction_code)
|
||||
else:
|
||||
# 不匹配,完整编码x和y
|
||||
x_encoded.append(self.encode_number(dx))
|
||||
y_encoded.append(self.encode_number(dy))
|
||||
|
||||
# 时间总是编码
|
||||
t_encoded.append(self.encode_number(dt))
|
||||
|
||||
# 拼接:x坐标 !! y坐标 !! 时间戳
|
||||
return "".join(x_encoded) + "!!" + "".join(y_encoded) + "!!" + "".join(t_encoded)
|
||||
|
||||
def encrypt_string(self,e, t, n):
|
||||
"""
|
||||
JS加密函数的Python实现
|
||||
|
||||
参数:
|
||||
e: 原始字符串
|
||||
t: 加密参数数组
|
||||
n: 十六进制字符串
|
||||
"""
|
||||
if not t or not n:
|
||||
return e
|
||||
|
||||
o = 0 # 偏移量
|
||||
i = e # 结果字符串
|
||||
s = t[0] # 12
|
||||
a = t[2] # 98
|
||||
_ = t[4] # 43
|
||||
|
||||
# 每次读取2个字符(十六进制)
|
||||
while o < len(n):
|
||||
r = n[o:o + 2] # 取2个字符
|
||||
if len(r) < 2:
|
||||
break
|
||||
o += 2
|
||||
|
||||
# 解析十六进制
|
||||
c = int(r, 16)
|
||||
|
||||
# 转换为字符
|
||||
l = chr(c)
|
||||
|
||||
# 计算插入位置: (s * c^2 + a * c + _) % len(e)
|
||||
u = (s * c * c + a * c + _) % len(e)
|
||||
|
||||
# 在位置u插入字符
|
||||
i = i[:u] + l + i[u:]
|
||||
|
||||
return i
|
||||
|
||||
def H(t:int, e:str) -> str:
|
||||
# 解析后缀
|
||||
n = e[-2:]
|
||||
r = []
|
||||
for char in n:
|
||||
o = ord(char)
|
||||
r.append(o - 87 if o > 57 else o - 48)
|
||||
n = 36 * r[0] + r[1]
|
||||
|
||||
# 计算目标值
|
||||
a = round(t) + n
|
||||
|
||||
# 构建字符池
|
||||
_ = [[], [], [], [], []]
|
||||
c = {}
|
||||
u = 0
|
||||
for char in e[:-2]:
|
||||
if char not in c:
|
||||
c[char] = 1
|
||||
_[u].append(char)
|
||||
u = (u + 1) % 5
|
||||
|
||||
# 生成结果
|
||||
f = a
|
||||
d = 4
|
||||
p = ""
|
||||
g = [1, 2, 5, 10, 50]
|
||||
|
||||
while f > 0:
|
||||
if f >= g[d]:
|
||||
h = int(random.random() * len(_[d]))
|
||||
p += _[d][h]
|
||||
f -= g[d]
|
||||
else:
|
||||
_.pop(d)
|
||||
g.pop(d)
|
||||
d -= 1
|
||||
|
||||
return p
|
||||
@@ -0,0 +1,134 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
斗鱼自动登录工具 - Python版
|
||||
|
||||
功能:
|
||||
1. 自动登录斗鱼账号
|
||||
2. 自动过极验滑块验证
|
||||
3. 自动获取邮箱验证码
|
||||
4. 批量获取Cookie
|
||||
"""
|
||||
|
||||
import sys
|
||||
import argparse
|
||||
from pathlib import Path
|
||||
from loguru import logger
|
||||
|
||||
from douyu import DouyuLogin, Config
|
||||
from utils import setup_logger, save_cookie, load_accounts
|
||||
|
||||
|
||||
def login_single(config: Config, account_index: int = 0) -> str:
|
||||
"""
|
||||
单账号登录
|
||||
|
||||
Args:
|
||||
config: 配置对象
|
||||
account_index: 账号索引
|
||||
|
||||
Returns:
|
||||
Cookie字符串
|
||||
"""
|
||||
accounts = config.get_accounts()
|
||||
|
||||
if account_index >= len(accounts):
|
||||
logger.error(f"账号索引 {account_index} 超出范围,共有 {len(accounts)} 个账号")
|
||||
return ""
|
||||
|
||||
account = accounts[account_index]
|
||||
logger.info(f"登录账号: {account.username}")
|
||||
|
||||
# 创建登录器
|
||||
proxy = config.get_proxy()
|
||||
proxy_url = None
|
||||
if proxy.enabled:
|
||||
proxy_url = {
|
||||
'http': proxy.http or proxy.https,
|
||||
'https': proxy.https or proxy.http,
|
||||
}
|
||||
|
||||
loginer = DouyuLogin(account, proxy=proxy_url)
|
||||
|
||||
# 执行登录
|
||||
result = loginer.login()
|
||||
|
||||
if result.success:
|
||||
# 保存Cookie
|
||||
cookie_dir = config.get_cookie_dir()
|
||||
save_cookie(account.username, result.cookie, cookie_dir)
|
||||
return result.cookie
|
||||
else:
|
||||
logger.error(f"登录失败: {result.message}")
|
||||
return ""
|
||||
|
||||
|
||||
def login_batch(config: Config) -> dict:
|
||||
"""
|
||||
批量登录
|
||||
|
||||
Returns:
|
||||
{username: cookie} 字典
|
||||
"""
|
||||
accounts = config.get_accounts()
|
||||
results = {}
|
||||
|
||||
logger.info(f"开始批量登录,共 {len(accounts)} 个账号")
|
||||
|
||||
for i, account in enumerate(accounts):
|
||||
logger.info(f"[{i+1}/{len(accounts)}] 登录账号: {account.username}")
|
||||
|
||||
try:
|
||||
cookie = login_single(config, i)
|
||||
if cookie:
|
||||
results[account.username] = cookie
|
||||
logger.success(f"账号 {account.username} 登录成功")
|
||||
else:
|
||||
logger.error(f"账号 {account.username} 登录失败")
|
||||
except Exception as e:
|
||||
logger.error(f"账号 {account.username} 登录异常: {e}")
|
||||
|
||||
# 统计结果
|
||||
success_count = len(results)
|
||||
total_count = len(accounts)
|
||||
logger.info(f"批量登录完成: 成功 {success_count}/{total_count}")
|
||||
|
||||
return results
|
||||
|
||||
|
||||
def main():
|
||||
"""主函数"""
|
||||
parser = argparse.ArgumentParser(description='斗鱼自动登录工具')
|
||||
parser.add_argument('-c', '--config', default='config.yaml', help='配置文件路径')
|
||||
parser.add_argument('-i', '--index', type=int, default=0, help='单账号登录时的账号索引')
|
||||
parser.add_argument('-b', '--batch', action='store_true', help='批量登录模式')
|
||||
parser.add_argument('-v', '--verbose', action='store_true', help='详细日志')
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
# 配置日志
|
||||
log_level = "DEBUG" if args.verbose else "INFO"
|
||||
setup_logger(level=log_level)
|
||||
|
||||
# 加载配置
|
||||
try:
|
||||
config = Config(args.config)
|
||||
except FileNotFoundError as e:
|
||||
logger.error(str(e))
|
||||
sys.exit(1)
|
||||
|
||||
# 执行登录
|
||||
if args.batch:
|
||||
results = login_batch(config)
|
||||
print(f"\n登录结果: 成功 {len(results)} 个账号")
|
||||
else:
|
||||
cookie = login_single(config, args.index)
|
||||
if cookie:
|
||||
print(f"\n登录成功!")
|
||||
print(f"Cookie: {cookie[:50]}...")
|
||||
else:
|
||||
print("\n登录失败")
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -0,0 +1,25 @@
|
||||
[project]
|
||||
name = "douyu-login-py"
|
||||
version = "0.1.0"
|
||||
description = "斗鱼自动登录工具 - Python版"
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.12,<3.13"
|
||||
dependencies = [
|
||||
"requests>=2.31.0",
|
||||
"pycryptodome>=3.19.0",
|
||||
"numpy>=1.24.0",
|
||||
"opencv-python-headless>=4.8.0",
|
||||
"Pillow>=10.0.0",
|
||||
"PyYAML>=6.0",
|
||||
"loguru>=0.7.0",
|
||||
]
|
||||
|
||||
[project.scripts]
|
||||
douyu-login = "main:main"
|
||||
|
||||
[build-system]
|
||||
requires = ["hatchling"]
|
||||
build-backend = "hatchling.build"
|
||||
|
||||
[tool.hatch.build.targets.wheel]
|
||||
packages = ["douyu", "geetest", "utils"]
|
||||
@@ -0,0 +1,7 @@
|
||||
requests>=2.31.0
|
||||
pycryptodome>=3.19.0
|
||||
numpy>=1.24.0
|
||||
opencv-python-headless>=4.8.0
|
||||
Pillow>=10.0.0
|
||||
PyYAML>=6.0
|
||||
loguru>=0.7.0
|
||||
@@ -0,0 +1,98 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
测试登录功能
|
||||
"""
|
||||
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
# 添加项目根目录到path
|
||||
sys.path.insert(0, str(Path(__file__).parent))
|
||||
|
||||
from douyu.config import Config, Account
|
||||
from douyu.crypto import encrypt_password, encrypt_nickname_or_phone
|
||||
from douyu.email_verifier import EmailVerifier
|
||||
from utils.logger import setup_logger
|
||||
|
||||
|
||||
def test_crypto():
|
||||
"""测试加密功能"""
|
||||
print("=== 测试加密功能 ===")
|
||||
|
||||
# 测试密码加密
|
||||
password = "test_password"
|
||||
encrypted = encrypt_password(password)
|
||||
print(f"密码: {password}")
|
||||
print(f"MD5: {encrypted}")
|
||||
|
||||
# 测试用户名加密
|
||||
username = "test_user"
|
||||
encrypted = encrypt_nickname_or_phone(username)
|
||||
print(f"用户名: {username}")
|
||||
print(f"加密后: {encrypted}")
|
||||
|
||||
print()
|
||||
|
||||
|
||||
def test_config():
|
||||
"""测试配置加载"""
|
||||
print("=== 测试配置加载 ===")
|
||||
|
||||
try:
|
||||
config = Config("config.yaml")
|
||||
accounts = config.get_accounts()
|
||||
|
||||
print(f"加载了 {len(accounts)} 个账号")
|
||||
for i, acc in enumerate(accounts):
|
||||
print(f" [{i}] {acc.username} - {acc.email}")
|
||||
|
||||
proxy = config.get_proxy()
|
||||
print(f"代理: {'启用' if proxy.enabled else '禁用'}")
|
||||
|
||||
except FileNotFoundError as e:
|
||||
print(f"配置文件不存在: {e}")
|
||||
except Exception as e:
|
||||
print(f"加载配置失败: {e}")
|
||||
|
||||
print()
|
||||
|
||||
|
||||
def test_email_verifier():
|
||||
"""测试邮箱验证器(不实际连接)"""
|
||||
print("=== 测试邮箱验证器 ===")
|
||||
|
||||
# 测试验证码提取
|
||||
verifier = EmailVerifier("", "", "", "")
|
||||
|
||||
# 模拟邮件内容
|
||||
test_cases = [
|
||||
"您的验证码是:123456,请在5分钟内完成验证。",
|
||||
"验证码:654321",
|
||||
"Your verification code is 789012",
|
||||
"【斗鱼】安全验证码:345678,切勿泄露给他人!",
|
||||
]
|
||||
|
||||
for text in test_cases:
|
||||
code = verifier._extract_verification_code(text)
|
||||
print(f"文本: {text[:30]}...")
|
||||
print(f"验证码: {code}")
|
||||
print()
|
||||
|
||||
print()
|
||||
|
||||
|
||||
def main():
|
||||
"""运行所有测试"""
|
||||
setup_logger(level="INFO")
|
||||
|
||||
print("斗鱼自动登录工具 - 测试\n")
|
||||
|
||||
test_crypto()
|
||||
test_config()
|
||||
test_email_verifier()
|
||||
|
||||
print("测试完成!")
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -0,0 +1,6 @@
|
||||
"""工具模块"""
|
||||
|
||||
from .logger import setup_logger
|
||||
from .helpers import load_accounts, save_cookie
|
||||
|
||||
__all__ = ["setup_logger", "load_accounts", "save_cookie"]
|
||||
@@ -0,0 +1,95 @@
|
||||
"""辅助工具函数"""
|
||||
|
||||
import json
|
||||
from pathlib import Path
|
||||
from typing import List
|
||||
from loguru import logger
|
||||
|
||||
from douyu.config import Account
|
||||
|
||||
|
||||
def load_accounts(config_path: str = "config.yaml") -> List[Account]:
|
||||
"""加载账号列表"""
|
||||
from douyu.config import Config
|
||||
|
||||
config = Config(config_path)
|
||||
accounts = config.get_accounts()
|
||||
|
||||
logger.info(f"加载了 {len(accounts)} 个账号")
|
||||
return accounts
|
||||
|
||||
|
||||
def save_cookie(username: str, cookie: str, cookie_dir: str = "data/cookies") -> str:
|
||||
"""
|
||||
保存Cookie到文件
|
||||
|
||||
Args:
|
||||
username: 用户名
|
||||
cookie: Cookie字符串
|
||||
cookie_dir: Cookie存储目录
|
||||
|
||||
Returns:
|
||||
文件路径
|
||||
"""
|
||||
import time
|
||||
|
||||
cookie_path = Path(cookie_dir)
|
||||
cookie_path.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
filepath = cookie_path / f"{username}.json"
|
||||
|
||||
data = {
|
||||
'username': username,
|
||||
'cookie': cookie,
|
||||
'timestamp': int(time.time()),
|
||||
}
|
||||
|
||||
with open(filepath, 'w', encoding='utf-8') as f:
|
||||
json.dump(data, f, ensure_ascii=False, indent=2)
|
||||
|
||||
logger.info(f"Cookie已保存: {filepath}")
|
||||
return str(filepath)
|
||||
|
||||
|
||||
def load_cookie(username: str, cookie_dir: str = "data/cookies") -> str:
|
||||
"""
|
||||
从文件加载Cookie
|
||||
|
||||
Args:
|
||||
username: 用户名
|
||||
cookie_dir: Cookie存储目录
|
||||
|
||||
Returns:
|
||||
Cookie字符串,不存在返回空字符串
|
||||
"""
|
||||
filepath = Path(cookie_dir) / f"{username}.json"
|
||||
|
||||
if not filepath.exists():
|
||||
return ""
|
||||
|
||||
try:
|
||||
with open(filepath, 'r', encoding='utf-8') as f:
|
||||
data = json.load(f)
|
||||
return data.get('cookie', '')
|
||||
except Exception as e:
|
||||
logger.error(f"加载Cookie失败: {e}")
|
||||
return ""
|
||||
|
||||
|
||||
def format_cookie_for_browser(cookie_str: str) -> dict:
|
||||
"""
|
||||
将Cookie字符串转换为浏览器格式
|
||||
|
||||
Args:
|
||||
cookie_str: Cookie字符串
|
||||
|
||||
Returns:
|
||||
Cookie字典
|
||||
"""
|
||||
cookies = {}
|
||||
for item in cookie_str.split(';'):
|
||||
item = item.strip()
|
||||
if '=' in item:
|
||||
key, value = item.split('=', 1)
|
||||
cookies[key.strip()] = value.strip()
|
||||
return cookies
|
||||
@@ -0,0 +1,42 @@
|
||||
"""日志配置模块"""
|
||||
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from loguru import logger
|
||||
|
||||
|
||||
def setup_logger(level: str = "INFO", log_file: str = None) -> None:
|
||||
"""
|
||||
配置日志
|
||||
|
||||
Args:
|
||||
level: 日志级别
|
||||
log_file: 日志文件路径
|
||||
"""
|
||||
# 移除默认handler
|
||||
logger.remove()
|
||||
|
||||
# 控制台输出
|
||||
logger.add(
|
||||
sys.stdout,
|
||||
level=level,
|
||||
format="<green>{time:YYYY-MM-DD HH:mm:ss}</green> | "
|
||||
"<level>{level: <8}</level> | "
|
||||
"<cyan>{name}</cyan>:<cyan>{function}</cyan>:<cyan>{line}</cyan> | "
|
||||
"<level>{message}</level>",
|
||||
colorize=True,
|
||||
)
|
||||
|
||||
# 文件输出
|
||||
if log_file:
|
||||
log_path = Path(log_file)
|
||||
log_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
logger.add(
|
||||
log_file,
|
||||
level=level,
|
||||
format="{time:YYYY-MM-DD HH:mm:ss} | {level: <8} | {name}:{function}:{line} | {message}",
|
||||
rotation="10 MB",
|
||||
retention="7 days",
|
||||
encoding="utf-8",
|
||||
)
|
||||
@@ -0,0 +1,219 @@
|
||||
version = 1
|
||||
revision = 3
|
||||
requires-python = "==3.12.*"
|
||||
|
||||
[[package]]
|
||||
name = "certifi"
|
||||
version = "2026.6.17"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/c9/c7/424b75da314c1045981bd9777432fad05a9e0c69daa4ed7e308bbaffe405/certifi-2026.6.17.tar.gz", hash = "sha256:024c88eeec92ca068db80f02b8b07c9cef7b9fe261d1d535abfd5abd6f6af432", size = 134594, upload-time = "2026-06-17T10:31:07.894Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/ef/2f/c5464532e965badff2f4c4c1a3a83f5697f0d7c407ed0cda44aaa99bb451/certifi-2026.6.17-py3-none-any.whl", hash = "sha256:2227dcbaafe0d2f59279d1762ddddc37783ed4354594f194ffc31d20f41fc3db", size = 133289, upload-time = "2026-06-17T10:31:06.348Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "charset-normalizer"
|
||||
version = "3.4.7"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/e7/a1/67fe25fac3c7642725500a3f6cfe5821ad557c3abb11c9d20d12c7008d3e/charset_normalizer-3.4.7.tar.gz", hash = "sha256:ae89db9e5f98a11a4bf50407d4363e7b09b31e55bc117b4f7d80aab97ba009e5", size = 144271, upload-time = "2026-04-02T09:28:39.342Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/0c/eb/4fc8d0a7110eb5fc9cc161723a34a8a6c200ce3b4fbf681bc86feee22308/charset_normalizer-3.4.7-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:eca9705049ad3c7345d574e3510665cb2cf844c2f2dcfe675332677f081cbd46", size = 311328, upload-time = "2026-04-02T09:26:24.331Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f8/e3/0fadc706008ac9d7b9b5be6dc767c05f9d3e5df51744ce4cc9605de7b9f4/charset_normalizer-3.4.7-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6178f72c5508bfc5fd446a5905e698c6212932f25bcdd4b47a757a50605a90e2", size = 208061, upload-time = "2026-04-02T09:26:25.568Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/42/f0/3dd1045c47f4a4604df85ec18ad093912ae1344ac706993aff91d38773a2/charset_normalizer-3.4.7-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e1421b502d83040e6d7fb2fb18dff63957f720da3d77b2fbd3187ceb63755d7b", size = 229031, upload-time = "2026-04-02T09:26:26.865Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/dc/67/675a46eb016118a2fbde5a277a5d15f4f69d5f3f5f338e5ee2f8948fcf43/charset_normalizer-3.4.7-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:edac0f1ab77644605be2cbba52e6b7f630731fc42b34cb0f634be1a6eface56a", size = 225239, upload-time = "2026-04-02T09:26:28.044Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4b/f8/d0118a2f5f23b02cd166fa385c60f9b0d4f9194f574e2b31cef350ad7223/charset_normalizer-3.4.7-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5649fd1c7bade02f320a462fdefd0b4bd3ce036065836d4f42e0de958038e116", size = 216589, upload-time = "2026-04-02T09:26:29.239Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b1/f1/6d2b0b261b6c4ceef0fcb0d17a01cc5bc53586c2d4796fa04b5c540bc13d/charset_normalizer-3.4.7-cp312-cp312-manylinux_2_31_armv7l.whl", hash = "sha256:203104ed3e428044fd943bc4bf45fa73c0730391f9621e37fe39ecf477b128cb", size = 202733, upload-time = "2026-04-02T09:26:30.5Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6f/c0/7b1f943f7e87cc3db9626ba17807d042c38645f0a1d4415c7a14afb5591f/charset_normalizer-3.4.7-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:298930cec56029e05497a76988377cbd7457ba864beeea92ad7e844fe74cd1f1", size = 212652, upload-time = "2026-04-02T09:26:31.709Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/38/dd/5a9ab159fe45c6e72079398f277b7d2b523e7f716acc489726115a910097/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:708838739abf24b2ceb208d0e22403dd018faeef86ddac04319a62ae884c4f15", size = 211229, upload-time = "2026-04-02T09:26:33.282Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d5/ff/531a1cad5ca855d1c1a8b69cb71abfd6d85c0291580146fda7c82857caa1/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:0f7eb884681e3938906ed0434f20c63046eacd0111c4ba96f27b76084cd679f5", size = 203552, upload-time = "2026-04-02T09:26:34.845Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c1/4c/a5fb52d528a8ca41f7598cb619409ece30a169fbdf9cdce592e53b46c3a6/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:4dc1e73c36828f982bfe79fadf5919923f8a6f4df2860804db9a98c48824ce8d", size = 230806, upload-time = "2026-04-02T09:26:36.152Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/59/7a/071feed8124111a32b316b33ae4de83d36923039ef8cf48120266844285b/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:aed52fea0513bac0ccde438c188c8a471c4e0f457c2dd20cdbf6ea7a450046c7", size = 212316, upload-time = "2026-04-02T09:26:37.672Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fd/35/f7dba3994312d7ba508e041eaac39a36b120f32d4c8662b8814dab876431/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:fea24543955a6a729c45a73fe90e08c743f0b3334bbf3201e6c4bc1b0c7fa464", size = 227274, upload-time = "2026-04-02T09:26:38.93Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8a/2d/a572df5c9204ab7688ec1edc895a73ebded3b023bb07364710b05dd1c9be/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:bb6d88045545b26da47aa879dd4a89a71d1dce0f0e549b1abcb31dfe4a8eac49", size = 218468, upload-time = "2026-04-02T09:26:40.17Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/86/eb/890922a8b03a568ca2f336c36585a4713c55d4d67bf0f0c78924be6315ca/charset_normalizer-3.4.7-cp312-cp312-win32.whl", hash = "sha256:2257141f39fe65a3fdf38aeccae4b953e5f3b3324f4ff0daf9f15b8518666a2c", size = 148460, upload-time = "2026-04-02T09:26:41.416Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/35/d9/0e7dffa06c5ab081f75b1b786f0aefc88365825dfcd0ac544bdb7b2b6853/charset_normalizer-3.4.7-cp312-cp312-win_amd64.whl", hash = "sha256:5ed6ab538499c8644b8a3e18debabcd7ce684f3fa91cf867521a7a0279cab2d6", size = 159330, upload-time = "2026-04-02T09:26:42.554Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9e/5d/481bcc2a7c88ea6b0878c299547843b2521ccbc40980cb406267088bc701/charset_normalizer-3.4.7-cp312-cp312-win_arm64.whl", hash = "sha256:56be790f86bfb2c98fb742ce566dfb4816e5a83384616ab59c49e0604d49c51d", size = 147828, upload-time = "2026-04-02T09:26:44.075Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/db/8f/61959034484a4a7c527811f4721e75d02d653a35afb0b6054474d8185d4c/charset_normalizer-3.4.7-py3-none-any.whl", hash = "sha256:3dce51d0f5e7951f8bb4900c257dad282f49190fdbebecd4ba99bcc41fef404d", size = 61958, upload-time = "2026-04-02T09:28:37.794Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "colorama"
|
||||
version = "0.4.6"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "douyu-login-py"
|
||||
version = "0.1.0"
|
||||
source = { editable = "." }
|
||||
dependencies = [
|
||||
{ name = "loguru" },
|
||||
{ name = "numpy" },
|
||||
{ name = "opencv-python-headless" },
|
||||
{ name = "pillow" },
|
||||
{ name = "pycryptodome" },
|
||||
{ name = "pyyaml" },
|
||||
{ name = "requests" },
|
||||
]
|
||||
|
||||
[package.metadata]
|
||||
requires-dist = [
|
||||
{ name = "loguru", specifier = ">=0.7.0" },
|
||||
{ name = "numpy", specifier = ">=1.24.0" },
|
||||
{ name = "opencv-python-headless", specifier = ">=4.8.0" },
|
||||
{ name = "pillow", specifier = ">=10.0.0" },
|
||||
{ name = "pycryptodome", specifier = ">=3.19.0" },
|
||||
{ name = "pyyaml", specifier = ">=6.0" },
|
||||
{ name = "requests", specifier = ">=2.31.0" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "idna"
|
||||
version = "3.18"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/cd/63/9496c57188a2ee585e0f1db071d75089a11e98aa86eb99d9d7618fc1edce/idna-3.18.tar.gz", hash = "sha256:ffb385a7e039654cef1ab9ef32c6fafe283c0c0467bba1d9029738ce4a14a848", size = 196711, upload-time = "2026-06-02T14:34:07.794Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/1e/5e/d4e9f1a599fb8e573b7b87160658329fbf28d19eac2718f51fc3def3aa5a/idna-3.18-py3-none-any.whl", hash = "sha256:7f952cbe720b688055e3f87de14f5c3e5fdaa8bc3928985c4077ca689de849a2", size = 65455, upload-time = "2026-06-02T14:34:06.319Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "loguru"
|
||||
version = "0.7.3"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "colorama", marker = "sys_platform == 'win32'" },
|
||||
{ name = "win32-setctime", marker = "sys_platform == 'win32'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/3a/05/a1dae3dffd1116099471c643b8924f5aa6524411dc6c63fdae648c4f1aca/loguru-0.7.3.tar.gz", hash = "sha256:19480589e77d47b8d85b2c827ad95d49bf31b0dcde16593892eb51dd18706eb6", size = 63559, upload-time = "2024-12-06T11:20:56.608Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/0c/29/0348de65b8cc732daa3e33e67806420b2ae89bdce2b04af740289c5c6c8c/loguru-0.7.3-py3-none-any.whl", hash = "sha256:31a33c10c8e1e10422bfd431aeb5d351c7cf7fa671e3c4df004162264b28220c", size = 61595, upload-time = "2024-12-06T11:20:54.538Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "numpy"
|
||||
version = "2.4.6"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/d0/ad/fed0499ce6a338d2a03ebae59cd15093910c8875328855781952abf6c2fe/numpy-2.4.6.tar.gz", hash = "sha256:f3a3570c4a2a16746ac2c31a7c7c7b0c186b95ce902e33db6f28094ed7387dda", size = 20735807, upload-time = "2026-05-18T23:37:14.07Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/95/2a/3d7b5ac8aac24feaf9ad7ed58f45b0bbc06d37e4338ae84c9f2298b570f9/numpy-2.4.6-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:001fbb8e08d942dd57599e781f2472269ee7f2755fae407b4f67b2f0b17da3f1", size = 16689119, upload-time = "2026-05-18T23:33:54.065Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ea/12/92c4c131527599e8288d6918e888d88726f84d805d784b771f32408aeaef/numpy-2.4.6-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ebfb099f8dcf083deef3ac1ca4c1503f387cf76296fcb3816b66f5ecb5f54fdb", size = 14699246, upload-time = "2026-05-18T23:33:57.621Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ad/fe/c0a6b7b2ca128a8fb228575147073b660656734b8ebe4d76c8fd748dcc79/numpy-2.4.6-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:3213d622a0283a39a93d188f3cf72b26862df52fbb4ca3697f51705016523d41", size = 5204410, upload-time = "2026-05-18T23:34:00.302Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f3/d4/9770d14ba719432bb90a421bfd443872ed0f70f7264b64bec12ea363d5fd/numpy-2.4.6-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:357cc07a6d7b0b182ff02249616a03742827ebb1277546b5c7cd7f7620a45698", size = 6551240, upload-time = "2026-05-18T23:34:02.852Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c9/c6/50a46a6205feba2343f1d6d17438107c5dc491ed1c736e6ea68689fd906b/numpy-2.4.6-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5f9fb9157b4ce2971008323afe46053787b526ef624fea915b261468a8421a0f", size = 15671012, upload-time = "2026-05-18T23:34:05.485Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/99/60/14115e6364fa676c5397c2ad3004e527e9aa487abf5d0706ec81bbd08529/numpy-2.4.6-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:90f9849678c75fe7afa2d348ac842c168b0a4d3d61919687216dfc547976d853", size = 16645538, upload-time = "2026-05-18T23:34:09.265Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ae/c5/693cbe59e57db94d2231fa519ca3978dc9e19da5a8f088588f5c6e947ff2/numpy-2.4.6-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:c1a2af6c6ef86344a6b0db6b97834208bf598db514f2b155042439b62605601a", size = 17020706, upload-time = "2026-05-18T23:34:13.053Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ef/fc/85b7c4eff9b4966ade25c2273cf7e7012e92366c032058653934b37de044/numpy-2.4.6-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e5805d5a22fd19c8ccff10a9561f9df94436b0545619ea579db2d3c35294bce2", size = 18368541, upload-time = "2026-05-18T23:34:17.024Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f6/81/e1b27545deedce7f4a0b348618c6b62d74e36a4dc9ccd42f3eb2f85eee32/numpy-2.4.6-cp312-cp312-win32.whl", hash = "sha256:e3eeb0aabd6bd5ce64faae67e9935203a6991b4bc2a485a767fbafb2c5125f45", size = 5962825, upload-time = "2026-05-18T23:34:20.3Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ab/ca/feab00bd44aa5fe1ad2c18f08b4d3bb92e26484b0b1d1443897809ed528c/numpy-2.4.6-cp312-cp312-win_amd64.whl", hash = "sha256:d8e8286dd7cea7895157318d1b91cdacac64c479f3cbc8dce548331728484751", size = 12321687, upload-time = "2026-05-18T23:34:23.095Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/63/cf/5a6d34850a39d1093558564f77ee8e8e0bee5061151b8f05a55711001ec7/numpy-2.4.6-cp312-cp312-win_arm64.whl", hash = "sha256:4081eb135ac24158bd51cdfbef16f1c64df7063b1143f24731387137c092bec8", size = 10221482, upload-time = "2026-05-18T23:34:25.876Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "opencv-python-headless"
|
||||
version = "4.13.0.92"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "numpy" },
|
||||
]
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/79/42/2310883be3b8826ac58c3f2787b9358a2d46923d61f88fedf930bc59c60c/opencv_python_headless-4.13.0.92-cp37-abi3-macosx_13_0_arm64.whl", hash = "sha256:1a7d040ac656c11b8c38677cc8cccdc149f98535089dbe5b081e80a4e5903209", size = 46247192, upload-time = "2026-02-05T07:01:35.187Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2d/1e/6f9e38005a6f7f22af785df42a43139d0e20f169eb5787ce8be37ee7fcc9/opencv_python_headless-4.13.0.92-cp37-abi3-macosx_14_0_x86_64.whl", hash = "sha256:3e0a6f0a37994ec6ce5f59e936be21d5d6384a4556f2d2da9c2f9c5dc948394c", size = 32568914, upload-time = "2026-02-05T07:01:51.989Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/21/76/9417a6aef9def70e467a5bf560579f816148a4c658b7d525581b356eda9e/opencv_python_headless-4.13.0.92-cp37-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:5c8cfc8e87ed452b5cecb9419473ee5560a989859fe1d10d1ce11ae87b09a2cb", size = 33703709, upload-time = "2026-02-05T10:24:46.469Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/92/ce/bd17ff5772938267fd49716e94ca24f616ff4cb1ff4c6be13085108037be/opencv_python_headless-4.13.0.92-cp37-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:0525a3d2c0b46c611e2130b5fdebc94cf404845d8fa64d2f3a3b679572a5bd22", size = 56016764, upload-time = "2026-02-05T10:26:48.904Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8f/b4/b7bcbf7c874665825a8c8e1097e93ea25d1f1d210a3e20d4451d01da30aa/opencv_python_headless-4.13.0.92-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:eb60e36b237b1ebd40a912da5384b348df8ed534f6f644d8e0b4f103e272ba7d", size = 35010236, upload-time = "2026-02-05T10:28:11.031Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4b/33/b5db29a6c00eb8f50708110d8d453747ca125c8b805bc437b289dbdcc057/opencv_python_headless-4.13.0.92-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:0bd48544f77c68b2941392fcdf9bcd2b9cdf00e98cb8c29b2455d194763cf99e", size = 60391106, upload-time = "2026-02-05T10:30:14.236Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fb/c3/52cfea47cd33e53e8c0fbd6e7c800b457245c1fda7d61660b4ffe9596a7f/opencv_python_headless-4.13.0.92-cp37-abi3-win32.whl", hash = "sha256:a7cf08e5b191f4ebb530791acc0825a7986e0d0dee2a3c491184bd8599848a4b", size = 30812232, upload-time = "2026-02-05T07:02:29.594Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4a/90/b338326131ccb2aaa3c2c85d00f41822c0050139a4bfe723cfd95455bd2d/opencv_python_headless-4.13.0.92-cp37-abi3-win_amd64.whl", hash = "sha256:77a82fe35ddcec0f62c15f2ba8a12ecc2ed4207c17b0902c7a3151ae29f37fb6", size = 40070414, upload-time = "2026-02-05T07:02:26.448Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pillow"
|
||||
version = "12.2.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/8c/21/c2bcdd5906101a30244eaffc1b6e6ce71a31bd0742a01eb89e660ebfac2d/pillow-12.2.0.tar.gz", hash = "sha256:a830b1a40919539d07806aa58e1b114df53ddd43213d9c8b75847eee6c0182b5", size = 46987819, upload-time = "2026-04-01T14:46:17.687Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/58/be/7482c8a5ebebbc6470b3eb791812fff7d5e0216c2be3827b30b8bb6603ed/pillow-12.2.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:2d192a155bbcec180f8564f693e6fd9bccff5a7af9b32e2e4bf8c9c69dbad6b5", size = 5308279, upload-time = "2026-04-01T14:43:13.246Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d8/95/0a351b9289c2b5cbde0bacd4a83ebc44023e835490a727b2a3bd60ddc0f4/pillow-12.2.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f3f40b3c5a968281fd507d519e444c35f0ff171237f4fdde090dd60699458421", size = 4695490, upload-time = "2026-04-01T14:43:15.584Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/de/af/4e8e6869cbed569d43c416fad3dc4ecb944cb5d9492defaed89ddd6fe871/pillow-12.2.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:03e7e372d5240cc23e9f07deca4d775c0817bffc641b01e9c3af208dbd300987", size = 6284462, upload-time = "2026-04-01T14:43:18.268Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e9/9e/c05e19657fd57841e476be1ab46c4d501bffbadbafdc31a6d665f8b737b6/pillow-12.2.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:b86024e52a1b269467a802258c25521e6d742349d760728092e1bc2d135b4d76", size = 8094744, upload-time = "2026-04-01T14:43:20.716Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2b/54/1789c455ed10176066b6e7e6da1b01e50e36f94ba584dc68d9eebfe9156d/pillow-12.2.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7371b48c4fa448d20d2714c9a1f775a81155050d383333e0a6c15b1123dda005", size = 6398371, upload-time = "2026-04-01T14:43:23.443Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/43/e3/fdc657359e919462369869f1c9f0e973f353f9a9ee295a39b1fea8ee1a77/pillow-12.2.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:62f5409336adb0663b7caa0da5c7d9e7bdbaae9ce761d34669420c2a801b2780", size = 7087215, upload-time = "2026-04-01T14:43:26.758Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8b/f8/2f6825e441d5b1959d2ca5adec984210f1ec086435b0ed5f52c19b3b8a6e/pillow-12.2.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:01afa7cf67f74f09523699b4e88c73fb55c13346d212a59a2db1f86b0a63e8c5", size = 6509783, upload-time = "2026-04-01T14:43:29.56Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/67/f9/029a27095ad20f854f9dba026b3ea6428548316e057e6fc3545409e86651/pillow-12.2.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fc3d34d4a8fbec3e88a79b92e5465e0f9b842b628675850d860b8bd300b159f5", size = 7212112, upload-time = "2026-04-01T14:43:32.091Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/be/42/025cfe05d1be22dbfdb4f264fe9de1ccda83f66e4fc3aac94748e784af04/pillow-12.2.0-cp312-cp312-win32.whl", hash = "sha256:58f62cc0f00fd29e64b29f4fd923ffdb3859c9f9e6105bfc37ba1d08994e8940", size = 6378489, upload-time = "2026-04-01T14:43:34.601Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5d/7b/25a221d2c761c6a8ae21bfa3874988ff2583e19cf8a27bf2fee358df7942/pillow-12.2.0-cp312-cp312-win_amd64.whl", hash = "sha256:7f84204dee22a783350679a0333981df803dac21a0190d706a50475e361c93f5", size = 7084129, upload-time = "2026-04-01T14:43:37.213Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/10/e1/542a474affab20fd4a0f1836cb234e8493519da6b76899e30bcc5d990b8b/pillow-12.2.0-cp312-cp312-win_arm64.whl", hash = "sha256:af73337013e0b3b46f175e79492d96845b16126ddf79c438d7ea7ff27783a414", size = 2463612, upload-time = "2026-04-01T14:43:39.421Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pycryptodome"
|
||||
version = "3.23.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/8e/a6/8452177684d5e906854776276ddd34eca30d1b1e15aa1ee9cefc289a33f5/pycryptodome-3.23.0.tar.gz", hash = "sha256:447700a657182d60338bab09fdb27518f8856aecd80ae4c6bdddb67ff5da44ef", size = 4921276, upload-time = "2025-05-17T17:21:45.242Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/db/6c/a1f71542c969912bb0e106f64f60a56cc1f0fabecf9396f45accbe63fa68/pycryptodome-3.23.0-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:187058ab80b3281b1de11c2e6842a357a1f71b42cb1e15bce373f3d238135c27", size = 2495627, upload-time = "2025-05-17T17:20:47.139Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6e/4e/a066527e079fc5002390c8acdd3aca431e6ea0a50ffd7201551175b47323/pycryptodome-3.23.0-cp37-abi3-macosx_10_9_x86_64.whl", hash = "sha256:cfb5cd445280c5b0a4e6187a7ce8de5a07b5f3f897f235caa11f1f435f182843", size = 1640362, upload-time = "2025-05-17T17:20:50.392Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/50/52/adaf4c8c100a8c49d2bd058e5b551f73dfd8cb89eb4911e25a0c469b6b4e/pycryptodome-3.23.0-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:67bd81fcbe34f43ad9422ee8fd4843c8e7198dd88dd3d40e6de42ee65fbe1490", size = 2182625, upload-time = "2025-05-17T17:20:52.866Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5f/e9/a09476d436d0ff1402ac3867d933c61805ec2326c6ea557aeeac3825604e/pycryptodome-3.23.0-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c8987bd3307a39bc03df5c8e0e3d8be0c4c3518b7f044b0f4c15d1aa78f52575", size = 2268954, upload-time = "2025-05-17T17:20:55.027Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f9/c5/ffe6474e0c551d54cab931918127c46d70cab8f114e0c2b5a3c071c2f484/pycryptodome-3.23.0-cp37-abi3-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:aa0698f65e5b570426fc31b8162ed4603b0c2841cbb9088e2b01641e3065915b", size = 2308534, upload-time = "2025-05-17T17:20:57.279Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/18/28/e199677fc15ecf43010f2463fde4c1a53015d1fe95fb03bca2890836603a/pycryptodome-3.23.0-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:53ecbafc2b55353edcebd64bf5da94a2a2cdf5090a6915bcca6eca6cc452585a", size = 2181853, upload-time = "2025-05-17T17:20:59.322Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ce/ea/4fdb09f2165ce1365c9eaefef36625583371ee514db58dc9b65d3a255c4c/pycryptodome-3.23.0-cp37-abi3-musllinux_1_2_i686.whl", hash = "sha256:156df9667ad9f2ad26255926524e1c136d6664b741547deb0a86a9acf5ea631f", size = 2342465, upload-time = "2025-05-17T17:21:03.83Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/22/82/6edc3fc42fe9284aead511394bac167693fb2b0e0395b28b8bedaa07ef04/pycryptodome-3.23.0-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:dea827b4d55ee390dc89b2afe5927d4308a8b538ae91d9c6f7a5090f397af1aa", size = 2267414, upload-time = "2025-05-17T17:21:06.72Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/59/fe/aae679b64363eb78326c7fdc9d06ec3de18bac68be4b612fc1fe8902693c/pycryptodome-3.23.0-cp37-abi3-win32.whl", hash = "sha256:507dbead45474b62b2bbe318eb1c4c8ee641077532067fec9c1aa82c31f84886", size = 1768484, upload-time = "2025-05-17T17:21:08.535Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/54/2f/e97a1b8294db0daaa87012c24a7bb714147c7ade7656973fd6c736b484ff/pycryptodome-3.23.0-cp37-abi3-win_amd64.whl", hash = "sha256:c75b52aacc6c0c260f204cbdd834f76edc9fb0d8e0da9fbf8352ef58202564e2", size = 1799636, upload-time = "2025-05-17T17:21:10.393Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/18/3d/f9441a0d798bf2b1e645adc3265e55706aead1255ccdad3856dbdcffec14/pycryptodome-3.23.0-cp37-abi3-win_arm64.whl", hash = "sha256:11eeeb6917903876f134b56ba11abe95c0b0fd5e3330def218083c7d98bbcb3c", size = 1703675, upload-time = "2025-05-17T17:21:13.146Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pyyaml"
|
||||
version = "6.0.3"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/05/8e/961c0007c59b8dd7729d542c61a4d537767a59645b82a0b521206e1e25c2/pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f", size = 130960, upload-time = "2025-09-25T21:33:16.546Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/d1/33/422b98d2195232ca1826284a76852ad5a86fe23e31b009c9886b2d0fb8b2/pyyaml-6.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7f047e29dcae44602496db43be01ad42fc6f1cc0d8cd6c83d342306c32270196", size = 182063, upload-time = "2025-09-25T21:32:11.445Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/89/a0/6cf41a19a1f2f3feab0e9c0b74134aa2ce6849093d5517a0c550fe37a648/pyyaml-6.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0", size = 173973, upload-time = "2025-09-25T21:32:12.492Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ed/23/7a778b6bd0b9a8039df8b1b1d80e2e2ad78aa04171592c8a5c43a56a6af4/pyyaml-6.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9149cad251584d5fb4981be1ecde53a1ca46c891a79788c0df828d2f166bda28", size = 775116, upload-time = "2025-09-25T21:32:13.652Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/65/30/d7353c338e12baef4ecc1b09e877c1970bd3382789c159b4f89d6a70dc09/pyyaml-6.0.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5fdec68f91a0c6739b380c83b951e2c72ac0197ace422360e6d5a959d8d97b2c", size = 844011, upload-time = "2025-09-25T21:32:15.21Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8b/9d/b3589d3877982d4f2329302ef98a8026e7f4443c765c46cfecc8858c6b4b/pyyaml-6.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ba1cc08a7ccde2d2ec775841541641e4548226580ab850948cbfda66a1befcdc", size = 807870, upload-time = "2025-09-25T21:32:16.431Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/05/c0/b3be26a015601b822b97d9149ff8cb5ead58c66f981e04fedf4e762f4bd4/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8dc52c23056b9ddd46818a57b78404882310fb473d63f17b07d5c40421e47f8e", size = 761089, upload-time = "2025-09-25T21:32:17.56Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/be/8e/98435a21d1d4b46590d5459a22d88128103f8da4c2d4cb8f14f2a96504e1/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:41715c910c881bc081f1e8872880d3c650acf13dfa8214bad49ed4cede7c34ea", size = 790181, upload-time = "2025-09-25T21:32:18.834Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/74/93/7baea19427dcfbe1e5a372d81473250b379f04b1bd3c4c5ff825e2327202/pyyaml-6.0.3-cp312-cp312-win32.whl", hash = "sha256:96b533f0e99f6579b3d4d4995707cf36df9100d67e0c8303a0c55b27b5f99bc5", size = 137658, upload-time = "2025-09-25T21:32:20.209Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/86/bf/899e81e4cce32febab4fb42bb97dcdf66bc135272882d1987881a4b519e9/pyyaml-6.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:5fcd34e47f6e0b794d17de1b4ff496c00986e1c83f7ab2fb8fcfe9616ff7477b", size = 154003, upload-time = "2025-09-25T21:32:21.167Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1a/08/67bd04656199bbb51dbed1439b7f27601dfb576fb864099c7ef0c3e55531/pyyaml-6.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:64386e5e707d03a7e172c0701abfb7e10f0fb753ee1d773128192742712a98fd", size = 140344, upload-time = "2025-09-25T21:32:22.617Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "requests"
|
||||
version = "2.34.2"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "certifi" },
|
||||
{ name = "charset-normalizer" },
|
||||
{ name = "idna" },
|
||||
{ name = "urllib3" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/ac/c3/e2a2b89f2d3e2179abd6d00ebd70bff6273f37fb3e0cc209f48b39d00cbf/requests-2.34.2.tar.gz", hash = "sha256:f288924cae4e29463698d6d60bc6a4da69c89185ad1e0bcc4104f584e960b9ed", size = 142856, upload-time = "2026-05-14T19:25:27.735Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/a0/f4/c67b0b3f1b9245e8d266f0f112c500d50e5b4e83cb6f3b71b6528104182a/requests-2.34.2-py3-none-any.whl", hash = "sha256:2a0d60c172f83ac6ab31e4554906c0f3b3588d37b5cb939b1c061f4907e278e0", size = 73075, upload-time = "2026-05-14T19:25:26.443Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "urllib3"
|
||||
version = "2.7.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/53/0c/06f8b233b8fd13b9e5ee11424ef85419ba0d8ba0b3138bf360be2ff56953/urllib3-2.7.0.tar.gz", hash = "sha256:231e0ec3b63ceb14667c67be60f2f2c40a518cb38b03af60abc813da26505f4c", size = 433602, upload-time = "2026-05-07T16:13:18.596Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/7f/3e/5db95bcf282c52709639744ca2a8b149baccf648e39c8cc87553df9eae0c/urllib3-2.7.0-py3-none-any.whl", hash = "sha256:9fb4c81ebbb1ce9531cce37674bbc6f1360472bc18ca9a553ede278ef7276897", size = 131087, upload-time = "2026-05-07T16:13:17.151Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "win32-setctime"
|
||||
version = "1.2.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/b3/8f/705086c9d734d3b663af0e9bb3d4de6578d08f46b1b101c2442fd9aecaa2/win32_setctime-1.2.0.tar.gz", hash = "sha256:ae1fdf948f5640aae05c511ade119313fb6a30d7eabe25fef9764dca5873c4c0", size = 4867, upload-time = "2024-12-07T15:28:28.314Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/e1/07/c6fe3ad3e685340704d314d765b7912993bcb8dc198f0e7a89382d37974b/win32_setctime-1.2.0-py3-none-any.whl", hash = "sha256:95d644c4e708aba81dc3704a116d8cbc974d70b3bdb8be1d150e36be6e9d1390", size = 4083, upload-time = "2024-12-07T15:28:26.465Z" },
|
||||
]
|
||||
Reference in New Issue
Block a user