应优先使用ThreadPoolExecutor而非裸threading.Thread,因其内置任务队列、异常传播、自动回收机制,可有效控制并发数(建议5–10)、避免资源耗尽与伪串行问题。

threading 模块确实能加速简单网页抓取,但直接用 Thread 类手动管理线程容易失控——不是卡死就是并发数爆表,反而拖慢整体速度。
为什么不用 raw threading.Thread 直接循环启线程
新手常写这种代码:t = threading.Thread(target=fetch, args=(url,)); t.start(),然后对每个 URL 都这么干。问题在于:
- 没限制并发数,100 个 URL 就开 100 个线程,系统资源(文件描述符、内存)很快耗尽,
requests可能抛OSError: [Errno 24] Too many open files - 没做异常隔离,一个线程崩溃(比如 DNS 失败)不会影响其他线程,但也没法统一捕获或重试
- 没等所有线程结束就退出主程序,部分请求根本没发出去
-
join()放错位置(比如在循环里每启一个就join),结果变成伪串行
用 ThreadPoolExecutor 控制并发更稳
它封装了线程创建、复用、回收和错误传播,比裸 Thread 更适合爬虫场景。关键参数只有 max_workers,建议设为 5–10(不是 CPU 核数,而是根据目标网站响应延迟和自身带宽调):
- 目标站响应快(
- 目标站有反爬、响应慢或本地网络一般 → 建议 3–6,避免被限流或超时堆积
- 超过 20 很少带来收益,反而增加调度开销和失败率
示例:
from concurrent.futures import ThreadPoolExecutor
import requests
def fetch(url):
try:
resp = requests.get(url, timeout=8)
return url, resp.status_code, len(resp.content)
except Exception as e:
return url, "ERROR", str(e)
urls = ["https://httpbin.org/delay/1", "https://httpbin.org/delay/2"]
with ThreadPoolExecutor(max_workers=5) as executor:
results = list(executor.map(fetch, urls))
queue.Queue 适合需要动态加任务的场景
当 URL 列表不固定(比如解析 A 页面后才得到 B、C 的链接),或者要控制消费节奏(如每秒最多 2 个请求),queue.Queue 比 ThreadPoolExecutor 更灵活:
图片提示词生成器?不止如此。 马甲系统 —— 把脑海中的画面,翻译成AI能理解的专业表达。 用得越多,它越懂你:首次需要多问几句确认方向,用久了几乎一说就懂。 用得越多,它越快:缓存机制让后续对话越来越省。 RAG进化:成功案例持续入库,越跑越聪明。 输入「新手指南」查看完整功能介绍
立即学习“Python免费学习笔记(深入)”;
- 主线程往队列塞 URL,工作线程从队列取;队列满时自动阻塞生产者,天然限流
- 配合
queue.task_done()和queue.join()能精确等待全部任务完成 - 注意:别在工作线程里直接
print或写文件——多线程并发写同一文件会乱序或损坏,应先收集结果再统一处理
真正影响速度的往往不是线程数,而是超时与重试
很多“加速失败”的案例,根源是没设合理的 timeout 和重试逻辑:
-
requests.get(url, timeout=5)是必须的,否则一个挂掉的 URL 会让整个线程卡死 60 秒以上 - 单次失败不等于永久失败,加一层简单重试(比如
for _ in range(3))比盲目增线程更有效 - 别忽略
ConnectionError和Timeout的区别:前者可能需换代理,后者优先调大 timeout
线程本身只是调度器,底层还是靠 requests 发请求——把请求层调稳了,线程池才能真正跑起来。

















