非常游戏网
如何解决Python爬虫在长连接请求中超时的问题_设置合理的timeout参数与重试机制

如何解决Python爬虫在长连接请求中超时的问题_设置合理的timeout参数与重试机制

2026-05-29日常编程89341

requests.get()默认不设超时会无限等待,必须用timeout=(connect, read)元组分别控制连接与读取超时,单数值仅作用于读取阶段,连接阶段仍可能卡死。

requests.get() 默认不设超时,长连接请求卡住几小时都可能不报错——必须显式设置 timeout,且不能只填一个数字。

为什么单个 timeout 数值对长连接无效

requests 的 timeout 参数若只传一个数字(如 timeout=10),它只控制读取超时(read timeout),而连接阶段(connect)仍可能无限等待。长连接场景下,DNS 解析慢、中间网关阻塞、目标服务器 TCP backlog 满等,都会卡在连接建立环节,此时 timeout=10 完全不起作用。

常见错误现象:requests.exceptions.ConnectTimeout 报错但程序已卡死数分钟;或日志里没异常,但爬虫进程 CPU 为 0、内存不动、无响应。

  • 必须用元组形式: timeout=(connect_timeout, read_timeout)
  • 连接超时建议设为 3–5 秒:足够应对正常 DNS 和握手,又不至于久等
  • 读取超时需按业务定:静态页可设 7–15 秒;含动态渲染或大文件下载的,可放宽到 30 秒以上,但要配合重试
  • 避免设成 timeout=Nonetimeout=0:前者等于不设限,后者会直接抛 ValueError

requests 中如何正确捕获 ConnectTimeout 和 ReadTimeout

这两个异常虽然都属于超时,但触发阶段和修复方向完全不同,混用 except 会掩盖问题根源。

Python 3.14.3

微软官方的 Python 扩展,是 VS Code 安装量最高的扩展(209M+)。集成 IntelliSense(通过 Pylance)、调试(通过 Python Debugger)、代码检查、格式化、重构和单元测试等功能。支持 Jupyter Notebook、虚拟环境管理和多 Python 版本切换。

  • requests.exceptions.ConnectTimeout:说明根本连不上,应优先检查 DNS、代理可用性、防火墙或目标域名是否被屏蔽
  • requests.exceptions.ReadTimeout:连接已建好,但服务端迟迟不发数据,可能是后端处理慢、限流、或页面含大量 JS 渲染逻辑
  • 不要只写一个 except requests.exceptions.Timeout:——它无法区分是连接失败还是读取卡住
  • 推荐写法:
    try:
        resp = requests.get(url, timeout=(3, 15))
    except requests.exceptions.ConnectTimeout:
        # 记录 host + port,换 DNS 或跳过该域名
        log_error(f"Connect failed: {url}")
    except requests.exceptions.ReadTimeout:
        # 可重试,或降级用 head 请求探活
        retry_with_backoff(url)
    except requests.exceptions.RequestException as e:
        # 兜底,但不包含超时类异常
        handle_other_error(e)

重试机制不能只靠 time.sleep,要带退避和状态判断

简单 for 循环 + time.sleep(1) 重试,在高并发或目标限流时反而加剧问题:重复请求压垮自己,或触发 IP 封禁。

  • urllib3.util.retry.Retry 配合 requests.adapters.HTTPAdapter 更可靠
  • 关键参数:total=3(总重试次数)、backoff_factor=1(退避因子,第二次重试延迟 2 秒,第三次 4 秒)
  • 仅对特定状态码重试:status_forcelist=[429, 502, 503, 504],504 是典型网关超时,值得重试;但 404 或 403 就不该重试
  • 注意:ConnectTimeout 和 ReadTimeout 默认不进重试流程,需手动开启:connect=3, read=3
  • 示例初始化 session:
    s = requests.Session()
    retry_strategy = Retry(
        total=3,
        connect=3,
        read=3,
        backoff_factor=1,
        status_forcelist=[429, 502, 503, 504],
    )
    adapter = HTTPAdapter(max_retries=retry_strategy)
    s.mount("http://", adapter)
    s.mount("https://", adapter)

长连接场景下 timeout 和重试的组合陷阱

很多人把 timeout=(3, 30) 和重试 3 次一起用,以为最多耗时 3×33≈100 秒,实际可能远超——因为每次重试都会重新走完整 timeout 流程,且底层连接池可能复用失效连接。

  • 连接超时(3 秒)失败后,重试前不会等满 3 秒才放弃,但读取超时(30 秒)一旦触发,整个请求周期就至少占掉 30 秒
  • 如果目标服务器返回了连接但一直不发包(如 HTTP/2 流控挂起),read_timeout 才是真正瓶颈
  • 真实长连接问题常藏在底层:requests 默认启用连接池,但若服务器主动断连或 keep-alive 超时,复用旧连接会导致后续请求莫名卡住
  • 解决方案之一:加 headers={'Connection': 'close'} 强制不复用连接,牺牲一点性能换确定性
  • 更稳妥做法:监控单次请求耗时,超过阈值(如 25 秒)就主动 resp.close() 并销毁 session,再建新 session

超时不是调大数字就能解决的事;连接阶段和读取阶段得分开看,重试不是循环 sleep,长连接的“连接”二字本身就藏着最容易被忽略的状态管理成本。