""" 浏览器指纹生成与管理 - 生成逼真的浏览器指纹(UA、屏幕、Canvas、WebGL、TLS) - 指纹池轮换、冷却、黑名单机制 """ import asyncio import hashlib import random import time from datetime import datetime, timedelta from typing import Dict, Any, List, Optional, Set from loguru import logger from fake_useragent import UserAgent from src.core.config import get_config # ── 常量 ────────────────────────────────────────────── CHROME_VERSIONS = [ "120.0.6099.109", "121.0.6167.85", "122.0.6261.57", "123.0.6312.86", "124.0.6367.91", "125.0.6422.60", ] SCREEN_RESOLUTIONS = [ {"width": 1920, "height": 1080, "ratio": 1.0}, {"width": 1366, "height": 768, "ratio": 1.0}, {"width": 1536, "height": 864, "ratio": 1.25}, {"width": 2560, "height": 1440, "ratio": 1.5}, {"width": 1440, "height": 900, "ratio": 1.0}, {"width": 1680, "height": 1050, "ratio": 1.0}, {"width": 1280, "height": 800, "ratio": 1.0}, {"width": 3840, "height": 2160, "ratio": 2.0}, ] WEBGL_RENDERERS = [ "ANGLE (Intel, Intel(R) UHD Graphics 630 Direct3D11 vs_5_0 ps_5_0, D3D11)", "ANGLE (Intel, Intel(R) UHD Graphics Direct3D11 vs_5_0 ps_5_0, D3D11)", "ANGLE (AMD, AMD Radeon(TM) Graphics Direct3D11 vs_5_0 ps_5_0, D3D11)", "ANGLE (NVIDIA, NVIDIA GeForce GTX 1060 6GB Direct3D11 vs_5_0 ps_5_0, D3D11)", "ANGLE (NVIDIA, NVIDIA GeForce RTX 3060 Direct3D11 vs_5_0 ps_5_0, D3D11)", "ANGLE (Intel, Intel(R) Iris(R) Xe Graphics Direct3D11 vs_5_0 ps_5_0, D3D11)", ] FONTS_POOL = [ "Arial", "Arial Black", "Calibri", "Cambria", "Consolas", "Courier New", "Georgia", "Impact", "Segoe UI", "Tahoma", "Times New Roman", "Trebuchet MS", "Verdana", "Microsoft YaHei", "SimHei", "SimSun", "NSimSun", "FangSong", "KaiTi", ] SEC_CH_UA_TEMPLATES = { "chrome": '"Chromium";v="{major}", "Google Chrome";v="{major}", "Not-A.Brand";v="99"', "edge": '"Chromium";v="{major}", "Microsoft Edge";v="{major}", "Not-A.Brand";v="99"', } class FingerprintGenerator: """指纹生成器""" def __init__(self): try: self._ua = UserAgent(browsers=["chrome", "edge"], os=["windows", "macos"]) except Exception: self._ua = None def generate(self) -> Dict[str, Any]: browser = random.choice(["chrome", "edge"]) version = random.choice(CHROME_VERSIONS) major = version.split(".")[0] ua_string = self._build_ua(browser, version) screen = random.choice(SCREEN_RESOLUTIONS) fp_id = f"fp_{int(time.time())}_{random.randint(1000, 9999)}" return { "id": fp_id, "browser": browser, "version": version, "user_agent": ua_string, "screen": screen, "sec_ch_ua": SEC_CH_UA_TEMPLATES.get(browser, SEC_CH_UA_TEMPLATES["chrome"]).format(major=major), "http_headers": self._build_headers(ua_string, browser, major), "canvas_hash": hashlib.md5(f"canvas_{random.randint(100000, 999999)}".encode()).hexdigest()[:16], "webgl": { "vendor": "Google Inc. (Intel)", "renderer": random.choice(WEBGL_RENDERERS), }, "fonts": sorted(random.sample(FONTS_POOL, k=random.randint(10, len(FONTS_POOL)))), "timezone": "Asia/Shanghai", "language": "zh-CN", "created_at": datetime.now().isoformat(), "success_rate": 1.0, "usage_count": 0, "is_active": True, } def _build_ua(self, browser: str, version: str) -> str: os_strings = [ "Windows NT 10.0; Win64; x64", "Windows NT 10.0; Win64; x64", "Macintosh; Intel Mac OS X 10_15_7", ] os_str = random.choice(os_strings) major = version.split(".")[0] if browser == "edge": return ( f"Mozilla/5.0 ({os_str}) AppleWebKit/537.36 " f"(KHTML, like Gecko) Chrome/{version} Safari/537.36 Edg/{version}" ) return ( f"Mozilla/5.0 ({os_str}) AppleWebKit/537.36 " f"(KHTML, like Gecko) Chrome/{version} Safari/537.36" ) def _build_headers(self, ua: str, browser: str, major: str) -> Dict[str, str]: headers = { "User-Agent": ua, "Accept": "application/json, text/plain, */*", "Accept-Language": "zh-CN,zh;q=0.9,en;q=0.8", "Accept-Encoding": "gzip, deflate, br", "Connection": "keep-alive", "Referer": "https://www.dongqiudi.com/", "Origin": "https://www.dongqiudi.com", "Sec-Fetch-Dest": "empty", "Sec-Fetch-Mode": "cors", "Sec-Fetch-Site": "same-site", "Sec-Ch-Ua": SEC_CH_UA_TEMPLATES.get(browser, SEC_CH_UA_TEMPLATES["chrome"]).format(major=major), "Sec-Ch-Ua-Mobile": "?0", "Sec-Ch-Ua-Platform": '"Windows"', } return headers class FingerprintPool: """指纹池管理器""" def __init__(self): cfg = get_config().anti_detect.fingerprint self._pool_size = cfg.pool_size self._min_success_rate = cfg.min_success_rate self._cooldown = cfg.cooldown self._generator = FingerprintGenerator() self._pool: Dict[str, Dict[str, Any]] = {} self._blacklist: Set[str] = set() self._lock = asyncio.Lock() async def initialize(self, size: Optional[int] = None): size = size or self._pool_size async with self._lock: for _ in range(size): fp = self._generator.generate() self._pool[fp["id"]] = fp logger.info(f"指纹池初始化完成,共 {len(self._pool)} 个指纹") async def acquire(self) -> Dict[str, Any]: async with self._lock: now = datetime.now() candidates = [] for fp_id, fp in self._pool.items(): if fp_id in self._blacklist or not fp.get("is_active", True): continue if fp.get("success_rate", 1.0) < self._min_success_rate: fp["is_active"] = False continue last_used = fp.get("last_used") if last_used: last_dt = datetime.fromisoformat(last_used) if isinstance(last_used, str) else last_used if (now - last_dt).total_seconds() < self._cooldown: continue candidates.append(fp) if not candidates: logger.warning("指纹池耗尽,生成新指纹") fp = self._generator.generate() self._pool[fp["id"]] = fp candidates = [fp] candidates.sort(key=lambda x: (x.get("usage_count", 0), -x.get("success_rate", 1.0))) chosen = candidates[0] chosen["last_used"] = now.isoformat() chosen["usage_count"] = chosen.get("usage_count", 0) + 1 return chosen.copy() async def mark_success(self, fp_id: str): async with self._lock: fp = self._pool.get(fp_id) if not fp: return total = fp.get("usage_count", 1) fails = fp.get("fail_count", 0) fp["success_rate"] = 1.0 - (fails / total) if total > 0 else 1.0 async def mark_failure(self, fp_id: str, error_type: str = ""): async with self._lock: fp = self._pool.get(fp_id) if not fp: return fp["fail_count"] = fp.get("fail_count", 0) + 1 total = fp.get("usage_count", 1) fails = fp["fail_count"] fp["success_rate"] = 1.0 - (fails / total) if total > 0 else 0.0 if error_type in ("ip_blocked", "captcha", "fingerprint_detected"): self._blacklist.add(fp_id) fp["is_active"] = False logger.warning(f"指纹 {fp_id} 被加入黑名单: {error_type}") async def cleanup(self): async with self._lock: to_remove = [ fp_id for fp_id, fp in self._pool.items() if not fp.get("is_active") and fp.get("success_rate", 1.0) < self._min_success_rate * 0.5 ] for fp_id in to_remove: self._pool.pop(fp_id, None) self._blacklist.discard(fp_id) deficit = self._pool_size - len(self._pool) if deficit > 0: for _ in range(deficit): fp = self._generator.generate() self._pool[fp["id"]] = fp logger.info(f"指纹池补充 {deficit} 个新指纹") @property def stats(self) -> Dict[str, Any]: active = sum(1 for fp in self._pool.values() if fp.get("is_active", True)) rates = [fp.get("success_rate", 1.0) for fp in self._pool.values()] return { "total": len(self._pool), "active": active, "blacklisted": len(self._blacklist), "avg_success_rate": sum(rates) / len(rates) if rates else 1.0, }