This commit is contained in:
HuangHai
2026-01-20 14:05:19 +08:00
parent 646f3acd6d
commit d89388ab51
89 changed files with 12526 additions and 0 deletions

View File

@@ -0,0 +1,421 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
下载编排器
协调多种下载策略,实现智能降级和任务管理
"""
import asyncio
import time
import logging
import uuid
from typing import List, Dict, Any, Optional
from dataclasses import dataclass, field
from enum import Enum
from apiproxy.douyin.strategies.base import (
IDownloadStrategy,
DownloadTask,
DownloadResult,
TaskType,
TaskStatus
)
from apiproxy.douyin.strategies.api_strategy import EnhancedAPIStrategy
from apiproxy.douyin.strategies.retry_strategy import RetryStrategy
from .rate_limiter import AdaptiveRateLimiter, RateLimitConfig
logger = logging.getLogger(__name__)
class OrchestratorConfig:
"""编排器配置"""
def __init__(
self,
max_concurrent: int = 5,
enable_retry: bool = True,
enable_rate_limit: bool = True,
rate_limit_config: Optional[RateLimitConfig] = None,
priority_queue: bool = True,
save_progress: bool = True
):
self.max_concurrent = max_concurrent
self.enable_retry = enable_retry
self.enable_rate_limit = enable_rate_limit
self.rate_limit_config = rate_limit_config or RateLimitConfig()
self.priority_queue = priority_queue
self.save_progress = save_progress
class DownloadOrchestrator:
"""下载任务编排器"""
def __init__(self, config: Optional[OrchestratorConfig] = None):
"""
初始化编排器
Args:
config: 编排器配置
"""
self.config = config or OrchestratorConfig()
self.strategies: List[IDownloadStrategy] = []
self.rate_limiter = AdaptiveRateLimiter(self.config.rate_limit_config) if self.config.enable_rate_limit else None
# 任务队列
self.pending_queue = asyncio.Queue()
self.priority_tasks: List[DownloadTask] = []
self.active_tasks: Dict[str, DownloadTask] = {}
self.completed_tasks: List[DownloadTask] = []
self.failed_tasks: List[DownloadTask] = []
# 工作线程
self.workers: List[asyncio.Task] = []
self.running = False
# 统计信息
self.stats = {
'total_tasks': 0,
'completed_tasks': 0,
'failed_tasks': 0,
'retried_tasks': 0,
'average_duration': 0.0,
'success_rate': 0.0
}
# 初始化默认策略
self._init_default_strategies()
def _init_default_strategies(self):
"""初始化默认策略"""
# API策略
api_strategy = EnhancedAPIStrategy()
# 如果启用重试,包装策略
if self.config.enable_retry:
api_strategy = RetryStrategy(api_strategy)
self.register_strategy(api_strategy)
def register_strategy(self, strategy: IDownloadStrategy):
"""
注册下载策略
Args:
strategy: 下载策略实例
"""
self.strategies.append(strategy)
# 按优先级排序
self.strategies.sort(key=lambda s: s.get_priority(), reverse=True)
logger.info(f"注册策略: {strategy.name} (优先级: {strategy.get_priority()})")
async def add_task(self, url: str, task_type: Optional[TaskType] = None, priority: int = 0) -> str:
"""
添加下载任务
Args:
url: 下载URL
task_type: 任务类型
priority: 优先级(数值越大优先级越高)
Returns:
任务ID
"""
# 自动识别任务类型
if task_type is None:
task_type = self._detect_task_type(url)
# 创建任务
task = DownloadTask(
task_id=str(uuid.uuid4()),
url=url,
task_type=task_type,
priority=priority
)
# 添加到队列
if self.config.priority_queue and priority > 0:
self.priority_tasks.append(task)
self.priority_tasks.sort(key=lambda t: t.priority, reverse=True)
else:
await self.pending_queue.put(task)
self.stats['total_tasks'] += 1
logger.info(f"添加任务: {task.task_id} ({task_type.value}) 优先级: {priority}")
return task.task_id
async def add_batch(self, urls: List[str], task_type: Optional[TaskType] = None) -> List[str]:
"""
批量添加任务
Args:
urls: URL列表
task_type: 任务类型
Returns:
任务ID列表
"""
task_ids = []
for i, url in enumerate(urls):
# 批量任务使用递减优先级
priority = len(urls) - i
task_id = await self.add_task(url, task_type, priority)
task_ids.append(task_id)
return task_ids
async def start(self):
"""启动编排器"""
if self.running:
logger.warning("编排器已在运行")
return
self.running = True
logger.info(f"启动编排器,最大并发数: {self.config.max_concurrent}")
# 创建工作线程
for i in range(self.config.max_concurrent):
worker = asyncio.create_task(self._worker(i))
self.workers.append(worker)
async def stop(self):
"""停止编排器"""
if not self.running:
return
logger.info("停止编排器...")
self.running = False
# 取消所有工作线程
for worker in self.workers:
worker.cancel()
# 等待工作线程结束
await asyncio.gather(*self.workers, return_exceptions=True)
self.workers.clear()
logger.info("编排器已停止")
async def wait_completion(self, timeout: Optional[float] = None):
"""
等待所有任务完成
Args:
timeout: 超时时间(秒)
"""
start_time = time.time()
while self.running:
# 检查是否所有任务都完成
if (self.pending_queue.empty() and
not self.priority_tasks and
not self.active_tasks):
logger.info("所有任务已完成")
break
# 检查超时
if timeout and (time.time() - start_time) > timeout:
logger.warning(f"等待超时 ({timeout} 秒)")
break
await asyncio.sleep(1)
# 计算统计信息
self._calculate_stats()
async def _worker(self, worker_id: int):
"""
工作线程
Args:
worker_id: 工作线程ID
"""
logger.info(f"工作线程 {worker_id} 启动")
while self.running:
try:
# 获取任务
task = await self._get_next_task()
if task is None:
await asyncio.sleep(0.1)
continue
# 标记为活动任务
self.active_tasks[task.task_id] = task
# 限速控制
if self.rate_limiter:
await self.rate_limiter.acquire()
# 执行任务
logger.info(f"工作线程 {worker_id} 开始处理任务: {task.task_id}")
result = await self._execute_task(task)
# 移除活动任务
del self.active_tasks[task.task_id]
# 处理结果
if result.success:
self.completed_tasks.append(task)
self.stats['completed_tasks'] += 1
logger.info(f"任务 {task.task_id} 完成")
else:
# 检查是否需要重试
if task.increment_retry():
logger.warning(f"任务 {task.task_id} 失败,准备重试 ({task.retry_count}/{task.max_retries})")
await self.pending_queue.put(task)
self.stats['retried_tasks'] += 1
else:
self.failed_tasks.append(task)
self.stats['failed_tasks'] += 1
logger.error(f"任务 {task.task_id} 最终失败: {result.error_message}")
# 保存进度
if self.config.save_progress:
await self._save_progress()
except asyncio.CancelledError:
logger.info(f"工作线程 {worker_id} 被取消")
break
except Exception as e:
logger.error(f"工作线程 {worker_id} 异常: {e}")
await asyncio.sleep(1)
logger.info(f"工作线程 {worker_id} 结束")
async def _get_next_task(self) -> Optional[DownloadTask]:
"""获取下一个任务"""
# 优先处理高优先级任务
if self.priority_tasks:
return self.priority_tasks.pop(0)
# 从普通队列获取
try:
return await asyncio.wait_for(
self.pending_queue.get(),
timeout=0.1
)
except asyncio.TimeoutError:
return None
async def _execute_task(self, task: DownloadTask) -> DownloadResult:
"""
执行任务,尝试所有策略
Args:
task: 下载任务
Returns:
下载结果
"""
last_error = None
for strategy in self.strategies:
try:
# 检查策略是否能处理任务
if not await strategy.can_handle(task):
continue
logger.info(f"使用策略 {strategy.name} 处理任务 {task.task_id}")
# 执行下载
result = await strategy.download(task)
if result.success:
return result
last_error = result.error_message
logger.warning(f"策略 {strategy.name} 失败: {last_error}")
except Exception as e:
last_error = str(e)
logger.error(f"策略 {strategy.name} 异常: {e}")
# 所有策略都失败
return DownloadResult(
success=False,
task_id=task.task_id,
error_message=f"所有策略都失败: {last_error}",
retry_count=task.retry_count
)
def _detect_task_type(self, url: str) -> TaskType:
"""
自动检测任务类型
Args:
url: URL
Returns:
任务类型
"""
url_lower = url.lower()
if '/user/' in url_lower:
return TaskType.USER
elif '/video/' in url_lower or '/note/' in url_lower:
return TaskType.VIDEO
elif '/music/' in url_lower:
return TaskType.MUSIC
elif '/mix/' in url_lower or '/collection/' in url_lower:
return TaskType.MIX
elif 'live.douyin.com' in url_lower:
return TaskType.LIVE
else:
return TaskType.VIDEO # 默认为视频
def _calculate_stats(self):
"""计算统计信息"""
total = self.stats['total_tasks']
if total > 0:
self.stats['success_rate'] = self.stats['completed_tasks'] / total * 100
# 计算平均时长
durations = []
for task in self.completed_tasks:
if hasattr(task, 'duration'):
durations.append(task.duration)
if durations:
self.stats['average_duration'] = sum(durations) / len(durations)
async def _save_progress(self):
"""保存进度(可扩展为持久化到文件或数据库)"""
# TODO: 实现进度保存逻辑
pass
def get_stats(self) -> Dict[str, Any]:
"""获取统计信息"""
self._calculate_stats()
return self.stats.copy()
def get_task_status(self, task_id: str) -> Optional[TaskStatus]:
"""
获取任务状态
Args:
task_id: 任务ID
Returns:
任务状态
"""
# 检查活动任务
if task_id in self.active_tasks:
return self.active_tasks[task_id].status
# 检查完成任务
for task in self.completed_tasks:
if task.task_id == task_id:
return TaskStatus.COMPLETED
# 检查失败任务
for task in self.failed_tasks:
if task.task_id == task_id:
return TaskStatus.FAILED
# 检查待处理任务
for task in self.priority_tasks:
if task.task_id == task_id:
return TaskStatus.PENDING
return None

View File

@@ -0,0 +1,503 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
实时进度跟踪系统
支持WebSocket推送和进度监控
"""
import asyncio
import json
import time
import logging
from typing import Dict, List, Optional, Callable, Any
from dataclasses import dataclass, field, asdict
from enum import Enum
from datetime import datetime
logger = logging.getLogger(__name__)
# 动态导入WebSocket支持
try:
import websockets
from websockets.server import WebSocketServerProtocol
WEBSOCKET_AVAILABLE = True
except ImportError:
WEBSOCKET_AVAILABLE = False
logger.warning("websockets未安装,WebSocket功能不可用")
class EventType(Enum):
"""事件类型"""
TASK_ADDED = "task_added"
TASK_STARTED = "task_started"
TASK_PROGRESS = "task_progress"
TASK_COMPLETED = "task_completed"
TASK_FAILED = "task_failed"
TASK_RETRYING = "task_retrying"
STATS_UPDATE = "stats_update"
SPEED_UPDATE = "speed_update"
ERROR = "error"
INFO = "info"
@dataclass
class ProgressEvent:
"""进度事件"""
event_type: EventType
task_id: Optional[str] = None
data: Dict[str, Any] = field(default_factory=dict)
timestamp: float = field(default_factory=time.time)
def to_dict(self) -> Dict:
"""转换为字典"""
return {
'event_type': self.event_type.value,
'task_id': self.task_id,
'data': self.data,
'timestamp': self.timestamp
}
def to_json(self) -> str:
"""转换为JSON"""
return json.dumps(self.to_dict())
@dataclass
class TaskProgress:
"""任务进度信息"""
task_id: str
url: str
status: str
progress: float = 0.0 # 0-100
downloaded_bytes: int = 0
total_bytes: int = 0
speed: float = 0.0 # bytes/second
eta: float = 0.0 # seconds
start_time: float = field(default_factory=time.time)
end_time: Optional[float] = None
error_message: Optional[str] = None
def get_duration(self) -> float:
"""获取耗时"""
if self.end_time:
return self.end_time - self.start_time
return time.time() - self.start_time
def update_progress(self, downloaded: int, total: int):
"""更新进度"""
self.downloaded_bytes = downloaded
self.total_bytes = total
if total > 0:
self.progress = (downloaded / total) * 100
# 计算速度
duration = self.get_duration()
if duration > 0:
self.speed = downloaded / duration
# 计算剩余时间
if self.speed > 0 and total > downloaded:
self.eta = (total - downloaded) / self.speed
def to_dict(self) -> Dict:
"""转换为字典"""
return {
'task_id': self.task_id,
'url': self.url,
'status': self.status,
'progress': round(self.progress, 2),
'downloaded_bytes': self.downloaded_bytes,
'total_bytes': self.total_bytes,
'speed': round(self.speed, 2),
'eta': round(self.eta, 2),
'duration': round(self.get_duration(), 2),
'error_message': self.error_message
}
class ProgressTracker:
"""进度跟踪器"""
def __init__(self, enable_websocket: bool = True, ws_port: int = 8765):
"""
初始化进度跟踪器
Args:
enable_websocket: 是否启用WebSocket
ws_port: WebSocket端口
"""
self.enable_websocket = enable_websocket and WEBSOCKET_AVAILABLE
self.ws_port = ws_port
# 任务进度
self.tasks: Dict[str, TaskProgress] = {}
# 事件监听器
self.listeners: List[Callable[[ProgressEvent], None]] = []
# WebSocket连接
self.websocket_clients: List[WebSocketServerProtocol] = []
self.websocket_server = None
self.ws_task = None
# 统计信息
self.stats = {
'total_tasks': 0,
'active_tasks': 0,
'completed_tasks': 0,
'failed_tasks': 0,
'total_downloaded': 0,
'average_speed': 0.0,
'success_rate': 0.0
}
# 速度历史(用于计算平均速度)
self.speed_history = []
self.max_speed_history = 100
def add_listener(self, listener: Callable[[ProgressEvent], None]):
"""添加事件监听器"""
self.listeners.append(listener)
logger.debug(f"添加事件监听器: {listener}")
def remove_listener(self, listener: Callable[[ProgressEvent], None]):
"""移除事件监听器"""
if listener in self.listeners:
self.listeners.remove(listener)
async def emit_event(self, event: ProgressEvent):
"""触发事件"""
# 通知监听器
for listener in self.listeners:
try:
if asyncio.iscoroutinefunction(listener):
await listener(event)
else:
listener(event)
except Exception as e:
logger.error(f"事件监听器执行失败: {e}")
# 推送到WebSocket客户端
if self.websocket_clients:
await self._broadcast_websocket(event.to_json())
async def add_task(self, task_id: str, url: str):
"""添加任务"""
self.tasks[task_id] = TaskProgress(
task_id=task_id,
url=url,
status="pending"
)
self.stats['total_tasks'] += 1
await self.emit_event(ProgressEvent(
event_type=EventType.TASK_ADDED,
task_id=task_id,
data={'url': url}
))
async def start_task(self, task_id: str):
"""开始任务"""
if task_id in self.tasks:
self.tasks[task_id].status = "processing"
self.tasks[task_id].start_time = time.time()
self.stats['active_tasks'] += 1
await self.emit_event(ProgressEvent(
event_type=EventType.TASK_STARTED,
task_id=task_id
))
async def update_progress(
self,
task_id: str,
downloaded: int,
total: int,
extra_data: Optional[Dict] = None
):
"""更新任务进度"""
if task_id not in self.tasks:
return
task = self.tasks[task_id]
task.update_progress(downloaded, total)
# 更新速度历史
if task.speed > 0:
self.speed_history.append(task.speed)
if len(self.speed_history) > self.max_speed_history:
self.speed_history.pop(0)
# 计算平均速度
self.stats['average_speed'] = sum(self.speed_history) / len(self.speed_history)
event_data = task.to_dict()
if extra_data:
event_data.update(extra_data)
await self.emit_event(ProgressEvent(
event_type=EventType.TASK_PROGRESS,
task_id=task_id,
data=event_data
))
async def complete_task(self, task_id: str, success: bool = True, error: Optional[str] = None):
"""完成任务"""
if task_id not in self.tasks:
return
task = self.tasks[task_id]
task.end_time = time.time()
if success:
task.status = "completed"
task.progress = 100.0
self.stats['completed_tasks'] += 1
event_type = EventType.TASK_COMPLETED
else:
task.status = "failed"
task.error_message = error
self.stats['failed_tasks'] += 1
event_type = EventType.TASK_FAILED
self.stats['active_tasks'] = max(0, self.stats['active_tasks'] - 1)
# 更新成功率
total_finished = self.stats['completed_tasks'] + self.stats['failed_tasks']
if total_finished > 0:
self.stats['success_rate'] = (self.stats['completed_tasks'] / total_finished) * 100
await self.emit_event(ProgressEvent(
event_type=event_type,
task_id=task_id,
data=task.to_dict()
))
async def retry_task(self, task_id: str, retry_count: int):
"""重试任务"""
if task_id in self.tasks:
self.tasks[task_id].status = "retrying"
await self.emit_event(ProgressEvent(
event_type=EventType.TASK_RETRYING,
task_id=task_id,
data={'retry_count': retry_count}
))
async def update_stats(self):
"""更新统计信息"""
await self.emit_event(ProgressEvent(
event_type=EventType.STATS_UPDATE,
data=self.stats.copy()
))
def get_task_progress(self, task_id: str) -> Optional[TaskProgress]:
"""获取任务进度"""
return self.tasks.get(task_id)
def get_active_tasks(self) -> List[TaskProgress]:
"""获取活动任务"""
return [
task for task in self.tasks.values()
if task.status in ["processing", "retrying"]
]
def get_statistics(self) -> Dict[str, Any]:
"""获取统计信息"""
return self.stats.copy()
def clear_completed_tasks(self):
"""清理已完成的任务"""
completed_ids = [
task_id for task_id, task in self.tasks.items()
if task.status in ["completed", "failed"]
]
for task_id in completed_ids:
del self.tasks[task_id]
logger.info(f"清理了 {len(completed_ids)} 个已完成任务")
# WebSocket相关功能
async def start_websocket_server(self):
"""启动WebSocket服务器"""
if not self.enable_websocket:
logger.info("WebSocket功能未启用")
return
try:
self.websocket_server = await websockets.serve(
self._handle_websocket,
"localhost",
self.ws_port
)
logger.info(f"WebSocket服务器启动在 ws://localhost:{self.ws_port}")
except Exception as e:
logger.error(f"启动WebSocket服务器失败: {e}")
async def stop_websocket_server(self):
"""停止WebSocket服务器"""
if self.websocket_server:
self.websocket_server.close()
await self.websocket_server.wait_closed()
self.websocket_server = None
logger.info("WebSocket服务器已停止")
async def _handle_websocket(self, websocket: WebSocketServerProtocol, path: str):
"""处理WebSocket连接"""
logger.info(f"新的WebSocket连接: {websocket.remote_address}")
# 添加客户端
self.websocket_clients.append(websocket)
try:
# 发送当前状态
await websocket.send(json.dumps({
'type': 'init',
'data': {
'tasks': {
task_id: task.to_dict()
for task_id, task in self.tasks.items()
},
'stats': self.stats
}
}))
# 保持连接
async for message in websocket:
try:
data = json.loads(message)
await self._handle_ws_message(websocket, data)
except json.JSONDecodeError:
logger.warning(f"无效的WebSocket消息: {message}")
except websockets.exceptions.ConnectionClosed:
logger.info(f"WebSocket连接关闭: {websocket.remote_address}")
finally:
# 移除客户端
if websocket in self.websocket_clients:
self.websocket_clients.remove(websocket)
async def _handle_ws_message(self, websocket: WebSocketServerProtocol, data: Dict):
"""处理WebSocket消息"""
msg_type = data.get('type')
if msg_type == 'ping':
await websocket.send(json.dumps({'type': 'pong'}))
elif msg_type == 'get_stats':
await websocket.send(json.dumps({
'type': 'stats',
'data': self.stats
}))
elif msg_type == 'get_tasks':
await websocket.send(json.dumps({
'type': 'tasks',
'data': {
task_id: task.to_dict()
for task_id, task in self.tasks.items()
}
}))
async def _broadcast_websocket(self, message: str):
"""广播WebSocket消息"""
if not self.websocket_clients:
return
# 并发发送给所有客户端
disconnected = []
for client in self.websocket_clients:
try:
await client.send(message)
except websockets.exceptions.ConnectionClosed:
disconnected.append(client)
# 清理断开的连接
for client in disconnected:
self.websocket_clients.remove(client)
# 上下文管理器
async def __aenter__(self):
"""异步上下文管理器入口"""
await self.start_websocket_server()
return self
async def __aexit__(self, exc_type, exc_val, exc_tb):
"""异步上下文管理器出口"""
await self.stop_websocket_server()
def create_console_listener(use_rich: bool = True):
"""
创建控制台进度监听器
Args:
use_rich: 是否使用Rich库美化输出
Returns:
监听器函数
"""
if use_rich:
try:
from rich.console import Console
from rich.progress import Progress, SpinnerColumn, TextColumn, BarColumn
from rich.table import Table
console = Console()
progress = Progress(
SpinnerColumn(),
TextColumn("[progress.description]{task.description}"),
BarColumn(),
TextColumn("[progress.percentage]{task.percentage:>3.0f}%"),
)
task_map = {}
def rich_listener(event: ProgressEvent):
if event.event_type == EventType.TASK_ADDED:
task_id = event.task_id
if task_id not in task_map:
task_map[task_id] = progress.add_task(
f"[cyan]{event.data.get('url', 'Unknown')}",
total=100
)
elif event.event_type == EventType.TASK_PROGRESS:
task_id = event.task_id
if task_id in task_map:
progress.update(
task_map[task_id],
completed=event.data.get('progress', 0)
)
elif event.event_type == EventType.TASK_COMPLETED:
console.print(f"[green]✓ 任务完成: {event.task_id}")
elif event.event_type == EventType.TASK_FAILED:
console.print(f"[red]✗ 任务失败: {event.task_id} - {event.data.get('error_message', 'Unknown error')}")
return rich_listener
except ImportError:
pass
# 默认简单监听器
def simple_listener(event: ProgressEvent):
timestamp = datetime.fromtimestamp(event.timestamp).strftime('%H:%M:%S')
if event.event_type == EventType.TASK_PROGRESS:
progress = event.data.get('progress', 0)
speed = event.data.get('speed', 0)
print(f"[{timestamp}] 任务 {event.task_id}: {progress:.1f}% ({speed/1024/1024:.2f} MB/s)")
elif event.event_type == EventType.TASK_COMPLETED:
print(f"[{timestamp}] ✓ 任务完成: {event.task_id}")
elif event.event_type == EventType.TASK_FAILED:
print(f"[{timestamp}] ✗ 任务失败: {event.task_id}")
return simple_listener

