"""代理管理模块""" 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}'