ThreadPoolExecutor 是 Python 多线程爬虫最稳易用方案:自动管理生命周期、异常传播和资源回收,支持 Future 统一超时重试与结果聚合,线程隔离 Session 避免状态污染,配合 tenacity 实现精准指数退避重试。

用 concurrent.futures.ThreadPoolExecutor 实现多线程并发抓取,是 Python 中最稳、最容易上手的方案——它比裸写 threading 安全,比 aiohttp 兼容性更好,适合大多数中等规模爬取任务(比如 10–200 个 URL,需登录态或带 Cookie 的场景)。
为什么选 ThreadPoolExecutor 而不是手动 threading?
手动管理 threading.Thread + Queue 容易漏掉 task_done() 或忘记 join(),导致主线程提前退出或卡死;而 ThreadPoolExecutor 自动处理生命周期、异常传播和资源回收。更重要的是:executor.map() 和 executor.submit() 返回的是 Future 对象,可统一做超时、重试、结果聚合,不用自己加锁维护共享列表。
- 每个线程默认独占一个
requests.Session实例(避免线程间 session 状态污染) - 异常不会崩掉整个池子:单个请求失败,其他任务照常执行
- 返回结果顺序与输入一致(
map模式),省去手动对齐索引的麻烦
max_workers 设多少才不翻车?
这不是越大越好。线程数过多会引发 DNS 解析阻塞、端口耗尽、目标站限流,甚至触发反爬(比如连续 50 个请求在 1 秒内抵达)。经验法则是:
- 纯 HTTP 请求(无解析/计算):设为
10–32,多数情况下16是安全起点 - 含 HTML 解析(如
BeautifulSoup):降到8–16,CPU 开销会上升 - 目标站明确限制频率(如 robots.txt 写着
Crawl-delay: 2):直接按每秒 0.5 个请求反推,max_workers=2起步,再加随机 sleep - 本地网络带宽不足(比如跑在 10Mbps 家宽下):超过 20 个并发可能让 DNS 查询排队,反而更慢
如何避免线程间状态干扰和资源冲突?
常见错误是把 Session、计数器、文件句柄这些全局对象直接扔进多线程里共用。正确做法是:
立即学习“Python免费学习笔记(深入)”;
- 每个线程内新建
requests.Session(),或用threading.local()绑定 session 实例 - 写文件别用
open(..., 'a')多线程追加——改用queue.Queue缓存结果,最后单线程批量落盘 - 统计成功/失败数?别用全局
int变量,改用threading.Lock包裹+=操作,或直接靠Future.result()收集结构化结果 - 若需复用登录态(如带
Set-Cookie),必须确保 session 创建和请求都在同一线程内完成,不能跨线程传递 session 对象
配合 tenacity 做可靠重试,比手写 try/except 强在哪?
手写重试容易陷入无限循环(比如 404 永远不重试,503 却狂重试),也难控制退避策略。tenacity 提供声明式重试逻辑:
from tenacity import retry, stop_after_attempt, wait_exponential, retry_if_exception_type
@retry(
stop=stop_after_attempt(3),
wait=wait_exponential(multiplier=1, min=1, max=10),
retry=retry_if_exception_type((requests.exceptions.Timeout, requests.exceptions.ConnectionError))
)
def fetch_with_retry(url):
return requests.get(url, timeout=5)
关键点:
- 只对特定异常重试(如超时、连接断开),跳过 4xx 错误(说明请求本身有问题)
-
wait_exponential实现指数退避,避免雪崩式重试 - 装饰器可直接套在
fetch_url上,ThreadPoolExecutor调用时自动生效
真正容易被忽略的,是 DNS 缓存和 TCP 连接复用——默认 requests 每次都新建连接,高并发下握手开销巨大。务必在每个线程的 Session 里启用连接池:session = requests.Session(); adapter = requests.adapters.HTTPAdapter(pool_connections=10, pool_maxsize=20),否则线程数再多也白搭。


















