继续优化登录逻辑
This commit is contained in:
+28
-16
@@ -66,6 +66,7 @@ class DouyuLogin:
|
|||||||
"state=https%3A%2F%2Fwww.douyu.com%2Fdirectory"
|
"state=https%3A%2F%2Fwww.douyu.com%2Fdirectory"
|
||||||
)
|
)
|
||||||
REQUEST_TIMEOUT = (10, 30)
|
REQUEST_TIMEOUT = (10, 30)
|
||||||
|
MAX_STATIC_RETRY = 3 # 静态代理失败上限(无法切换代理时的最大重试次数)
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
@@ -73,7 +74,6 @@ class DouyuLogin:
|
|||||||
proxy: Optional[str | Mapping[str, str]] = None,
|
proxy: Optional[str | Mapping[str, str]] = None,
|
||||||
proxy_api_url: Optional[str] = None,
|
proxy_api_url: Optional[str] = None,
|
||||||
timeout: tuple[float, float] = REQUEST_TIMEOUT,
|
timeout: tuple[float, float] = REQUEST_TIMEOUT,
|
||||||
max_proxy_retries: int = 0,
|
|
||||||
max_login_retries: int = 0,
|
max_login_retries: int = 0,
|
||||||
max_total_time: float = 0,
|
max_total_time: float = 0,
|
||||||
proxy_fetcher: Optional[ProxyFetcher] = None,
|
proxy_fetcher: Optional[ProxyFetcher] = None,
|
||||||
@@ -82,7 +82,6 @@ class DouyuLogin:
|
|||||||
self.account = account
|
self.account = account
|
||||||
self.proxy = proxy
|
self.proxy = proxy
|
||||||
self.timeout = timeout
|
self.timeout = timeout
|
||||||
self.max_proxy_retries = max_proxy_retries # 0=无限切换直到成功
|
|
||||||
self.max_login_retries = max_login_retries # 0=无限重试直到成功
|
self.max_login_retries = max_login_retries # 0=无限重试直到成功
|
||||||
self.max_total_time = max_total_time # 0=不限制单账号登录总时长
|
self.max_total_time = max_total_time # 0=不限制单账号登录总时长
|
||||||
self.stop_event = stop_event
|
self.stop_event = stop_event
|
||||||
@@ -94,6 +93,7 @@ class DouyuLogin:
|
|||||||
|
|
||||||
self._current_proxy_url: Optional[str] = None
|
self._current_proxy_url: Optional[str] = None
|
||||||
self._cookie_enrich_error = ""
|
self._cookie_enrich_error = ""
|
||||||
|
self._static_retry_count = 0
|
||||||
self._setup_session()
|
self._setup_session()
|
||||||
|
|
||||||
def _is_stopped(self) -> bool:
|
def _is_stopped(self) -> bool:
|
||||||
@@ -183,15 +183,14 @@ class DouyuLogin:
|
|||||||
parsed = urlsplit(url)
|
parsed = urlsplit(url)
|
||||||
return urlunsplit((parsed.scheme, parsed.netloc, parsed.path, "", ""))
|
return urlunsplit((parsed.scheme, parsed.netloc, parsed.path, "", ""))
|
||||||
|
|
||||||
def _request(self, method: str, url: str, max_retries: int = 2, **kwargs) -> requests.Response:
|
def _request(self, method: str, url: str, **kwargs) -> requests.Response:
|
||||||
"""
|
"""
|
||||||
统一发送请求,附带分段超时和更明确的错误信息。
|
统一发送请求,附带分段超时和更明确的错误信息。
|
||||||
代理连接失败时直接抛异常,由 login 整体重试换新代理。
|
代理失败/超时直接抛异常,由 login 整体重试换新代理(短效代理失效后重试同一个无意义)。
|
||||||
"""
|
"""
|
||||||
timeout = kwargs.pop('timeout', self.timeout)
|
timeout = kwargs.pop('timeout', self.timeout)
|
||||||
safe_url = self._safe_url(url)
|
safe_url = self._safe_url(url)
|
||||||
|
|
||||||
for attempt in range(max_retries):
|
|
||||||
self._ensure_not_stopped()
|
self._ensure_not_stopped()
|
||||||
started = time.monotonic()
|
started = time.monotonic()
|
||||||
|
|
||||||
@@ -210,13 +209,7 @@ class DouyuLogin:
|
|||||||
f"timeout={timeout}"
|
f"timeout={timeout}"
|
||||||
) from exc
|
) from exc
|
||||||
except requests.ConnectionError as exc:
|
except requests.ConnectionError as exc:
|
||||||
elapsed = time.monotonic() - started
|
|
||||||
err_str = str(exc)
|
err_str = str(exc)
|
||||||
is_proxy_err = "proxy" in err_str.lower() or "Proxy" in type(exc).__name__
|
|
||||||
if is_proxy_err and attempt < max_retries - 1:
|
|
||||||
logger.warning(f"代理连接失败,尝试 {attempt + 1}/{max_retries}: {self._truncate_error(err_str)}")
|
|
||||||
self._sleep_interruptible(1)
|
|
||||||
continue
|
|
||||||
raise ConnectionError(
|
raise ConnectionError(
|
||||||
f"{method.upper()} {safe_url} 连接失败: {self._truncate_error(err_str)}"
|
f"{method.upper()} {safe_url} 连接失败: {self._truncate_error(err_str)}"
|
||||||
) from exc
|
) from exc
|
||||||
@@ -226,8 +219,6 @@ class DouyuLogin:
|
|||||||
f"{method.upper()} {safe_url} 请求失败,耗时 {elapsed:.1f}s: {exc}"
|
f"{method.upper()} {safe_url} 请求失败,耗时 {elapsed:.1f}s: {exc}"
|
||||||
) from exc
|
) from exc
|
||||||
|
|
||||||
raise ConnectionError(f"{method.upper()} {safe_url} 连接失败,已重试 {max_retries} 次")
|
|
||||||
|
|
||||||
def _request_json(self, method: str, url: str, source: str, **kwargs) -> dict:
|
def _request_json(self, method: str, url: str, source: str, **kwargs) -> dict:
|
||||||
"""请求 JSON 接口,并在响应异常时输出可定位的信息。"""
|
"""请求 JSON 接口,并在响应异常时输出可定位的信息。"""
|
||||||
response = self._request(method, url, **kwargs)
|
response = self._request(method, url, **kwargs)
|
||||||
@@ -274,7 +265,11 @@ class DouyuLogin:
|
|||||||
else:
|
else:
|
||||||
logger.info(f"登录整体重试 {attempt} (无限重试),换新代理从头开始")
|
logger.info(f"登录整体重试 {attempt} (无限重试),换新代理从头开始")
|
||||||
# 重试前:取新代理 + 重置 session
|
# 重试前:取新代理 + 重置 session
|
||||||
self._prepare_retry()
|
if not self._prepare_retry():
|
||||||
|
return LoginResult(
|
||||||
|
success=False,
|
||||||
|
message=f"静态代理连续失败 {self.MAX_STATIC_RETRY} 次,无法切换代理",
|
||||||
|
)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# 1️⃣ 第一次登录(获取极验参数)
|
# 1️⃣ 第一次登录(获取极验参数)
|
||||||
@@ -330,14 +325,31 @@ class DouyuLogin:
|
|||||||
# 所有重试耗尽或超时
|
# 所有重试耗尽或超时
|
||||||
return LoginResult(success=False, message=str(e))
|
return LoginResult(success=False, message=str(e))
|
||||||
|
|
||||||
def _prepare_retry(self) -> None:
|
def _prepare_retry(self) -> bool:
|
||||||
"""重试前准备:取新代理、重置 session cookies。"""
|
"""重试前准备:取新代理、重置 session cookies。
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
True - 可以继续重试
|
||||||
|
False - 静态代理已连续失败 MAX_STATIC_RETRY 次,无法切换代理,应放弃
|
||||||
|
"""
|
||||||
self.session.cookies.clear()
|
self.session.cookies.clear()
|
||||||
if self.proxy_fetcher:
|
if self.proxy_fetcher:
|
||||||
new_proxy = self.proxy_fetcher.fetch_new_proxy()
|
new_proxy = self.proxy_fetcher.fetch_new_proxy()
|
||||||
if new_proxy:
|
if new_proxy:
|
||||||
self._apply_proxy(new_proxy)
|
self._apply_proxy(new_proxy)
|
||||||
logger.info(f"重试换新代理: {new_proxy}")
|
logger.info(f"重试换新代理: {new_proxy}")
|
||||||
|
return True
|
||||||
|
if self.proxy:
|
||||||
|
self._static_retry_count += 1
|
||||||
|
if self._static_retry_count > self.MAX_STATIC_RETRY:
|
||||||
|
logger.error(
|
||||||
|
f"静态代理连续失败 {self.MAX_STATIC_RETRY} 次,无法切换代理"
|
||||||
|
)
|
||||||
|
return False
|
||||||
|
logger.warning(
|
||||||
|
f"静态代理失败重试 {self._static_retry_count}/{self.MAX_STATIC_RETRY}"
|
||||||
|
)
|
||||||
|
return True
|
||||||
|
|
||||||
def _first_login(self) -> Tuple[str, str, str, dict]:
|
def _first_login(self) -> Tuple[str, str, str, dict]:
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -8,6 +8,8 @@
|
|||||||
时由 ProxyResolver 内部被动同步兜底。
|
时由 ProxyResolver 内部被动同步兜底。
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
import threading
|
||||||
|
import time
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
from loguru import logger
|
from loguru import logger
|
||||||
@@ -19,13 +21,18 @@ from .proxy_whitelist import DouyuWhitelistSyncer
|
|||||||
class ProxyFetcher:
|
class ProxyFetcher:
|
||||||
"""每次从代理 API 取 1 个新代理,无缓存无复用。"""
|
"""每次从代理 API 取 1 个新代理,无缓存无复用。"""
|
||||||
|
|
||||||
|
# 代理 API 最小调用间隔(秒)——防止高并发 + 多轮重试把代理 API 打爆
|
||||||
|
MIN_FETCH_INTERVAL = 1.5
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
api_url: str,
|
api_url: str,
|
||||||
whitelist_platform: str = "xiequ",
|
whitelist_platform: str = "xiequ",
|
||||||
whitelist_credentials: dict = None,
|
whitelist_credentials: dict = None,
|
||||||
|
stop_event: Optional[threading.Event] = None,
|
||||||
):
|
):
|
||||||
self.api_url = api_url
|
self.api_url = api_url
|
||||||
|
self.stop_event = stop_event
|
||||||
|
|
||||||
self._whitelist_syncer = (
|
self._whitelist_syncer = (
|
||||||
DouyuWhitelistSyncer(
|
DouyuWhitelistSyncer(
|
||||||
@@ -36,6 +43,10 @@ class ProxyFetcher:
|
|||||||
else None
|
else None
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# 节流:跨线程共享的"上次取代理时刻"
|
||||||
|
self._last_fetch_at = 0.0
|
||||||
|
self._fetch_lock = threading.Lock()
|
||||||
|
|
||||||
def warmup_whitelist(self) -> tuple[bool, str]:
|
def warmup_whitelist(self) -> tuple[bool, str]:
|
||||||
"""批次启动前调用一次:把当前本地出口 IP 同步到白名单。
|
"""批次启动前调用一次:把当前本地出口 IP 同步到白名单。
|
||||||
|
|
||||||
@@ -60,12 +71,31 @@ class ProxyFetcher:
|
|||||||
|
|
||||||
不主动检测/同步本地出口 IP;如果代理 API 返回"白名单错误",
|
不主动检测/同步本地出口 IP;如果代理 API 返回"白名单错误",
|
||||||
由 ProxyResolver 内部被动同步兜底。
|
由 ProxyResolver 内部被动同步兜底。
|
||||||
|
|
||||||
|
进程内节流:高并发场景下任意两次调用至少间隔 MIN_FETCH_INTERVAL 秒,
|
||||||
|
避免代理 API 被限频。
|
||||||
"""
|
"""
|
||||||
|
with self._fetch_lock:
|
||||||
|
elapsed = time.monotonic() - self._last_fetch_at
|
||||||
|
if elapsed < self.MIN_FETCH_INTERVAL:
|
||||||
|
wait = self.MIN_FETCH_INTERVAL - elapsed
|
||||||
|
logger.debug(f"代理 API 节流等待 {wait:.2f}s")
|
||||||
|
if self.stop_event:
|
||||||
|
if self.stop_event.wait(wait):
|
||||||
|
return None
|
||||||
|
else:
|
||||||
|
time.sleep(wait)
|
||||||
|
self._last_fetch_at = time.monotonic()
|
||||||
|
|
||||||
|
if self.stop_event and self.stop_event.is_set():
|
||||||
|
return None
|
||||||
|
|
||||||
resolver = ProxyResolver(
|
resolver = ProxyResolver(
|
||||||
api_url=self.api_url,
|
api_url=self.api_url,
|
||||||
whitelist_syncer=self._whitelist_syncer,
|
whitelist_syncer=self._whitelist_syncer,
|
||||||
sync_local_exit_ip=False,
|
sync_local_exit_ip=False,
|
||||||
sync_whitelist_once=False,
|
sync_whitelist_once=False,
|
||||||
|
stop_event=self.stop_event,
|
||||||
)
|
)
|
||||||
result, msg = resolver.fetch_verified(
|
result, msg = resolver.fetch_verified(
|
||||||
max_attempts=max_attempts,
|
max_attempts=max_attempts,
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
"""代理获取、白名单同步、可用性验证的统一流程。"""
|
"""代理获取、白名单同步、可用性验证的统一流程。"""
|
||||||
|
|
||||||
|
import threading
|
||||||
import time
|
import time
|
||||||
from typing import Callable, Optional, Protocol
|
from typing import Callable, Optional, Protocol
|
||||||
|
|
||||||
@@ -33,15 +34,28 @@ class ProxyResolver:
|
|||||||
log_func: Optional[LogFunc] = None,
|
log_func: Optional[LogFunc] = None,
|
||||||
sync_local_exit_ip: bool = False,
|
sync_local_exit_ip: bool = False,
|
||||||
sync_whitelist_once: bool = True,
|
sync_whitelist_once: bool = True,
|
||||||
|
stop_event: Optional[threading.Event] = None,
|
||||||
):
|
):
|
||||||
self.api_url = api_url
|
self.api_url = api_url
|
||||||
self.whitelist_syncer = whitelist_syncer
|
self.whitelist_syncer = whitelist_syncer
|
||||||
self.log_func = log_func
|
self.log_func = log_func
|
||||||
self.sync_local_exit_ip = sync_local_exit_ip
|
self.sync_local_exit_ip = sync_local_exit_ip
|
||||||
self.sync_whitelist_once = sync_whitelist_once
|
self.sync_whitelist_once = sync_whitelist_once
|
||||||
|
self.stop_event = stop_event
|
||||||
self._last_synced_ip: Optional[str] = None
|
self._last_synced_ip: Optional[str] = None
|
||||||
self._has_synced_whitelist = False
|
self._has_synced_whitelist = False
|
||||||
|
|
||||||
|
def _is_stopped(self) -> bool:
|
||||||
|
return bool(self.stop_event and self.stop_event.is_set())
|
||||||
|
|
||||||
|
def _wait_or_stopped(self, seconds: float) -> bool:
|
||||||
|
"""等待 seconds 秒,期间收到停止信号返回 True。"""
|
||||||
|
if self.stop_event:
|
||||||
|
return self.stop_event.wait(seconds)
|
||||||
|
if seconds > 0:
|
||||||
|
time.sleep(seconds)
|
||||||
|
return False
|
||||||
|
|
||||||
def _log(self, level: str, message: str) -> None:
|
def _log(self, level: str, message: str) -> None:
|
||||||
if self.log_func:
|
if self.log_func:
|
||||||
self.log_func(level, message)
|
self.log_func(level, message)
|
||||||
@@ -92,13 +106,18 @@ class ProxyResolver:
|
|||||||
last_error = ""
|
last_error = ""
|
||||||
|
|
||||||
for attempt in range(1, max_attempts + 1):
|
for attempt in range(1, max_attempts + 1):
|
||||||
|
if self._is_stopped():
|
||||||
|
return None, '任务已停止'
|
||||||
if attempt > 1:
|
if attempt > 1:
|
||||||
delay = min(attempt - 1, 2)
|
delay = min(attempt - 1, 2)
|
||||||
if self.log_func:
|
if self.log_func:
|
||||||
self._log('info', f'等待 {delay}s 后重试...')
|
self._log('info', f'等待 {delay}s 后重试...')
|
||||||
time.sleep(delay)
|
if self._wait_or_stopped(delay):
|
||||||
|
return None, '任务已停止'
|
||||||
|
|
||||||
self._sync_local_exit_ip_if_needed(attempt)
|
self._sync_local_exit_ip_if_needed(attempt)
|
||||||
|
if self._is_stopped():
|
||||||
|
return None, '任务已停止'
|
||||||
self._log('info', f'代理预检 {attempt}/{max_attempts}: 正在获取代理')
|
self._log('info', f'代理预检 {attempt}/{max_attempts}: 正在获取代理')
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
|||||||
@@ -54,7 +54,6 @@ async def create_batch(
|
|||||||
account_ids=valid_ids,
|
account_ids=valid_ids,
|
||||||
created_by=current.id,
|
created_by=current.id,
|
||||||
creator_permissions=get_user_permissions(current),
|
creator_permissions=get_user_permissions(current),
|
||||||
max_proxy_retries=req.max_proxy_retries,
|
|
||||||
max_login_retries=req.max_login_retries,
|
max_login_retries=req.max_login_retries,
|
||||||
max_total_time=req.max_total_time,
|
max_total_time=req.max_total_time,
|
||||||
proxy_config=proxy,
|
proxy_config=proxy,
|
||||||
|
|||||||
@@ -124,7 +124,6 @@ class AccountOut(BaseModel):
|
|||||||
# ---- 登录任务 ----
|
# ---- 登录任务 ----
|
||||||
class LoginBatchRequest(BaseModel):
|
class LoginBatchRequest(BaseModel):
|
||||||
account_ids: list[int]
|
account_ids: list[int]
|
||||||
max_proxy_retries: int = 0 # 代理切换次数,0=无限切换直到成功
|
|
||||||
max_login_retries: int = 0 # 登录整体重试次数,0=无限重试直到成功
|
max_login_retries: int = 0 # 登录整体重试次数,0=无限重试直到成功
|
||||||
max_total_time: float = 0 # 单账号登录总时长上限,0=不限制
|
max_total_time: float = 0 # 单账号登录总时长上限,0=不限制
|
||||||
concurrency: int = 3 # 并发数,1-10
|
concurrency: int = 3 # 并发数,1-10
|
||||||
|
|||||||
@@ -25,7 +25,6 @@ class LoginBatchRunner:
|
|||||||
account_ids: list[int],
|
account_ids: list[int],
|
||||||
created_by: int,
|
created_by: int,
|
||||||
creator_permissions: list[str],
|
creator_permissions: list[str],
|
||||||
max_proxy_retries: int = 0,
|
|
||||||
max_login_retries: int = 0,
|
max_login_retries: int = 0,
|
||||||
max_total_time: float = 0,
|
max_total_time: float = 0,
|
||||||
proxy_config: Optional[ProxyConfigModel] = None,
|
proxy_config: Optional[ProxyConfigModel] = None,
|
||||||
@@ -37,7 +36,6 @@ class LoginBatchRunner:
|
|||||||
self.account_ids = account_ids
|
self.account_ids = account_ids
|
||||||
self.created_by = created_by
|
self.created_by = created_by
|
||||||
self.creator_permissions = creator_permissions
|
self.creator_permissions = creator_permissions
|
||||||
self.max_proxy_retries = max_proxy_retries
|
|
||||||
# 限制最大重试次数,避免无限重试导致资源耗尽
|
# 限制最大重试次数,避免无限重试导致资源耗尽
|
||||||
# 0 表示使用默认值 20,其他值保持原样
|
# 0 表示使用默认值 20,其他值保持原样
|
||||||
self.max_login_retries = max_login_retries if max_login_retries > 0 else 20
|
self.max_login_retries = max_login_retries if max_login_retries > 0 else 20
|
||||||
@@ -68,6 +66,7 @@ class LoginBatchRunner:
|
|||||||
api_url=proxy_config.api_url,
|
api_url=proxy_config.api_url,
|
||||||
whitelist_platform=wl_platform,
|
whitelist_platform=wl_platform,
|
||||||
whitelist_credentials=wl_credentials,
|
whitelist_credentials=wl_credentials,
|
||||||
|
stop_event=self._stop,
|
||||||
)
|
)
|
||||||
|
|
||||||
def stop(self):
|
def stop(self):
|
||||||
@@ -151,7 +150,6 @@ class LoginBatchRunner:
|
|||||||
loginer = DouyuLogin(
|
loginer = DouyuLogin(
|
||||||
account,
|
account,
|
||||||
proxy=proxy_dict,
|
proxy=proxy_dict,
|
||||||
max_proxy_retries=self.max_proxy_retries,
|
|
||||||
max_login_retries=self.max_login_retries,
|
max_login_retries=self.max_login_retries,
|
||||||
max_total_time=self.max_total_time,
|
max_total_time=self.max_total_time,
|
||||||
proxy_fetcher=self._shared_proxy_fetcher,
|
proxy_fetcher=self._shared_proxy_fetcher,
|
||||||
|
|||||||
Reference in New Issue
Block a user