From e778acbccd8614a143e520d4e32a115b16001d95 Mon Sep 17 00:00:00 2001 From: yml2213 Date: Tue, 23 Jun 2026 09:39:43 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E6=9E=81=E9=AA=8C=E9=87=8D=E8=AF=95?= =?UTF-8?q?=E5=8C=BA=E5=88=86=E4=BB=A3=E7=90=86=E6=AD=BB=E4=BA=A1=E5=92=8C?= =?UTF-8?q?=E4=B8=B4=E6=97=B6=E6=B3=A2=E5=8A=A8=EF=BC=8C=E5=87=8F=E5=B0=91?= =?UTF-8?q?=E6=97=A0=E6=95=88=E6=8D=A2=E4=BB=A3=E7=90=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 _is_proxy_connection_error 判断异常类型 - 代理连接类错误(ProxyError/超时/502):立即换代理+标记坏 - 临时波动(slide/网络不给力/KeyError):先用原代理重试2次,才换代理 - _refresh_proxy 新增 mark_bad 参数,临时换代理不标记坏 - 解决日志中'网络不给力'反复换代理导致池枯竭的问题 --- core/douyu/login.py | 84 ++++++++++++++++++++++++++++++++++++++------- 1 file changed, 72 insertions(+), 12 deletions(-) diff --git a/core/douyu/login.py b/core/douyu/login.py index 371f14a..cc0d0e6 100644 --- a/core/douyu/login.py +++ b/core/douyu/login.py @@ -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