TimeoutError主因是超时策略未分层、未适配、无兜底;requests需用元组timeout=(connect, read),重试须用Retry类配置指数退避,Playwright需显式处理iframe和状态检测,底层socket.timeout须单独捕获。

TimeoutError不是脚本写错了,而是你没告诉它“等多久算够”。绝大多数自动化脚本的超时问题,根源不在网络或网站,而在超时策略本身没分层、没适配、没兜底。
requests.timeout参数为什么不能只传一个数字
传timeout=10会让连接和读取共用同一个计时器——如果DNS解析慢了2秒、TCP握手卡了3秒,那留给服务器响应的时间只剩5秒。一旦后端处理稍慢(比如查库、渲染模板),就直接触发ReadTimeout,但错误日志里只显示TimeoutError,根本看不出是卡在哪儿。
- 必须用元组:
timeout=(3, 7):前值管连接(DNS+TCP),后值管读取(首字节+完整响应) - 连接超时设太短(如
0.5)会导致大量正常域名被误判为不可达;设太长(如15)会让脚本在DNS污染或防火墙拦截时死等 - 读取超时要单独调大:某些CDN会在连接建立后故意延迟首字节,此时
connect没问题,但read一直不开始
重试不能靠while循环手动写
自己写for i in range(3): try: ... except TimeoutError: time.sleep(1)容易失控:没退避、没区分错误类型、可能无限重试失败URL。
- 必须用
urllib3.util.retry.Retry,挂载到requests.Session() -
total=3控制总重试次数,status_forcelist=[429, 500, 502, 503, 504]补服务端错误,backoff_factor=1实现指数退避(第1次等1s,第2次等2s,第3次等4s) - 不显式
session.mount('https://', adapter),重试配置完全不生效
Playwright/Puppeteer类工具的超时陷阱
这类工具默认等待page.wait_for_selector()是全局超时(通常30秒),但页面状态变化未必是元素出现——可能是iframe未加载、弹窗未关闭、验证码模态框阻塞。
安全 Python 代码沙箱:在隔离的沙箱中执行任意 Python 代码,预装 requests、NumPy、pandas 库。返回 stdout、stderr。
立即学习“Python免费学习笔记(深入)”;
- 别只等一个选择器:
await page.wait_for_selector(".main-content", timeout=15000)前,先检查是否存在.yidun_modal__title或.ant-modal-confirm-title - 遇到iframe必须显式切换:
await page.frame_locator("#tinymce-1722477822617216_ifr").locator("body").fill("text"),否则wait_for_selector永远找不到 - 超时后不要直接抛异常,优先做状态判断:
if await page.query_selector(".loading-spinner"): await page.wait_for_timeout(2000)
底层socket超时容易被忽略
requests抛的是requests.exceptions.Timeout,但urllib、原生socket、数据库驱动(如psycopg2)抛的是socket.timeout——它是socket.error子类,和内置TimeoutError(OSError别名)无关。只捕获后者,会漏掉真实超时。
- 涉及
urllib.request或自建socket连接时,必须显式import socket并except socket.timeout: - 用
urllib.error.URLError包裹时,要把socket.timeout放在前面,否则会被URLError吞掉 - 全局设置
socket.setdefaulttimeout(5)影响所有socket操作,包括DNS解析,慎用
超时处理最麻烦的点不在代码怎么写,而在每个请求都要判断:这次是连不上,还是连上了但没数据,还是数据来了但解析卡住——没有银弹,只有分层检测+状态感知+退避重试。

















