Python爬虫并发方案全解析:多线程、多进程与异步协程实战指南

引言

在爬虫开发中,性能瓶颈往往不在于代码语法,而在于对网络I/O、并发模型和资源调度的底层理解。当我们需要批量抓取网页数据时,串行请求的效率显然无法满足需求。Python提供了三种主流的并发方案:多线程(threading)多进程(multiprocessing)异步协程(asyncio + aiohttp)。本文将深入剖析这三种方案的适用场景、使用方法及优缺点,帮助你根据实际需求做出最佳选择。


一、多线程(threading / ThreadPoolExecutor)

1. 适用场景

多线程适合I/O密集型任务,如网络请求、文件读写等。由于Python的GIL(全局解释器锁)限制,同一时刻只有一个线程执行Python字节码,因此多线程无法利用多核CPU提升计算性能。但对于I/O密集型任务,线程可以在阻塞时让出GIL,实现并发效果。

典型应用:小批量网页爬取、同时处理多个客户端请求、日志记录等轻量级并发场景。

2. 使用方法

方式一:threading.Thread 直接创建
import threading
import requests
import time

def fetch_url(url):
    try:
        resp = requests.get(url, timeout=5)
        print(f"成功爬取: {url} 状态码: {resp.status_code}")
    except Exception as e:
        print(f"失败: {url} 错误: {e}")

urls = [
    "https://www.baidu.com",
    "https://www.qq.com",
    "https://www.sina.com.cn"
]

threads = []
for url in urls:
    t = threading.Thread(target=fetch_url, args=(url,))
    threads.append(t)
    t.start()

for t in threads:
    t.join()
方式二:Queue + 多线程(任务分发)

使用Queue队列做任务分发,线程安全,适合入门爬虫:

import threading
import queue
import requests

url_list = ["https://www.baidu.com", "https://www.qq.com", "https://www.sina.com.cn"]
q = queue.Queue()
for url in url_list:
    q.put(url)

def spider():
    while not q.empty():
        url = q.get()
        try:
            resp = requests.get(url, timeout=5)
            print(f"成功爬取: {url} 状态码: {resp.status_code}")
        except Exception as e:
            print(f"失败: {url} 错误: {e}")
        finally:
            q.task_done()

thread_num = 3
for _ in range(thread_num):
    t = threading.Thread(target=spider)
    t.daemon = True
    t.start()

q.join()
方式三:ThreadPoolExecutor 线程池

使用concurrent.futures.ThreadPoolExecutor简化线程管理:

from concurrent.futures import ThreadPoolExecutor
import requests

def fetch(url):
    resp = requests.get(url, timeout=5)
    return resp.status_code

urls = ["https://www.baidu.com", "https://www.qq.com", "https://www.sina.com.cn"]

with ThreadPoolExecutor(max_workers=3) as executor:
    futures = [executor.submit(fetch, url) for url in urls]
    for f in futures:
        print(f.result())

3. 优缺点

优点

  • 实现简单,易于理解,适合入门
  • 共享内存空间,数据传递方便
  • 轻量级调度,I/O密集场景高效

缺点

  • 受GIL限制,无法真正并行执行CPU密集型任务
  • 线程切换开销较大,实际有效并行度有限
  • 需要处理线程安全问题(如加锁)

二、多进程(multiprocessing / ProcessPoolExecutor)

1. 适用场景

多进程适合CPU密集型任务,如HTML解析、数据清洗、图像处理等。每个进程拥有独立的内存空间和Python解释器,可以绕过GIL限制,充分利用多核CPU。

典型应用:大规模数据解析、分布式爬虫、需要跨CPU核心并行计算的任务。

2. 使用方法

方式一:multiprocessing.Process
import multiprocessing

def cpu_task(n):
    total = sum(i*i for i in range(n))
    print(f"进程 {multiprocessing.current_process().name} 完成")

if __name__ == "__main__":
    p1 = multiprocessing.Process(target=cpu_task, args=(5_000_000,))
    p2 = multiprocessing.Process(target=cpu_task, args=(5_000_000,))
    p1.start()
    p2.start()
    p1.join()
    p2.join()
方式二:进程间通信(Queue)
def producer(q):
    for i in range(5):
        q.put(f"数据{i}")
    q.put(None)

def consumer(q):
    while True:
        data = q.get()
        if data is None:
            break
        print(f"消费:{data}")

if __name__ == "__main__":
    q = multiprocessing.Queue()
    p1 = multiprocessing.Process(target=producer, args=(q,))
    p2 = multiprocessing.Process(target=consumer, args=(q,))
    p1.start()
    p2.start()
    p1.join()
    p2.join()
方式三:ProcessPoolExecutor 进程池
from concurrent.futures import ProcessPoolExecutor

def square(n):
    return n * n

