fix: 极验重试区分代理死亡和临时波动,减少无效换代理

- 新增 _is_proxy_connection_error 判断异常类型
- 代理连接类错误(ProxyError/超时/502):立即换代理+标记坏
- 临时波动(slide/网络不给力/KeyError):先用原代理重试2次,才换代理
- _refresh_proxy 新增 mark_bad 参数,临时换代理不标记坏
- 解决日志中'网络不给力'反复换代理导致池枯竭的问题
This commit is contained in:
yml2213
2026-06-23 09:39:43 +08:00
parent ab4b5e7f08
commit e778acbccd
+72 -12
View File
@@ -151,21 +151,55 @@ class DouyuLogin:
}
self._current_proxy_url = new_proxy
def _refresh_proxy(self) -> Optional[str]:
"""刷新代理IP:先标记当前代理为坏(移出代理池),再获取新代理"""
def _refresh_proxy(self, mark_bad: bool = True) -> Optional[str]:
"""
刷新代理IP。
Args:
mark_bad: 是否标记当前代理为坏。默认True(代理确实不可用时)。
设为False时仅换代理,不标记坏(临时网络波动,代理本身可能没问题)。
"""
if not self.proxy_manager:
return None
# 标记当前代理为不可用,避免再被复用
if self._current_proxy_url:
if mark_bad and self._current_proxy_url:
self.proxy_manager.mark_bad(self._current_proxy_url)
new_proxy = self.proxy_manager.get_proxy()
if new_proxy:
self._apply_proxy(new_proxy)
logger.info(f"已切换代理: {new_proxy}")
logger.info(f"已切换代理: {new_proxy}{' (旧代理已标记坏)' if mark_bad else ' (旧代理保留)'}")
return new_proxy
@staticmethod
def _is_proxy_connection_error(err_str: str) -> bool:
"""判断异常是否为代理连接类错误(代理已死),而非临时网络波动。"""
err_lower = err_str.lower()
# 代理连接/超时类:代理本身不可达
proxy_dead_keywords = [
'proxyerror',
'tunnel connection failed',
'connecttimeouterror',
'connection refused',
'unable to connect to proxy',
'proxy connection',
'502 bad gateway',
'503 service unavailable',
]
if any(kw in err_lower for kw in proxy_dead_keywords):
return True
# SSL/证书错误也视为代理问题
if 'ssl' in err_lower and 'proxy' in err_lower:
return True
return False
@staticmethod
def _truncate_error(err_str: str, max_len: int = 80) -> str:
"""截断错误信息,避免日志刷屏。"""
if len(err_str) <= max_len:
return err_str
return err_str[:max_len] + "..."
def _safe_url(self, url: str) -> str:
"""隐藏查询参数,避免日志泄露登录回调 code。"""
parsed = urlsplit(url)
@@ -385,6 +419,10 @@ class DouyuLogin:
"""
解决极验 fullpage 验证(带重试机制)
区分两类错误:
- 临时波动(slide、网络不给力、KeyError):先用原代理重试,连续失败才换
- 代理死亡(ProxyError、连接超时):立即换代理
Args:
gt: 极验gt参数
challenge: 极验challenge参数(第一次登录返回的)
@@ -397,6 +435,10 @@ class DouyuLogin:
# max_proxy_retries=0 表示无限重试直到成功
max_attempts = self.max_proxy_retries if self.max_proxy_retries > 0 else 999999
# 连续临时失败计数(同一代理下),超过阈值才换代理
_soft_fail_streak = 0
_SOFT_FAIL_THRESHOLD = 2 # 同一代理连续临时失败2次才换
for attempt in range(max_attempts):
try:
if self.max_proxy_retries > 0:
@@ -435,9 +477,14 @@ class DouyuLogin:
logger.success(f"极验 fullpage 验证成功! validate={validate[:20]}...")
return validate, seccode
else:
logger.warning(f"极验验证失败: {message},重试中...")
# 刷新代理
self._refresh_proxy()
# 极验返回失败(slide 等)—— 通常是临时问题,先原代理重试
_soft_fail_streak += 1
if _soft_fail_streak >= _SOFT_FAIL_THRESHOLD:
logger.warning(f"极验验证失败: {message},同一代理连续 {_soft_fail_streak} 次,换代理")
self._refresh_proxy(mark_bad=False)
_soft_fail_streak = 0
else:
logger.warning(f"极验验证失败: {message},原代理重试 ({_soft_fail_streak}/{_SOFT_FAIL_THRESHOLD})")
time.sleep(1)
continue
else:
@@ -448,10 +495,23 @@ class DouyuLogin:
return validate, seccode
except Exception as e:
logger.warning(f"极验验证异常: {e},重试中...")
# 刷新代理
self._refresh_proxy()
# 增加等待时间,避免请求过于频繁
err_str = str(e)
is_proxy_dead = self._is_proxy_connection_error(err_str)
if is_proxy_dead:
# 代理确实不可用:立即换,标记坏
logger.warning(f"极验验证代理连接失败: {_truncate_error(err_str)},换代理")
self._refresh_proxy(mark_bad=True)
_soft_fail_streak = 0
else:
# 临时异常(KeyError、网络不给力等):先原代理重试
_soft_fail_streak += 1
if _soft_fail_streak >= _SOFT_FAIL_THRESHOLD:
logger.warning(f"极验验证临时异常: {_truncate_error(err_str)},连续 {_soft_fail_streak} 次,换代理")
self._refresh_proxy(mark_bad=False)
_soft_fail_streak = 0
else:
logger.warning(f"极验验证临时异常: {_truncate_error(err_str)},原代理重试 ({_soft_fail_streak}/{_SOFT_FAIL_THRESHOLD})")
time.sleep(2)
continue