Files
live-hub-py/core/douyu/proxy.py
T
yml2213andClaude Fable 5 8fdf8d4b69 优化代理获取和登录逻辑
- 每个账号独立获取代理,不再共用一个代理IP
- 代理预检失败只标记该账号失败,不终止整个批次
- 代理验证改为并发:verify_proxies_concurrent 多IP同时验证
- resolve_working_proxy 多IP并发验证,找到可用即返回
- parse_proxy_response 改为返回列表,支持多行/逗号分隔
- 邮箱验证码匹配收件人地址,防止多账号并发取错验证码
- 修正 mail.bdhg.xyz 旧数据端口和SSL配置
- bdhg.xyz 默认配置改为 143端口非SSL

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-22 22:44:01 +08:00

289 lines
9.8 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""代理管理模块"""
import re
import time
import requests
from typing import Optional
from loguru import logger
class ProxyManager:
"""代理管理器"""
def __init__(self, api_url: str = ""):
self.api_url = api_url
self.current_proxy: Optional[str] = None
def get_proxy(self) -> Optional[str]:
"""
从代理API获取代理IP,支持API返回多个IP(多行格式),
逐一验证返回第一个可用的。
Returns:
代理URL,格式: http://ip:port
"""
try:
logger.info("获取代理IP...")
response = requests.get(self.api_url, timeout=10)
response.raise_for_status()
text = response.text.strip()
logger.debug(f"代理API响应: {text}")
proxy_urls, _ = parse_proxy_response(text)
if not proxy_urls:
logger.warning(f"无法解析代理地址: {text}")
return None
if len(proxy_urls) == 1:
proxy = proxy_urls[0]
self.current_proxy = proxy
logger.info(f"获取到代理: {proxy}")
return proxy
# 多个代理逐一验证,返回第一个可用的
logger.info(f"获取到 {len(proxy_urls)} 个代理,逐一验证")
for proxy in proxy_urls:
if self.verify_proxy(proxy):
self.current_proxy = proxy
logger.success(f"可用代理: {proxy}")
return proxy
logger.warning(f"代理 {proxy} 不可用,尝试下一个")
logger.warning("所有代理均不可用")
self.current_proxy = None
return None
except Exception as e:
logger.error(f"获取代理失败: {e}")
return None
def get_proxies_dict(self, proxy: str = None) -> dict:
"""获取requests使用的proxies字典"""
proxy = proxy or self.current_proxy
if proxy:
return {'http': proxy, 'https': proxy}
return {}
def verify_proxy(self, proxy: str = None) -> bool:
"""验证代理是否可用"""
proxy = proxy or self.current_proxy
if not proxy:
return False
try:
response = requests.get(
'https://httpbin.org/ip',
proxies={'http': proxy, 'https': proxy},
timeout=10
)
if response.status_code == 200:
data = response.json()
logger.info(f"代理验证成功,当前IP: {data.get('origin')}")
return True
return False
except Exception as e:
logger.error(f"代理验证失败: {e}")
return False
def get_proxy_manager(api_url: str = "") -> ProxyManager:
"""获取代理管理器实例(每次传入 api_url 时创建新实例,避免全局状态污染)"""
return ProxyManager(api_url)
def parse_proxy_response(text: str) -> tuple[list[str], Optional[str]]:
"""
解析代理API响应,支持返回多个代理地址。
Returns:
(proxy_urls, whitelist_ip)
- proxy_urls: 解析到的所有代理地址列表(http://ip:port
- whitelist_ip: 需要添加到白名单的IP(当API返回白名单错误时),无错误时为 None
"""
text = text.strip()
# 白名单错误优先检测
if '添加白名单' in text or '白名单' in text:
ip_match = re.search(r'(\d+\.\d+\.\d+\.\d+)', text)
if ip_match:
return [], ip_match.group(1)
# 解析所有 ip:port,支持多行格式
matches = re.findall(r'(\d+\.\d+\.\d+\.\d+):(\d+)', text)
proxies = []
for ip, port in matches:
# 排除白名单提示中误匹配的
proxy = f"http://{ip}:{port}"
proxies.append(proxy)
if proxies:
return proxies, None
return [], None
def verify_proxy_url(proxy_url: str, timeout: tuple = (4, 6)) -> tuple[bool, str]:
"""
验证代理是否可用。
验证优先级:
1. 斗鱼主站(最相关,能访问斗鱼才是最终目的)
2. myip(快速 IP 验证)
3. 百度 IP 查询(备用)
Returns:
(是否可用, 消息)
"""
proxies = {'http': proxy_url, 'https': proxy_url}
targets = [
('https://www.douyu.com', '斗鱼主站'),
('https://myip.ipip.net', 'IP验证'),
('https://qifu-api.baidubce.com/ip/local/geo/v1/district', '百度IP查询'),
]
for url, label in targets:
try:
response = requests.get(
url, proxies=proxies, timeout=timeout,
headers={'User-Agent': 'Mozilla/5.0'},
)
response.raise_for_status()
return True, f'代理可用 → {label}'
except Exception as e:
err_msg = str(e)
if 'Tunnel connection failed' in err_msg or '503' in err_msg:
detail = '代理拒绝连接(白名单可能未生效)'
elif 'timed out' in err_msg.lower():
detail = '连接超时'
else:
detail = type(e).__name__
logger.debug(f"代理验证 {label} 失败: {detail}")
continue
return False, '代理验证失败(所有目标不可达)'
def verify_proxies_concurrent(proxy_urls: list[str], timeout: tuple = (4, 6), max_workers: int = 5) -> tuple[Optional[str], str]:
"""
并发验证多个代理URL,返回第一个可用的。
Args:
proxy_urls: 代理URL列表
timeout: 验证超时
max_workers: 最大并发数
Returns:
(可用的代理URL, 消息)
"""
if not proxy_urls:
return None, '无代理可验证'
if len(proxy_urls) == 1:
ok, msg = verify_proxy_url(proxy_urls[0], timeout)
if ok:
return proxy_urls[0], msg
return None, msg
from concurrent.futures import ThreadPoolExecutor, as_completed
with ThreadPoolExecutor(max_workers=min(max_workers, len(proxy_urls))) as executor:
future_map = {
executor.submit(verify_proxy_url, p, timeout): p
for p in proxy_urls
}
for future in as_completed(future_map):
proxy_url = future_map[future]
try:
ok, msg = future.result()
if ok:
logger.success(f"并发验证找到可用代理: {proxy_url}")
# 取消剩余任务
for f in future_map:
if f != future:
f.cancel()
return proxy_url, msg
except Exception:
continue
return None, f'共 {len(proxy_urls)} 个代理均不可用'
def resolve_working_proxy(
api_url: str,
whitelist_uid: str = "",
whitelist_ukey: str = "",
max_attempts: int = 3,
log_func=None,
) -> tuple[Optional[str], str]:
"""
从代理API获取可用代理,自动处理白名单同步。
支持API返回多个代理IP,逐一验证直到找到可用的。
Args:
api_url: 代理API地址
whitelist_uid: 白名单UID(启用白名单时传入)
whitelist_ukey: 白名单UKEY
max_attempts: 最大获取尝试次数(默认3次)
log_func: 日志回调函数 (level, message)
Returns:
(代理URL, 消息)
"""
def log(level, msg):
if log_func:
log_func(level, msg)
else:
getattr(logger, level if level in ('info', 'warning', 'error', 'success') else 'info', logger.info)(msg)
synced_whitelist = False
last_error = ""
for attempt in range(1, max_attempts + 1):
# 重试之间增加退避延迟,避免代理API返回同一个不可用IP
if attempt > 1:
delay = min(attempt, 3)
log('info', f'等待 {delay}s 后重试...')
time.sleep(delay)
log('info', f'代理预检 {attempt}/{max_attempts}: 正在获取代理')
try:
response = requests.get(api_url, timeout=10)
response.raise_for_status()
text = response.text.strip()
proxy_urls, whitelist_ip = parse_proxy_response(text)
if proxy_urls:
log('info', f'获取到 {len(proxy_urls)} 个代理,并发验证')
proxy_url, msg = verify_proxies_concurrent(proxy_urls)
if proxy_url:
log('success', f'代理预检成功: {proxy_url}')
return proxy_url, msg
last_error = msg
log('warning', f'代理预检 {attempt}/{max_attempts}: {last_error}')
continue
# 代理API返回白名单错误
if whitelist_ip and not synced_whitelist and whitelist_uid and whitelist_ukey:
log('warning', f'代理需要白名单IP: {whitelist_ip},自动同步...')
from core.douyu.whitelist import WhitelistManager
manager = WhitelistManager(whitelist_uid, whitelist_ukey)
ok, sync_msg = manager.sync_ip(whitelist_ip)
log('success' if ok else 'error', f'白名单同步: {sync_msg}')
if ok:
synced_whitelist = True
log('info', '白名单已更新,等待2秒后重试...')
time.sleep(2)
continue
return None, f'白名单同步失败: {sync_msg}'
last_error = f'代理API响应无法解析'
log('warning', f'代理预检 {attempt}/{max_attempts}: {last_error}: {text[:80]}')
except Exception as exc:
last_error = f'代理API请求失败: {exc}'
log('warning', f'代理预检 {attempt}/{max_attempts}: {last_error}')
return None, f'代理预检失败({max_attempts}次尝试均失败): {last_error}'