test / config.py
22333Misaka's picture
Upload 5 files
efd8f02 verified
import os
import sys
import glob
import random
import threading
import shutil
import platform
import subprocess
from typing import List, Dict, Optional
import logging
def setup_logging():
"""配置日志"""
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
datefmt='%Y-%m-%d %H:%M:%S'
)
logger = logging.getLogger(__name__)
class BrowserPathFinder:
"""跨平台浏览器路径查找器"""
# 浏览器可执行文件名(按优先级排序)
BROWSER_EXECUTABLES = {
'windows': [
'chrome.exe',
'msedge.exe',
'chromium.exe',
'brave.exe',
'vivaldi.exe',
'opera.exe',
],
'darwin': [ # macOS
'Google Chrome',
'Chromium',
'Microsoft Edge',
'Brave Browser',
'Vivaldi',
'Opera',
],
'linux': [
'google-chrome',
'google-chrome-stable',
'google-chrome-beta',
'google-chrome-dev',
'google-chrome-unstable',
'chromium',
'chromium-browser',
'microsoft-edge',
'microsoft-edge-stable',
'microsoft-edge-beta',
'microsoft-edge-dev',
'brave-browser',
'brave-browser-stable',
'brave',
'vivaldi',
'vivaldi-stable',
'opera',
'opera-stable',
'chrome', # 通用名称
],
}
# 各平台常见安装路径
COMMON_PATHS = {
'windows': [
# Chrome
os.path.expandvars(r'%ProgramFiles%\Google\Chrome\Application'),
os.path.expandvars(r'%ProgramFiles(x86)%\Google\Chrome\Application'),
os.path.expandvars(r'%LocalAppData%\Google\Chrome\Application'),
# Edge
os.path.expandvars(r'%ProgramFiles%\Microsoft\Edge\Application'),
os.path.expandvars(r'%ProgramFiles(x86)%\Microsoft\Edge\Application'),
os.path.expandvars(r'%LocalAppData%\Microsoft\Edge\Application'),
# Brave
os.path.expandvars(r'%ProgramFiles%\BraveSoftware\Brave-Browser\Application'),
os.path.expandvars(r'%LocalAppData%\BraveSoftware\Brave-Browser\Application'),
# Chromium
os.path.expandvars(r'%LocalAppData%\Chromium\Application'),
# Vivaldi
os.path.expandvars(r'%LocalAppData%\Vivaldi\Application'),
# Opera
os.path.expandvars(r'%LocalAppData%\Programs\Opera'),
# Playwright
os.path.expandvars(r'%LocalAppData%\ms-playwright'),
os.path.expandvars(r'%UserProfile%\.cache\ms-playwright'),
# Puppeteer
os.path.expandvars(r'%LocalAppData%\puppeteer'),
os.path.expandvars(r'%UserProfile%\.cache\puppeteer'),
],
'darwin': [ # macOS
# 标准应用目录
'/Applications',
os.path.expanduser('~/Applications'),
# Chrome
'/Applications/Google Chrome.app/Contents/MacOS',
os.path.expanduser('~/Applications/Google Chrome.app/Contents/MacOS'),
# Chromium
'/Applications/Chromium.app/Contents/MacOS',
os.path.expanduser('~/Applications/Chromium.app/Contents/MacOS'),
# Edge
'/Applications/Microsoft Edge.app/Contents/MacOS',
os.path.expanduser('~/Applications/Microsoft Edge.app/Contents/MacOS'),
# Brave
'/Applications/Brave Browser.app/Contents/MacOS',
os.path.expanduser('~/Applications/Brave Browser.app/Contents/MacOS'),
# Vivaldi
'/Applications/Vivaldi.app/Contents/MacOS',
# Opera
'/Applications/Opera.app/Contents/MacOS',
# Homebrew
'/opt/homebrew/bin',
'/usr/local/bin',
'/opt/homebrew/Caskroom/google-chrome/latest/Google Chrome.app/Contents/MacOS',
# Playwright
os.path.expanduser('~/.cache/ms-playwright'),
os.path.expanduser('~/Library/Caches/ms-playwright'),
# Puppeteer
os.path.expanduser('~/.cache/puppeteer'),
],
'linux': [
# 标准路径
'/usr/bin',
'/usr/local/bin',
'/bin',
'/sbin',
# Chrome
'/opt/google/chrome',
'/opt/google/chrome-stable',
'/opt/google/chrome-beta',
'/opt/google/chrome-unstable',
# Chromium
'/opt/chromium',
'/opt/chromium-browser',
'/usr/lib/chromium',
'/usr/lib/chromium-browser',
'/usr/lib64/chromium-browser',
# Edge
'/opt/microsoft/msedge',
'/opt/microsoft/msedge-stable',
'/opt/microsoft/msedge-beta',
'/opt/microsoft/msedge-dev',
# Brave
'/opt/brave.com/brave',
'/opt/brave.com/brave-browser',
'/usr/lib/brave-browser',
# Snap 安装路径
'/snap/bin',
'/snap/chromium/current/usr/lib/chromium-browser',
'/snap/chromium/current/usr/lib/chromium',
'/var/lib/snapd/snap/bin',
# Flatpak 路径
'/var/lib/flatpak/exports/bin',
os.path.expanduser('~/.local/share/flatpak/exports/bin'),
# AppImage 路径
os.path.expanduser('~/Applications'),
os.path.expanduser('~/.local/bin'),
# Playwright
os.path.expanduser('~/.cache/ms-playwright'),
'/root/.cache/ms-playwright',
'/home/*/.cache/ms-playwright',
# Puppeteer
os.path.expanduser('~/.cache/puppeteer'),
'/root/.cache/puppeteer',
'/home/*/.cache/puppeteer',
# Docker 常见路径
'/headless-shell',
'/chrome',
'/chromium',
'/browser',
'/app/chrome',
'/app/chromium',
'/usr/share/chromium',
# NixOS
'/run/current-system/sw/bin',
os.path.expanduser('~/.nix-profile/bin'),
],
}
# Playwright/Puppeteer 浏览器的 glob 模式
GLOB_PATTERNS = [
# Playwright Chromium
os.path.expanduser('~/.cache/ms-playwright/chromium-*/chrome-linux/chrome'),
os.path.expanduser('~/.cache/ms-playwright/chromium-*/chrome-mac/Chromium.app/Contents/MacOS/Chromium'),
os.path.expanduser('~/.cache/ms-playwright/chromium-*/chrome-win/chrome.exe'),
'/root/.cache/ms-playwright/chromium-*/chrome-linux/chrome',
'/home/*/.cache/ms-playwright/chromium-*/chrome-linux/chrome',
# Playwright Chrome
os.path.expanduser('~/.cache/ms-playwright/chrome-*/chrome-linux/chrome'),
os.path.expanduser('~/.cache/ms-playwright/chrome-*/chrome-mac/Google Chrome for Testing.app/Contents/MacOS/Google Chrome for Testing'),
'/root/.cache/ms-playwright/chrome-*/chrome-linux/chrome',
# Puppeteer
os.path.expanduser('~/.cache/puppeteer/chrome/*/chrome-linux64/chrome'),
os.path.expanduser('~/.cache/puppeteer/chrome/*/chrome-mac-x64/Google Chrome for Testing.app/Contents/MacOS/Google Chrome for Testing'),
os.path.expanduser('~/.cache/puppeteer/chrome/*/chrome-win64/chrome.exe'),
'/root/.cache/puppeteer/chrome/*/chrome-linux64/chrome',
'/home/*/.cache/puppeteer/chrome/*/chrome-linux64/chrome',
# Windows Playwright
os.path.expandvars(r'%LocalAppData%\ms-playwright\chromium-*\chrome-win\chrome.exe'),
os.path.expandvars(r'%UserProfile%\.cache\ms-playwright\chromium-*\chrome-win\chrome.exe'),
]
@classmethod
def get_platform(cls) -> str:
"""获取当前平台"""
system = platform.system().lower()
if system == 'windows':
return 'windows'
elif system == 'darwin':
return 'darwin'
else:
return 'linux' # Linux 和其他 Unix-like 系统
@classmethod
def find_browser(cls) -> Optional[str]:
"""
查找可用的浏览器路径
返回找到的第一个可执行浏览器路径,未找到返回None
"""
current_platform = cls.get_platform()
# 1. 首先检查环境变量
for env_var in ['BROWSER_PATH', 'CHROME_PATH', 'CHROMIUM_PATH', 'CHROME_EXECUTABLE_PATH']:
env_path = os.getenv(env_var)
if env_path and cls._is_valid_executable(env_path):
return env_path
# 2. 使用 which/where 命令查找
executables = cls.BROWSER_EXECUTABLES.get(current_platform, cls.BROWSER_EXECUTABLES['linux'])
for name in executables:
path = shutil.which(name)
if path and cls._is_valid_executable(path):
return path
# 3. 检查 glob 模式(Playwright/Puppeteer)
for pattern in cls.GLOB_PATTERNS:
matches = glob.glob(pattern)
for match in sorted(matches, reverse=True): # 优先使用最新版本
if cls._is_valid_executable(match):
return match
# 4. 遍历常见路径
common_paths = cls.COMMON_PATHS.get(current_platform, cls.COMMON_PATHS['linux'])
for base_path in common_paths:
# 处理包含通配符的路径
if '*' in base_path:
expanded_paths = glob.glob(base_path)
else:
expanded_paths = [base_path]
for expanded_path in expanded_paths:
if not os.path.isdir(expanded_path):
continue
for name in executables:
full_path = os.path.join(expanded_path, name)
if cls._is_valid_executable(full_path):
return full_path
# 5. macOS 特殊处理:检查 .app 包
if current_platform == 'darwin':
found = cls._find_macos_app()
if found:
return found
# 6. 递归搜索特定目录
search_roots = {
'windows': [
os.path.expandvars(r'%ProgramFiles%'),
os.path.expandvars(r'%LocalAppData%'),
],
'darwin': [
'/Applications',
os.path.expanduser('~/Applications'),
],
'linux': [
'/opt',
'/usr/lib',
'/snap',
os.path.expanduser('~/.cache'),
],
}
for search_root in search_roots.get(current_platform, []):
if os.path.isdir(search_root):
found = cls._recursive_search(search_root, executables, max_depth=4)
if found:
return found
# 7. Linux: 使用 find 命令(最后手段)
if current_platform == 'linux':
found = cls._find_with_command(executables)
if found:
return found
return None
@classmethod
def _is_valid_executable(cls, path: str) -> bool:
"""检查路径是否是有效的可执行文件"""
if not path:
return False
# 处理 Windows 路径
path = os.path.normpath(path)
if not os.path.isfile(path):
return False
# Windows 不需要检查执行权限
if cls.get_platform() == 'windows':
return path.lower().endswith('.exe')
return os.access(path, os.X_OK)
@classmethod
def _find_macos_app(cls) -> Optional[str]:
"""macOS 特殊处理:查找 .app 包"""
app_dirs = ['/Applications', os.path.expanduser('~/Applications')]
app_names = [
('Google Chrome.app', 'Contents/MacOS/Google Chrome'),
('Chromium.app', 'Contents/MacOS/Chromium'),
('Microsoft Edge.app', 'Contents/MacOS/Microsoft Edge'),
('Brave Browser.app', 'Contents/MacOS/Brave Browser'),
('Vivaldi.app', 'Contents/MacOS/Vivaldi'),
('Opera.app', 'Contents/MacOS/Opera'),
]
for app_dir in app_dirs:
for app_name, executable_path in app_names:
full_path = os.path.join(app_dir, app_name, executable_path)
if cls._is_valid_executable(full_path):
return full_path
return None
@classmethod
def _recursive_search(cls, root: str, executables: List[str], max_depth: int = 3, current_depth: int = 0) -> Optional[str]:
"""递归搜索目录"""
if current_depth >= max_depth:
return None
# 跳过的目录
skip_dirs = {'.git', 'node_modules', '__pycache__', 'cache', 'tmp', 'temp',
'logs', 'log', '.npm', '.yarn', 'site-packages', 'dist-packages'}
try:
for entry in os.scandir(root):
try:
if entry.is_file(follow_symlinks=True):
if entry.name in executables or entry.name.lower() in [e.lower() for e in executables]:
if cls._is_valid_executable(entry.path):
return entry.path
elif entry.is_dir(follow_symlinks=False):
if entry.name.lower() in skip_dirs:
continue
found = cls._recursive_search(entry.path, executables, max_depth, current_depth + 1)
if found:
return found
except (PermissionError, OSError):
continue
except (PermissionError, OSError):
pass
return None
@classmethod
def _find_with_command(cls, executables: List[str]) -> Optional[str]:
"""使用系统命令查找浏览器(Linux)"""
for name in executables[:5]: # 只检查前几个常见的
try:
result = subprocess.run(
['find', '/', '-maxdepth', '6', '-name', name, '-type', 'f', '-executable', '-print', '-quit'],
capture_output=True,
text=True,
timeout=30,
stderr=subprocess.DEVNULL
)
if result.returncode == 0 and result.stdout.strip():
path = result.stdout.strip().split('\n')[0]
if cls._is_valid_executable(path):
return path
except (subprocess.TimeoutExpired, FileNotFoundError, Exception):
continue
return None
@classmethod
def get_browser_info(cls) -> Dict:
"""获取浏览器信息"""
path = cls.find_browser()
info = {
'found': path is not None,
'path': path,
'version': None,
'platform': cls.get_platform()
}
return info
@classmethod
def find_all_browsers(cls) -> List[Dict]:
"""查找所有可用的浏览器"""
browsers = []
seen_paths = set()
current_platform = cls.get_platform()
executables = cls.BROWSER_EXECUTABLES.get(current_platform, cls.BROWSER_EXECUTABLES['linux'])
# 查找所有可能的浏览器
for name in executables:
path = shutil.which(name)
if path and path not in seen_paths and cls._is_valid_executable(path):
seen_paths.add(path)
browsers.append({'name': name, 'path': path})
# 检查 glob 模式
for pattern in cls.GLOB_PATTERNS:
matches = glob.glob(pattern)
for match in matches:
if match not in seen_paths and cls._is_valid_executable(match):
seen_paths.add(match)
browsers.append({'name': os.path.basename(match), 'path': match})
return browsers
class Config:
"""配置管理器"""
def __init__(self):
self._lock = threading.Lock()
self._browser_path = None
self._user_agent = None
self._max_workers = 1
self._headless = True
self._email_configs = []
self._browser_fingerprint = {}
self._load_from_env()
self._detect_browser()
def _detect_browser(self):
"""检测浏览器路径"""
import os
# 优先使用环境变量
env_path = os.getenv('BROWSER_PATH') or os.getenv('CHROME_PATH')
if env_path and os.path.isfile(env_path):
self._browser_path = env_path
logger.info(f"[Config] 使用环境变量指定的浏览器: {env_path}")
return
# 自动检测
# 自动检测
logger.info(f"[Config] 正在自动检测浏览器路径 (平台: {BrowserPathFinder.get_platform()})...")
browser_info = BrowserPathFinder.get_browser_info()
if browser_info['found']:
self._browser_path = browser_info['path']
if browser_info['found']:
self._browser_path = browser_info['path']
logger.info(f"[Config] ✓ 检测到浏览器: {browser_info['path']}")
if browser_info['version']:
logger.info(f"[Config] ✓ 浏览器版本: {browser_info['version']}")
else:
logger.warning("[Config] ✗ 警告: 未检测到可用的浏览器!")
logger.warning("[Config] 请确保已安装 Chrome/Chromium/Edge 或设置 BROWSER_PATH 环境变量")
logger.warning("[Config] 支持的浏览器: Chrome, Chromium, Edge, Brave, Vivaldi, Opera")
# 列出所有检查过的路径(调试用)
all_browsers = BrowserPathFinder.find_all_browsers()
if all_browsers:
logger.info(f"[Config] 找到以下浏览器但可能不兼容:")
for b in all_browsers:
logger.info(f"[Config] - {b['name']}: {b['path']}")
def get_browser_path(self) -> Optional[str]:
"""获取浏览器路径"""
with self._lock:
return self._browser_path
def set_browser_path(self, path: str):
"""设置浏览器路径"""
with self._lock:
if os.path.isfile(path):
self._browser_path = path
else:
raise ValueError(f"无效的浏览器路径: {path}")
def _load_from_env(self):
"""从环境变量加载配置"""
# User-Agent
self._user_agent = os.getenv(
'USER_AGENT',
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.0.0 Safari/537.36'
)
# 最大工作线程数
self._max_workers = int(os.getenv('MAX_WORKERS', '1'))
# 无头模式
self._headless = os.getenv('HEADLESS', 'true').lower() == 'true'
# 邮箱配置(支持多个,用分号分隔)
self._email_configs = []
worker_domains = os.getenv('WORKER_DOMAINS', '').split(';')
email_domains = os.getenv('EMAIL_DOMAINS', '').split(';')
admin_passwords = os.getenv('ADMIN_PASSWORDS', '').split(';')
# 确保三个列表长度一致
max_len = max(len(worker_domains), len(email_domains), len(admin_passwords))
for i in range(max_len):
worker = worker_domains[i].strip() if i < len(worker_domains) else ''
email = email_domains[i].strip() if i < len(email_domains) else ''
password = admin_passwords[i].strip() if i < len(admin_passwords) else ''
if worker and email and password:
self._email_configs.append({
'worker_domain': worker,
'email_domain': email,
'admin_password': password
})
# 浏览器指纹配置
self._browser_fingerprint = {
'window_size': os.getenv('WINDOW_SIZE', '1920x1080'),
'timezone': os.getenv('TIMEZONE', 'Asia/Shanghai'),
'locale': os.getenv('LOCALE', 'zh-CN'),
'platform': os.getenv('PLATFORM', 'Win64'),
'color_depth': int(os.getenv('COLOR_DEPTH', '24')),
'device_memory': int(os.getenv('DEVICE_MEMORY', '8')),
'hardware_concurrency': int(os.getenv('HARDWARE_CONCURRENCY', '8'))
}
def get_user_agent(self) -> str:
with self._lock:
return self._user_agent
def set_user_agent(self, ua: str):
with self._lock:
self._user_agent = ua
def get_max_workers(self) -> int:
with self._lock:
return self._max_workers
def set_max_workers(self, count: int):
with self._lock:
self._max_workers = max(1, min(count, 10))
def get_headless(self) -> bool:
with self._lock:
return self._headless
def set_headless(self, headless: bool):
with self._lock:
self._headless = headless
def get_email_configs(self) -> List[Dict]:
with self._lock:
return self._email_configs.copy()
def get_email_configs_safe(self) -> List[Dict]:
"""获取邮箱配置(隐藏密码)"""
with self._lock:
return [
{
'worker_domain': c['worker_domain'],
'email_domain': c['email_domain'],
'admin_password': '***'
}
for c in self._email_configs
]
def get_random_email_config(self) -> Optional[Dict]:
with self._lock:
if self._email_configs:
return random.choice(self._email_configs)
return None
def add_email_config(self, worker_domain: str, email_domain: str, admin_password: str):
with self._lock:
self._email_configs.append({
'worker_domain': worker_domain,
'email_domain': email_domain,
'admin_password': admin_password
})
def update_email_config(self, index: int, worker_domain: str = None,
email_domain: str = None, admin_password: str = None):
with self._lock:
if 0 <= index < len(self._email_configs):
if worker_domain:
self._email_configs[index]['worker_domain'] = worker_domain
if email_domain:
self._email_configs[index]['email_domain'] = email_domain
if admin_password:
self._email_configs[index]['admin_password'] = admin_password
else:
raise IndexError("配置索引不存在")
def delete_email_config(self, index: int):
with self._lock:
if 0 <= index < len(self._email_configs):
del self._email_configs[index]
else:
raise IndexError("配置索引不存在")
def get_browser_fingerprint(self) -> Dict:
with self._lock:
return self._browser_fingerprint.copy()
def set_browser_fingerprint(self, fingerprint: Dict):
with self._lock:
self._browser_fingerprint.update(fingerprint)