View File

@@ -0,0 +1,494 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
队列管理器
支持任务持久化和断点恢复
"""
import asyncio
import json
import sqlite3
import time
import logging
import pickle
from typing import List, Dict, Optional, Any
from pathlib import Path
from dataclasses import dataclass, asdict
from enum import Enum
from apiproxy.douyin.strategies.base import DownloadTask, TaskStatus, TaskType
logger = logging.getLogger(__name__)
class PersistentQueue:
"""持久化队列管理器"""
def __init__(
self,
db_path: str = "download_queue.db",
max_size: int = 10000,
checkpoint_interval: int = 60
):
"""
初始化队列管理器
Args:
db_path: 数据库文件路径
max_size: 队列最大容量
checkpoint_interval: 检查点保存间隔(秒)
"""
self.db_path = Path(db_path)
self.max_size = max_size
self.checkpoint_interval = checkpoint_interval
self.conn: Optional[sqlite3.Connection] = None
self.queue = asyncio.Queue(maxsize=max_size)
self._checkpoint_task = None
self._lock = asyncio.Lock()
# 初始化数据库
self._init_database()
# 恢复未完成的任务
self._restore_tasks()
def _init_database(self):
"""初始化数据库"""
self.conn = sqlite3.connect(str(self.db_path), check_same_thread=False)
cursor = self.conn.cursor()
# 创建任务表
cursor.execute('''
CREATE TABLE IF NOT EXISTS tasks (
task_id TEXT PRIMARY KEY,
url TEXT NOT NULL,
task_type TEXT NOT NULL,
priority INTEGER DEFAULT 0,
status TEXT NOT NULL,
retry_count INTEGER DEFAULT 0,
max_retries INTEGER DEFAULT 3,
metadata TEXT,
created_at REAL NOT NULL,
updated_at REAL NOT NULL,
completed_at REAL,
error_message TEXT,
result TEXT
)
''')
# 创建索引
cursor.execute('CREATE INDEX IF NOT EXISTS idx_status ON tasks(status)')
cursor.execute('CREATE INDEX IF NOT EXISTS idx_priority ON tasks(priority DESC)')
cursor.execute('CREATE INDEX IF NOT EXISTS idx_created_at ON tasks(created_at)')
# 创建进度表
cursor.execute('''
CREATE TABLE IF NOT EXISTS progress (
id INTEGER PRIMARY KEY AUTOINCREMENT,
timestamp REAL NOT NULL,
total_tasks INTEGER,
pending_tasks INTEGER,
active_tasks INTEGER,
completed_tasks INTEGER,
failed_tasks INTEGER,
success_rate REAL,
average_duration REAL
)
''')
self.conn.commit()
logger.info(f"数据库初始化完成: {self.db_path}")
def _restore_tasks(self):
"""从数据库恢复未完成的任务"""
cursor = self.conn.cursor()
# 将所有PROCESSING状态的任务重置为PENDING
cursor.execute('''
UPDATE tasks
SET status = ?, updated_at = ?
WHERE status = ?
''', (TaskStatus.PENDING.value, time.time(), TaskStatus.PROCESSING.value))
# 获取所有待处理的任务
cursor.execute('''
SELECT task_id, url, task_type, priority, retry_count, max_retries, metadata, created_at
FROM tasks
WHERE status IN (?, ?)
ORDER BY priority DESC, created_at ASC
''', (TaskStatus.PENDING.value, TaskStatus.RETRYING.value))
restored_count = 0
for row in cursor.fetchall():
task = self._row_to_task(row)
if task:
try:
self.queue.put_nowait(task)
restored_count += 1
except asyncio.QueueFull:
break
self.conn.commit()
if restored_count > 0:
logger.info(f"从数据库恢复了 {restored_count} 个未完成任务")
def _row_to_task(self, row: tuple) -> Optional[DownloadTask]:
"""将数据库行转换为任务对象"""
try:
task_id, url, task_type, priority, retry_count, max_retries, metadata_str, created_at = row
metadata = {}
if metadata_str:
try:
metadata = json.loads(metadata_str)
except:
pass
return DownloadTask(
task_id=task_id,
url=url,
task_type=TaskType(task_type),
priority=priority,
retry_count=retry_count,
max_retries=max_retries,
metadata=metadata,
created_at=created_at
)
except Exception as e:
logger.error(f"转换任务失败: {e}")
return None
async def add_task(self, task: DownloadTask) -> bool:
"""
添加任务到队列
Args:
task: 下载任务
Returns:
是否成功添加
"""
async with self._lock:
try:
# 保存到数据库
cursor = self.conn.cursor()
cursor.execute('''
INSERT OR REPLACE INTO tasks (
task_id, url, task_type, priority, status,
retry_count, max_retries, metadata,
created_at, updated_at
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
''', (
task.task_id,
task.url,
task.task_type.value,
task.priority,
task.status.value,
task.retry_count,
task.max_retries,
json.dumps(task.metadata),
task.created_at,
task.updated_at
))
self.conn.commit()
# 添加到内存队列
await self.queue.put(task)
logger.debug(f"任务 {task.task_id} 已添加到队列")
return True
except Exception as e:
logger.error(f"添加任务失败: {e}")
return False
async def get_task(self, timeout: float = 1.0) -> Optional[DownloadTask]:
"""
从队列获取任务
Args:
timeout: 超时时间
Returns:
下载任务
"""
try:
task = await asyncio.wait_for(self.queue.get(), timeout=timeout)
# 更新数据库状态
await self.update_task_status(task.task_id, TaskStatus.PROCESSING)
return task
except asyncio.TimeoutError:
return None
async def update_task_status(
self,
task_id: str,
status: TaskStatus,
error_message: Optional[str] = None,
result: Optional[Dict] = None
):
"""
更新任务状态
Args:
task_id: 任务ID
status: 新状态
error_message: 错误信息
result: 执行结果
"""
async with self._lock:
cursor = self.conn.cursor()
update_fields = {
'status': status.value,
'updated_at': time.time()
}
if error_message:
update_fields['error_message'] = error_message
if result:
update_fields['result'] = json.dumps(result)
if status == TaskStatus.COMPLETED:
update_fields['completed_at'] = time.time()
# 构建UPDATE语句
set_clause = ', '.join([f'{k} = ?' for k in update_fields.keys()])
values = list(update_fields.values()) + [task_id]
cursor.execute(
f'UPDATE tasks SET {set_clause} WHERE task_id = ?',
values
)
self.conn.commit()
async def requeue_task(self, task: DownloadTask):
"""
将任务重新加入队列
Args:
task: 下载任务
"""
task.retry_count += 1
task.status = TaskStatus.RETRYING
task.updated_at = time.time()
await self.add_task(task)
logger.info(f"任务 {task.task_id} 重新加入队列 (重试 {task.retry_count}/{task.max_retries})")
def get_statistics(self) -> Dict[str, Any]:
"""获取队列统计信息"""
cursor = self.conn.cursor()
# 统计各状态任务数
cursor.execute('''
SELECT status, COUNT(*)
FROM tasks
GROUP BY status
''')
status_counts = {}
for status, count in cursor.fetchall():
status_counts[status] = count
# 计算成功率
total = sum(status_counts.values())
completed = status_counts.get(TaskStatus.COMPLETED.value, 0)
success_rate = (completed / total * 100) if total > 0 else 0
# 计算平均耗时
cursor.execute('''
SELECT AVG(completed_at - created_at)
FROM tasks
WHERE status = ? AND completed_at IS NOT NULL
''', (TaskStatus.COMPLETED.value,))
avg_duration = cursor.fetchone()[0] or 0
stats = {
'total_tasks': total,
'pending_tasks': status_counts.get(TaskStatus.PENDING.value, 0),
'processing_tasks': status_counts.get(TaskStatus.PROCESSING.value, 0),
'completed_tasks': completed,
'failed_tasks': status_counts.get(TaskStatus.FAILED.value, 0),
'retrying_tasks': status_counts.get(TaskStatus.RETRYING.value, 0),
'success_rate': success_rate,
'average_duration': avg_duration,
'queue_size': self.queue.qsize()
}
return stats
async def save_progress(self):
"""保存进度到数据库"""
stats = self.get_statistics()
cursor = self.conn.cursor()
cursor.execute('''
INSERT INTO progress (
timestamp, total_tasks, pending_tasks, active_tasks,
completed_tasks, failed_tasks, success_rate, average_duration
) VALUES (?, ?, ?, ?, ?, ?, ?, ?)
''', (
time.time(),
stats['total_tasks'],
stats['pending_tasks'],
stats['processing_tasks'],
stats['completed_tasks'],
stats['failed_tasks'],
stats['success_rate'],
stats['average_duration']
))
self.conn.commit()
logger.debug("进度已保存")
async def start_checkpoint(self):
"""启动检查点保存任务"""
if not self._checkpoint_task:
self._checkpoint_task = asyncio.create_task(self._checkpoint_loop())
logger.info("检查点保存任务已启动")
async def stop_checkpoint(self):
"""停止检查点保存任务"""
if self._checkpoint_task:
self._checkpoint_task.cancel()
try:
await self._checkpoint_task
except asyncio.CancelledError:
pass
self._checkpoint_task = None
logger.info("检查点保存任务已停止")
async def _checkpoint_loop(self):
"""检查点保存循环"""
while True:
try:
await asyncio.sleep(self.checkpoint_interval)
await self.save_progress()
except asyncio.CancelledError:
break
except Exception as e:
logger.error(f"保存检查点失败: {e}")
def get_recent_progress(self, hours: int = 24) -> List[Dict]:
"""
获取最近的进度记录
Args:
hours: 获取最近多少小时的记录
Returns:
进度记录列表
"""
cursor = self.conn.cursor()
since = time.time() - hours * 3600
cursor.execute('''
SELECT timestamp, total_tasks, completed_tasks, failed_tasks, success_rate
FROM progress
WHERE timestamp > ?
ORDER BY timestamp DESC
LIMIT 100
''', (since,))
records = []
for row in cursor.fetchall():
records.append({
'timestamp': row[0],
'total_tasks': row[1],
'completed_tasks': row[2],
'failed_tasks': row[3],
'success_rate': row[4]
})
return records
def cleanup_old_tasks(self, days: int = 7):
"""
清理旧任务记录
Args:
days: 保留最近多少天的记录
"""
cursor = self.conn.cursor()
cutoff = time.time() - days * 86400
cursor.execute('''
DELETE FROM tasks
WHERE status IN (?, ?) AND updated_at < ?
''', (TaskStatus.COMPLETED.value, TaskStatus.FAILED.value, cutoff))
deleted = cursor.rowcount
self.conn.commit()
if deleted > 0:
logger.info(f"清理了 {deleted} 条旧任务记录")
def export_tasks(self, status: Optional[TaskStatus] = None) -> List[Dict]:
"""
导出任务列表
Args:
status: 筛选状态
Returns:
任务列表
"""
cursor = self.conn.cursor()
if status:
cursor.execute('''
SELECT * FROM tasks WHERE status = ?
ORDER BY created_at DESC
''', (status.value,))
else:
cursor.execute('''
SELECT * FROM tasks
ORDER BY created_at DESC
''')
tasks = []
columns = [desc[0] for desc in cursor.description]
for row in cursor.fetchall():
task_dict = dict(zip(columns, row))
# 解析JSON字段
if task_dict.get('metadata'):
try:
task_dict['metadata'] = json.loads(task_dict['metadata'])
except:
pass
if task_dict.get('result'):
try:
task_dict['result'] = json.loads(task_dict['result'])
except:
pass
tasks.append(task_dict)
return tasks
def close(self):
"""关闭数据库连接"""
if self.conn:
self.conn.close()
self.conn = None
logger.info("数据库连接已关闭")
async def __aenter__(self):
"""异步上下文管理器入口"""
await self.start_checkpoint()
return self
async def __aexit__(self, exc_type, exc_val, exc_tb):
"""异步上下文管理器出口"""
await self.stop_checkpoint()
await self.save_progress()
self.close()

View File

@@ -0,0 +1,308 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
智能限速器
防止请求过快导致被封禁
"""
import asyncio
import time
import logging
from collections import deque
from typing import Optional, Dict, Any
from dataclasses import dataclass, field
from enum import Enum
logger = logging.getLogger(__name__)
class RateLimitStrategy(Enum):
"""限速策略枚举"""
FIXED = "fixed" # 固定速率
ADAPTIVE = "adaptive" # 自适应速率
BURST = "burst" # 突发模式
@dataclass
class RateLimitConfig:
"""限速配置"""
max_per_second: int = 2
max_per_minute: int = 30
max_per_hour: int = 1000
burst_size: int = 5
strategy: RateLimitStrategy = RateLimitStrategy.ADAPTIVE
cooldown_time: int = 60 # 触发限制后的冷却时间(秒)
class AdaptiveRateLimiter:
"""自适应限速器"""
def __init__(self, config: Optional[RateLimitConfig] = None):
"""
初始化限速器
Args:
config: 限速配置
"""
self.config = config or RateLimitConfig()
self.requests = deque()
self.failures = deque()
self.lock = asyncio.Lock()
# 当前限制值(可动态调整)
self.current_max_per_second = self.config.max_per_second
self.current_max_per_minute = self.config.max_per_minute
self.current_max_per_hour = self.config.max_per_hour
# 统计信息
self.stats = {
'total_requests': 0,
'blocked_requests': 0,
'rate_adjustments': 0,
'current_rate': self.current_max_per_second,
'failure_rate': 0.0
}
# 冷却状态
self.cooldown_until = 0
async def acquire(self) -> bool:
"""
获取请求许可
Returns:
是否获得许可
"""
async with self.lock:
now = time.time()
# 检查是否在冷却期
if self.cooldown_until > now:
remaining = self.cooldown_until - now
logger.warning(f"限速器处于冷却期,还需等待 {remaining:.1f} 秒")
await asyncio.sleep(remaining)
self.cooldown_until = 0
# 清理过期记录
self._clean_old_records(now)
# 检查速率限制
while not self._can_proceed(now):
# 计算需要等待的时间
wait_time = self._calculate_wait_time(now)
if wait_time > 0:
logger.debug(f"速率限制,等待 {wait_time:.2f} 秒")
await asyncio.sleep(wait_time)
now = time.time()
self._clean_old_records(now)
else:
# 无法继续,记录被阻塞的请求
self.stats['blocked_requests'] += 1
return False
# 记录请求
self.requests.append(now)
self.stats['total_requests'] += 1
# 自适应调整
if self.config.strategy == RateLimitStrategy.ADAPTIVE:
self._adjust_rate()
return True
async def __aenter__(self):
"""异步上下文管理器入口"""
await self.acquire()
return self
async def __aexit__(self, exc_type, exc_val, exc_tb):
"""异步上下文管理器出口"""
if exc_type is not None:
# 发生异常,记录失败
self.record_failure()
def record_failure(self):
"""记录失败请求"""
now = time.time()
self.failures.append(now)
# 自适应调整
if self.config.strategy == RateLimitStrategy.ADAPTIVE:
self._handle_failure()
def _can_proceed(self, now: float) -> bool:
"""检查是否可以继续请求"""
# 检查每秒限制
recent_second = [r for r in self.requests if now - r < 1]
if len(recent_second) >= self.current_max_per_second:
return False
# 检查每分钟限制
recent_minute = [r for r in self.requests if now - r < 60]
if len(recent_minute) >= self.current_max_per_minute:
return False
# 检查每小时限制
recent_hour = [r for r in self.requests if now - r < 3600]
if len(recent_hour) >= self.current_max_per_hour:
return False
# 突发模式检查
if self.config.strategy == RateLimitStrategy.BURST:
recent_burst = [r for r in self.requests if now - r < 0.1]
if len(recent_burst) >= self.config.burst_size:
return False
return True
def _calculate_wait_time(self, now: float) -> float:
"""计算需要等待的时间"""
wait_times = []
# 计算每秒限制的等待时间
recent_second = [r for r in self.requests if now - r < 1]
if len(recent_second) >= self.current_max_per_second:
oldest_in_second = min(recent_second)
wait_times.append(1 - (now - oldest_in_second))
# 计算每分钟限制的等待时间
recent_minute = [r for r in self.requests if now - r < 60]
if len(recent_minute) >= self.current_max_per_minute:
oldest_in_minute = min(recent_minute)
wait_times.append(60 - (now - oldest_in_minute))
# 返回最小等待时间
return min(wait_times) if wait_times else 0.1
def _clean_old_records(self, now: float):
"""清理过期记录"""
# 保留最近1小时的请求记录
while self.requests and now - self.requests[0] > 3600:
self.requests.popleft()
# 保留最近10分钟的失败记录
while self.failures and now - self.failures[0] > 600:
self.failures.popleft()
def _adjust_rate(self):
"""自适应调整速率"""
now = time.time()
# 计算失败率
recent_failures = [f for f in self.failures if now - f < 60]
recent_requests = [r for r in self.requests if now - r < 60]
if len(recent_requests) > 10:
failure_rate = len(recent_failures) / len(recent_requests)
self.stats['failure_rate'] = failure_rate
if failure_rate > 0.3:
# 失败率过高,降低速率
self._decrease_rate()
elif failure_rate < 0.05 and len(recent_requests) > 20:
# 失败率很低,尝试提高速率
self._increase_rate()
def _handle_failure(self):
"""处理失败,调整限速策略"""
now = time.time()
recent_failures = [f for f in self.failures if now - f < 10]
# 如果短时间内失败次数过多,触发冷却
if len(recent_failures) >= 5:
logger.warning(f"检测到频繁失败,进入冷却期 {self.config.cooldown_time} 秒")
self.cooldown_until = now + self.config.cooldown_time
self._decrease_rate()
def _decrease_rate(self):
"""降低请求速率"""
old_rate = self.current_max_per_second
self.current_max_per_second = max(1, int(self.current_max_per_second * 0.7))
self.current_max_per_minute = max(10, int(self.current_max_per_minute * 0.7))
self.current_max_per_hour = max(100, int(self.current_max_per_hour * 0.7))
if old_rate != self.current_max_per_second:
self.stats['rate_adjustments'] += 1
self.stats['current_rate'] = self.current_max_per_second
logger.info(f"降低请求速率: {old_rate}/s -> {self.current_max_per_second}/s")
def _increase_rate(self):
"""提高请求速率"""
old_rate = self.current_max_per_second
# 不超过配置的最大值
self.current_max_per_second = min(
self.config.max_per_second,
int(self.current_max_per_second * 1.2)
)
self.current_max_per_minute = min(
self.config.max_per_minute,
int(self.current_max_per_minute * 1.2)
)
self.current_max_per_hour = min(
self.config.max_per_hour,
int(self.current_max_per_hour * 1.2)
)
if old_rate != self.current_max_per_second:
self.stats['rate_adjustments'] += 1
self.stats['current_rate'] = self.current_max_per_second
logger.info(f"提高请求速率: {old_rate}/s -> {self.current_max_per_second}/s")
def get_stats(self) -> Dict[str, Any]:
"""获取统计信息"""
return self.stats.copy()
def reset_stats(self):
"""重置统计信息"""
self.stats = {
'total_requests': 0,
'blocked_requests': 0,
'rate_adjustments': 0,
'current_rate': self.current_max_per_second,
'failure_rate': 0.0
}
def set_cooldown(self, seconds: int):
"""手动设置冷却时间"""
self.cooldown_until = time.time() + seconds
logger.info(f"手动设置冷却期 {seconds} 秒")
class SimpleRateLimiter:
"""简单限速器(固定速率)"""
def __init__(self, requests_per_second: float = 1.0):
"""
初始化简单限速器
Args:
requests_per_second: 每秒允许的请求数
"""
self.requests_per_second = requests_per_second
self.min_interval = 1.0 / requests_per_second
self.last_request_time = 0
self.lock = asyncio.Lock()
async def acquire(self):
"""获取请求许可"""
async with self.lock:
now = time.time()
time_since_last = now - self.last_request_time
if time_since_last < self.min_interval:
sleep_time = self.min_interval - time_since_last
await asyncio.sleep(sleep_time)
self.last_request_time = time.time()
async def __aenter__(self):
"""异步上下文管理器入口"""
await self.acquire()
return self
async def __aexit__(self, exc_type, exc_val, exc_tb):
"""异步上下文管理器出口"""
pass