with ProcessPoolExecutor(max_workers=4) as executor:
    results = executor.map(square, range(10))
    print(list(results))

3. 优缺点

优点

  • 绕过GIL,真正实现并行计算
  • 适合CPU密集型任务,性能提升明显
  • 进程间隔离性强,数据安全性高

缺点

  • 内存占用高,每个进程有独立内存空间
  • 进程间通信复杂,需使用Queue、Pipe等工具
  • 创建和销毁进程的开销较大

三、异步协程(asyncio + aiohttp)

1. 适用场景

异步协程适合高并发I/O密集型任务,如大规模网页爬取、实时数据抓取、Web服务等。它在单线程内通过事件循环实现并发,可以管理数万级并发连接,资源占用极低。

典型应用:百万级网页抓取、实时数据监控、高性能API服务。

2. 使用方法

基础异步抓取
import asyncio
import aiohttp
import time

url_list = [
    "https://www.baidu.com",
    "https://www.qq.com",
    "https://www.sina.com.cn",
    "https://www.163.com",
    "https://www.taobao.com"
]

async def fetch(session, url):
    try:
        async with session.get(url, timeout=aiohttp.ClientTimeout(total=5)) as resp:
            print(f"成功爬取: {url} 状态码: {resp.status}")
            return await resp.text()
    except Exception as e:
        print(f"失败: {url} 错误: {e}")

async def main():
    async with aiohttp.ClientSession() as session:
        tasks = [fetch(session, url) for url in url_list]
        await asyncio.gather(*tasks)

if __name__ == "__main__":
    start = time.time()
    asyncio.run(main())
    end = time.time()
    print(f"\n协程异步爬虫总耗时: {end - start:.2f} 秒")
批量控制与限流

使用信号量(Semaphore)控制并发数量,避免对目标服务器造成过大压力:

async def fetch_with_semaphore(sem, session, url):
    async with sem:
        return await fetch(session, url)

async def main():
    sem = asyncio.Semaphore(10)  # 限制并发数为10
    async with aiohttp.ClientSession() as session:
        tasks = [fetch_with_semaphore(sem, session, url) for url in url_list]
        await asyncio.gather(*tasks)
批量分页处理
batch_size = 100
for i in range(0, len(urls), batch_size):
    batch = urls[i:i+batch_size]
    results = await asyncio.gather(*[fetch(session, url) for url in batch])

3. 优缺点

优点

  • 单线程内实现高并发,资源占用极低(每个协程仅需几KB内存)
  • 速度远超多线程,尤其适合大批量请求
  • 代码逻辑清晰,避免回调地狱

缺点

  • 所有I/O操作必须是异步兼容的,不能直接使用requests.get()time.sleep()等同步函数
  • 调试相对复杂,需要掌握事件循环和异步语法
  • 不适合CPU密集型任务(会阻塞事件循环)

四、三种方案对比总结

维度 多线程 多进程 异步协程
适用场景 I/O密集型(小批量) CPU密集型 I/O密集型(大批量)
并发能力 中等 高(受CPU核心数限制) 极高(万级连接)
资源占用 中等
开发复杂度
GIL影响 受限制 绕过GIL 单线程无GIL问题
速度表现 一般 CPU任务快,I/O任务慢 I/O任务极快
典型耗时 2.2秒(10个任务) 7.6秒(CPU任务) 2.5秒(100+协程)

五、选型建议

1. 根据任务类型选择

  • I/O密集型 + 小批量(几十个请求):多线程即可,简单高效
  • I/O密集型 + 大批量(成千上万请求):优先选择异步协程,性能碾压
  • CPU密集型(数据解析、计算):多进程是唯一选择
  • 混合型(抓取+解析):可采用多进程+异步协程的混合架构,多进程负责解析,异步协程负责抓取

2. 实际项目中的最佳实践

  • 连接复用:使用aiohttp.ClientSession复用TCP连接,减少握手开销
  • 限速控制:添加随机延时,避免触发反爬机制
  • 重试机制:失败请求自动重试,提高抓取成功率
  • 批量写入:数据存储时批量操作,减少I/O调用
  • 监控告警:统计抓取成功率、延迟、内存占用等指标

3. 反爬应对策略

多线程/多进程可模拟多IP请求,降低封禁风险;而异步协程由于单线程特性,在反爬场景中需要额外配置代理池。


结语

Python爬虫的并发方案没有绝对的“最优解”,只有“最适合”。理解每种方案的底层原理和适用场景,才能在实际项目中做出明智的选择。对于大多数爬虫场景,异步协程凭借其极致的并发能力和低资源消耗,正成为越来越多开发者的首选。但如果你需要处理CPU密集型任务,或者项目对开发速度要求更高,多线程和多进程依然是可靠的选择。

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