【爬虫教程】第3章:异步编程模型:asyncio/async-http深度应用

第3章:异步编程模型:asyncio/async-http深度应用

目录

  • [3.1 引言:为什么需要异步编程?](#3.1 引言:为什么需要异步编程?)
    • [3.1.1 同步编程的局限性](#3.1.1 同步编程的局限性)
    • [3.1.2 异步编程的优势](#3.1.2 异步编程的优势)
    • [3.1.3 异步 vs 多线程 vs 多进程](#3.1.3 异步 vs 多线程 vs 多进程)
  • [3.2 事件循环(Event Loop)深度解析](#3.2 事件循环(Event Loop)深度解析)
    • [3.2.1 什么是事件循环?](#3.2.1 什么是事件循环?)
    • [3.2.2 select/poll/epoll机制](#3.2.2 select/poll/epoll机制)
    • [3.2.3 任务队列和回调队列](#3.2.3 任务队列和回调队列)
    • [3.2.4 事件循环的执行流程](#3.2.4 事件循环的执行流程)
  • [3.3 协程(Coroutine)执行机制](#3.3 协程(Coroutine)执行机制)
    • [3.3.1 生成器的yield语法](#3.3.1 生成器的yield语法)
    • [3.3.2 yield from的委托语法](#3.3.2 yield from的委托语法)
    • [3.3.3 async/await的工作原理](#3.3.3 async/await的工作原理)
    • [3.3.4 协程的状态机](#3.3.4 协程的状态机)
  • [3.4 异步上下文管理器和迭代器](#3.4 异步上下文管理器和迭代器)
    • [3.4.1 async with的实现原理](#3.4.1 async with的实现原理)
    • [3.4.2 async for的实现原理](#3.4.2 async for的实现原理)
  • [3.5 asyncio模块深度应用](#3.5 asyncio模块深度应用)
    • [3.5.1 基础用法(create_task、gather、wait)](#3.5.1 基础用法(create_task、gather、wait))
    • [3.5.2 并发原语(Lock、Semaphore、Event、Condition、Queue)](#3.5.2 并发原语(Lock、Semaphore、Event、Condition、Queue))
    • [3.5.3 TaskGroup(Python 3.11+)](#3.5.3 TaskGroup(Python 3.11+))
    • [3.5.4 uvloop事件循环](#3.5.4 uvloop事件循环)
  • [3.6 异步HTTP客户端:aiohttp和httpx](#3.6 异步HTTP客户端:aiohttp和httpx)
    • [3.6.1 aiohttp的使用](#3.6.1 aiohttp的使用)
    • [3.6.2 httpx异步客户端的使用](#3.6.2 httpx异步客户端的使用)
    • [3.6.3 性能对比](#3.6.3 性能对比)
  • [3.7 实战演练:构建1000并发的高性能异步爬虫](#3.7 实战演练:构建1000并发的高性能异步爬虫)
    • [3.7.1 步骤1:编写同步爬虫基准代码](#3.7.1 步骤1:编写同步爬虫基准代码)
    • [3.7.2 步骤2:改写为异步爬虫代码](#3.7.2 步骤2:改写为异步爬虫代码)
    • [3.7.3 步骤3:使用信号量控制并发数](#3.7.3 步骤3:使用信号量控制并发数)
    • [3.7.4 步骤4:实现异步DNS解析和代理轮换](#3.7.4 步骤4:实现异步DNS解析和代理轮换)
    • [3.7.5 步骤5:进行性能基准测试](#3.7.5 步骤5:进行性能基准测试)
    • [3.7.6 步骤6:分析和对比不同方案的性能数据](#3.7.6 步骤6:分析和对比不同方案的性能数据)
  • [3.8 常见坑点与排错](#3.8 常见坑点与排错)
    • [3.8.1 忘记await导致协程未执行](#3.8.1 忘记await导致协程未执行)
    • [3.8.2 在协程中使用阻塞操作](#3.8.2 在协程中使用阻塞操作)
    • [3.8.3 异步上下文中使用同步库](#3.8.3 异步上下文中使用同步库)
    • [3.8.4 事件循环的关闭和清理](#3.8.4 事件循环的关闭和清理)
  • [3.9 总结](#3.9 总结)

3.1 引言:为什么需要异步编程?

在爬虫开发中,性能往往是关键因素。传统的同步编程在处理大量I/O操作时效率低下,而异步编程可以大幅提升性能。

3.1.1 同步编程的局限性

同步爬虫的问题:

python 复制代码
import requests
import time

def sync_crawler(urls):
    """同步爬虫:顺序执行"""
    results = []
    start_time = time.time()
    
    for url in urls:
        response = requests.get(url)
        results.append(response.text)
    
    elapsed = time.time() - start_time
    print(f"同步爬虫耗时: {elapsed:.2f}秒")
    return results

# 测试100个URL
urls = [f'https://httpbin.org/delay/1' for _ in range(100)]
sync_crawler(urls)
# 输出: 同步爬虫耗时: 100.00秒(每个请求1秒,100个请求)

问题分析:

  1. 阻塞等待:每个请求必须等待前一个请求完成
  2. CPU空闲:在等待网络响应时,CPU处于空闲状态
  3. 资源浪费:无法充分利用系统资源

时间线分析:
0 0 0 0 0 0 0 0 0 0 1 网络I/O 等待请求1完成 网络I/O 等待请求2完成 网络I/O 等待请求99完成 网络I/O 请求1 请求2 请求3 请求100 同步爬虫时间线

总耗时:100秒(每个请求1秒 × 100个请求)

3.1.2 异步编程的优势

异步爬虫的优势:

python 复制代码
import asyncio
import aiohttp
import time

async def async_crawler(urls):
    """异步爬虫:并发执行"""
    results = []
    start_time = time.time()
    
    async with aiohttp.ClientSession() as session:
        tasks = [fetch_url(session, url) for url in urls]
        results = await asyncio.gather(*tasks)
    
    elapsed = time.time() - start_time
    print(f"异步爬虫耗时: {elapsed:.2f}秒")
    return results

async def fetch_url(session, url):
    """异步获取URL"""
    async with session.get(url) as response:
        return await response.text()

# 测试100个URL
urls = [f'https://httpbin.org/delay/1' for _ in range(100)]
asyncio.run(async_crawler(urls))
# 输出: 异步爬虫耗时: 1.50秒(并发执行,几乎同时完成)

优势分析:

  1. 非阻塞等待:多个请求可以并发执行
  2. CPU高效利用:在等待网络响应时,可以处理其他任务
  3. 资源充分利用:单线程可以处理大量并发连接

时间线分析:
0 0 0 0 0 0 0 0 0 0 1 所有请求并发执行 等待所有响应完成 并发请求 异步爬虫时间线(100并发)

总耗时:约1.5秒(网络延迟 + 少量开销)

性能提升 :100秒 → 1.5秒,提升约66倍

3.1.3 异步 vs 多线程 vs 多进程

三种并发模型的对比:

特性 异步 多线程 多进程
适用场景 I/O密集型 I/O密集型 CPU密集型
资源消耗 低(单线程) 中等(多线程) 高(多进程)
上下文切换 协程切换(轻量) 线程切换(较重) 进程切换(最重)
内存占用 中等
GIL影响 无(单线程) 有(Python GIL) 无(多进程)
代码复杂度 中等
调试难度 较高 中等 较低

性能对比示例:

python 复制代码
import asyncio
import aiohttp
import requests
import threading
import time
from concurrent.futures import ThreadPoolExecutor, ProcessPoolExecutor

# 测试URL列表
urls = [f'https://httpbin.org/delay/0.5' for _ in range(100)]

def sync_benchmark():
    """同步基准测试"""
    start = time.time()
    for url in urls:
        requests.get(url)
    return time.time() - start

def async_benchmark():
    """异步基准测试"""
    async def fetch(session, url):
        async with session.get(url) as resp:
            return await resp.text()
    
    async def run():
        async with aiohttp.ClientSession() as session:
            tasks = [fetch(session, url) for url in urls]
            await asyncio.gather(*tasks)
    
    start = time.time()
    asyncio.run(run())
    return time.time() - start

def thread_benchmark():
    """多线程基准测试"""
    def fetch(url):
        return requests.get(url)
    
    start = time.time()
    with ThreadPoolExecutor(max_workers=50) as executor:
        executor.map(fetch, urls)
    return time.time() - start

# 运行基准测试
print(f"同步: {sync_benchmark():.2f}秒")
print(f"异步: {async_benchmark():.2f}秒")
print(f"多线程: {thread_benchmark():.2f}秒")

典型结果:

  • 同步:50秒
  • 异步:0.8秒
  • 多线程:2.5秒

结论:

  • I/O密集型任务:异步 > 多线程 > 同步
  • CPU密集型任务:多进程 > 多线程 > 异步

3.2 事件循环(Event Loop)深度解析

事件循环是异步编程的核心,理解事件循环的工作原理是掌握异步编程的关键。

3.2.1 什么是事件循环?

事件循环的定义:

事件循环是一个无限循环,它不断地检查是否有事件发生,并执行相应的回调函数。

简单的事件循环模型:

python 复制代码
class SimpleEventLoop:
    """简单的事件循环实现"""
    
    def __init__(self):
        self.ready_queue = []  # 就绪任务队列
        self.io_waiting = {}   # I/O等待字典 {fd: callback}
    
    def run_forever(self):
        """运行事件循环"""
        while self.ready_queue or self.io_waiting:
            # 1. 执行就绪任务
            while self.ready_queue:
                task = self.ready_queue.pop(0)
                task()
            
            # 2. 检查I/O事件
            ready_fds = self.select_io()  # 使用select/poll/epoll
            for fd in ready_fds:
                callback = self.io_waiting.pop(fd)
                self.ready_queue.append(callback)
    
    def select_io(self):
        """检查I/O事件(简化版)"""
        # 实际使用select/poll/epoll
        return []

事件循环的作用:

  1. 任务调度:管理协程的执行顺序
  2. I/O复用:监控多个I/O操作
  3. 回调执行:当I/O就绪时执行回调

3.2.2 select/poll/epoll机制

事件循环底层使用I/O多路复用机制来监控多个文件描述符。

select机制

工作原理:

python 复制代码
import select

def select_example():
    """select机制示例"""
    # 创建socket列表
    sockets = [socket1, socket2, socket3]
    
    while True:
        # 检查哪些socket可读
        readable, writable, exceptional = select.select(
            sockets,  # 可读列表
            sockets,  # 可写列表
            sockets,  # 异常列表
            timeout=1.0  # 超时时间
        )
        
        for sock in readable:
            data = sock.recv(1024)
            # 处理数据

select的特点:

  • 跨平台:Windows、Linux、macOS都支持
  • 限制:最多监控1024个文件描述符(某些系统)
  • 效率:O(n),需要遍历所有文件描述符
poll机制

工作原理:

python 复制代码
import select

def poll_example():
    """poll机制示例"""
    poller = select.poll()
    
    # 注册文件描述符
    for sock in sockets:
        poller.register(sock, select.POLLIN)
    
    while True:
        # 检查事件
        events = poller.poll(timeout=1000)  # 超时1秒
        
        for fd, event in events:
            if event & select.POLLIN:
                # 可读
                data = sock.recv(1024)

poll的特点:

  • 无限制:可以监控任意数量的文件描述符
  • 效率:O(n),仍然需要遍历
  • 平台限制:Windows不支持
epoll机制(Linux)

工作原理:

python 复制代码
import select

def epoll_example():
    """epoll机制示例(仅Linux)"""
    epoller = select.epoll()
    
    # 注册文件描述符
    for sock in sockets:
        epoller.register(sock.fileno(), select.EPOLLIN)
    
    while True:
        # 只返回就绪的事件(高效!)
        events = epoller.poll(timeout=1.0)
        
        for fd, event in events:
            if event & select.EPOLLIN:
                # 可读
                data = sock.recv(1024)

epoll的特点:

  • 高效:O(1),只返回就绪的事件
  • 平台限制:仅Linux支持
  • 大量连接:适合处理大量并发连接
三种机制的性能对比

少量连接

10-100
select/poll
中等连接

100-1000
poll/epoll
大量连接

1000+
epoll

性能对比表:

连接数 select poll epoll
10
100 中等 中等
1000
10000 很慢 很慢

Python asyncio的选择:

  • Windows :使用select(IOCP在ProactorEventLoop中)
  • Linux :使用epoll
  • macOS :使用kqueue(类似epoll)

3.2.3 任务队列和回调队列

事件循环维护多个队列来管理任务和回调。

队列结构:
事件循环
就绪队列

Ready Queue
等待队列

Waiting Queue
调度队列

Scheduled Queue
立即执行
等待I/O
定时任务

队列说明:

  1. 就绪队列(Ready Queue)

    • 存储可以立即执行的任务
    • 先进先出(FIFO)
  2. 等待队列(Waiting Queue)

    • 存储等待I/O操作的任务
    • 当I/O就绪时,任务移到就绪队列
  3. 调度队列(Scheduled Queue)

    • 存储定时任务
    • 按时间排序

Python代码示例:

python 复制代码
import asyncio

async def task1():
    print("任务1开始")
    await asyncio.sleep(1)
    print("任务1结束")

async def task2():
    print("任务2开始")
    await asyncio.sleep(0.5)
    print("任务2结束")

async def main():
    # 创建任务
    t1 = asyncio.create_task(task1())
    t2 = asyncio.create_task(task2())
    
    # 等待所有任务完成
    await asyncio.gather(t1, t2)

# 运行
asyncio.run(main())

执行流程:

  1. task1task2被添加到就绪队列
  2. 事件循环从就绪队列取出task1执行
  3. task1遇到await asyncio.sleep(1),被移到等待队列
  4. 事件循环取出task2执行
  5. task2遇到await asyncio.sleep(0.5),被移到等待队列
  6. 0.5秒后,task2被移回就绪队列,继续执行
  7. 1秒后,task1被移回就绪队列,继续执行

3.2.4 事件循环的执行流程

完整的事件循环执行流程:










启动事件循环
检查就绪队列
就绪队列

是否为空?
取出任务执行
检查I/O事件
任务是否

遇到await?
任务挂起

移到等待队列
任务完成
有I/O

就绪?
将等待任务

移到就绪队列
检查定时任务
有定时任务

到期?
将定时任务

移到就绪队列
事件循环

是否结束?
结束

Python代码实现:

python 复制代码
import asyncio
import time

async def example_task(name, delay):
    """示例任务"""
    print(f"[{time.time():.2f}] {name} 开始")
    await asyncio.sleep(delay)
    print(f"[{time.time():.2f}] {name} 结束")

async def main():
    """主函数"""
    print("=== 事件循环执行流程演示 ===")
    
    # 创建多个任务
    tasks = [
        example_task("任务1", 1.0),
        example_task("任务2", 0.5),
        example_task("任务3", 0.3),
    ]
    
    # 并发执行
    await asyncio.gather(*tasks)

# 运行
asyncio.run(main())

输出示例:

复制代码
=== 事件循环执行流程演示 ===
[1234567890.00] 任务1 开始
[1234567890.00] 任务2 开始
[1234567890.00] 任务3 开始
[1234567890.30] 任务3 结束
[1234567890.50] 任务2 结束
[1234567891.00] 任务1 结束

关键点:

  • 所有任务几乎同时开始
  • 任务按延迟时间顺序完成
  • 事件循环在等待期间可以处理其他任务

3.3 协程(Coroutine)执行机制

协程是异步编程的基本单位,理解协程的执行机制是掌握异步编程的基础。

3.3.1 生成器的yield语法

协程基于生成器实现,理解生成器有助于理解协程。

生成器基础:

python 复制代码
def simple_generator():
    """简单生成器"""
    print("开始")
    yield 1
    print("中间")
    yield 2
    print("结束")
    yield 3

# 使用生成器
gen = simple_generator()
print(next(gen))  # 输出: 开始, 1
print(next(gen))  # 输出: 中间, 2
print(next(gen))  # 输出: 结束, 3

生成器的状态:

python 复制代码
def generator_with_state():
    """带状态的生成器"""
    value = 0
    while value < 3:
        value += 1
        received = yield value
        print(f"收到: {received}")

gen = generator_with_state()
print(next(gen))      # 输出: 1(首次调用需要next,不能send)
print(gen.send(10))   # 输出: 收到: 10, 2
print(gen.send(20))   # 输出: 收到: 20, 3

生成器的优势:

  • 惰性计算:按需生成值
  • 状态保持:可以暂停和恢复
  • 内存高效:不需要存储所有值

3.3.2 yield from的委托语法

yield from允许生成器委托给另一个生成器。

基础用法:

python 复制代码
def generator1():
    yield 1
    yield 2

def generator2():
    yield from generator1()  # 委托给generator1
    yield 3

# 使用
for value in generator2():
    print(value)
# 输出: 1, 2, 3

为什么需要yield from?

python 复制代码
# 不使用yield from(繁琐)
def generator_manual():
    for value in generator1():
        yield value
    yield 3

# 使用yield from(简洁)
def generator_delegated():
    yield from generator1()
    yield 3

yield from的作用:

  1. 简化代码:不需要手动迭代
  2. 值传递:自动传递send的值
  3. 异常处理:自动传播异常

3.3.3 async/await的工作原理

async/await是Python 3.5+引入的语法糖,基于生成器实现。

async函数本质:

python 复制代码
# async函数实际上是生成器函数
async def async_function():
    await asyncio.sleep(1)
    return "完成"

# 等价于(简化版)
def generator_function():
    yield asyncio.sleep(1)
    return "完成"

await的工作原理:

python 复制代码
async def example():
    print("开始")
    result = await asyncio.sleep(1)  # 挂起,等待完成
    print("结束")
    return result

# await的执行流程:
# 1. 遇到await,协程挂起
# 2. 控制权返回给事件循环
# 3. 事件循环执行其他任务
# 4. await的对象完成后,协程恢复执行

await的底层实现(简化版):

python 复制代码
def await_implementation(coro, value):
    """await的简化实现"""
    try:
        # 发送值给协程
        result = coro.send(value)
        
        # 如果返回的是另一个协程,继续await
        if isinstance(result, types.CoroutineType):
            return await_implementation(result, None)
        
        return result
    except StopIteration as e:
        # 协程完成,返回结果
        return e.value

3.3.4 协程的状态机

协程有多个状态,理解状态转换有助于调试。

协程状态:
创建协程
开始执行
遇到await
await完成
执行完成
异常或取消
CORO_CREATED
CORO_RUNNING
CORO_SUSPENDED
CORO_CLOSED

Python代码演示:

python 复制代码
import asyncio
import inspect

async def example_coroutine():
    print("协程开始")
    print(f"状态: {inspect.getgeneratorstate(example_coroutine())}")
    
    await asyncio.sleep(1)
    print("协程恢复")
    
    return "完成"

# 创建协程对象
coro = example_coroutine()
print(f"创建后状态: {inspect.getgeneratorstate(coro)}")

# 运行协程
result = asyncio.run(coro)
print(f"结果: {result}")

状态说明:

  • CORO_CREATED:协程已创建,但未开始执行
  • CORO_RUNNING:协程正在执行
  • CORO_SUSPENDED:协程已挂起(遇到await)
  • CORO_CLOSED:协程已关闭(执行完成或异常)

3.4 异步上下文管理器和迭代器

3.4.1 async with的实现原理

async with用于异步上下文管理,类似于同步的with语句。

基础用法:

python 复制代码
async def example():
    async with aiohttp.ClientSession() as session:
        async with session.get('https://example.com') as response:
            return await response.text()

实现原理:

python 复制代码
class AsyncContextManager:
    """异步上下文管理器示例"""
    
    async def __aenter__(self):
        """进入上下文"""
        print("进入上下文")
        return self
    
    async def __aexit__(self, exc_type, exc_val, exc_tb):
        """退出上下文"""
        print("退出上下文")
        return False  # 不抑制异常

# 使用
async def use_context():
    async with AsyncContextManager() as ctx:
        print("在上下文中")

asyncio.run(use_context())

async with的执行流程:
渲染错误: Mermaid 渲染失败: Parse error on line 7: ...aenter__() CM->>Loop: await (可能挂起) ----------------------^ Expecting '+', '-', 'ACTOR', got 'loop'

3.4.2 async for的实现原理

async for用于异步迭代,类似于同步的for循环。

基础用法:

python 复制代码
async def example():
    async for item in async_generator():
        print(item)

实现原理:

python 复制代码
class AsyncIterator:
    """异步迭代器示例"""
    
    def __init__(self, items):
        self.items = items
        self.index = 0
    
    def __aiter__(self):
        """返回异步迭代器"""
        return self
    
    async def __anext__(self):
        """获取下一个元素"""
        if self.index >= len(self.items):
            raise StopAsyncIteration
        
        # 模拟异步操作
        await asyncio.sleep(0.1)
        
        item = self.items[self.index]
        self.index += 1
        return item

# 使用
async def use_iterator():
    async_iter = AsyncIterator([1, 2, 3, 4, 5])
    async for item in async_iter:
        print(item)

asyncio.run(use_iterator())

async for的执行流程:
渲染错误: Mermaid 渲染失败: Parse error on line 11: ...xt__() AI->>Loop: await (可能挂起) ----------------------^ Expecting '+', '-', 'ACTOR', got 'loop'


3.5 asyncio模块深度应用

3.5.1 基础用法(create_task、gather、wait)

create_task

create_task用于创建任务并立即调度执行。

python 复制代码
import asyncio

async def task(name, delay):
    print(f"{name} 开始")
    await asyncio.sleep(delay)
    print(f"{name} 结束")
    return f"{name} 完成"

async def main():
    # 创建任务
    task1 = asyncio.create_task(task("任务1", 1.0))
    task2 = asyncio.create_task(task("任务2", 0.5))
    
    # 等待任务完成
    result1 = await task1
    result2 = await task2
    
    print(f"结果: {result1}, {result2}")

asyncio.run(main())

create_task vs 直接await:

python 复制代码
# 错误:顺序执行
async def wrong_way():
    await task("任务1", 1.0)  # 等待1秒
    await task("任务2", 0.5)  # 再等待0.5秒
    # 总耗时: 1.5秒

# 正确:并发执行
async def right_way():
    task1 = asyncio.create_task(task("任务1", 1.0))
    task2 = asyncio.create_task(task("任务2", 0.5))
    await task1
    await task2
    # 总耗时: 1.0秒(并发执行)
gather

gather用于并发执行多个协程,并收集结果。

python 复制代码
async def main():
    # 并发执行多个任务
    results = await asyncio.gather(
        task("任务1", 1.0),
        task("任务2", 0.5),
        task("任务3", 0.3),
    )
    
    print(f"结果: {results}")
    # 输出: ['任务1 完成', '任务2 完成', '任务3 完成']

asyncio.run(main())

gather的高级用法:

python 复制代码
async def main():
    # return_exceptions=True: 异常不中断,返回异常对象
    results = await asyncio.gather(
        task("任务1", 1.0),
        task("任务2", 0.5),
        task("任务3", 0.3),
        return_exceptions=True
    )
    
    for i, result in enumerate(results):
        if isinstance(result, Exception):
            print(f"任务{i+1}失败: {result}")
        else:
            print(f"任务{i+1}成功: {result}")
wait

wait用于等待多个协程完成,提供更灵活的控制。

python 复制代码
async def main():
    tasks = [
        asyncio.create_task(task("任务1", 1.0)),
        asyncio.create_task(task("任务2", 0.5)),
        asyncio.create_task(task("任务3", 0.3)),
    ]
    
    # 等待所有任务完成
    done, pending = await asyncio.wait(tasks)
    
    print(f"完成: {len(done)}, 等待: {len(pending)}")
    
    # 获取结果
    for task in done:
        print(f"结果: {task.result()}")

asyncio.run(main())

wait的选项:

python 复制代码
# return_when参数
done, pending = await asyncio.wait(
    tasks,
    return_when=asyncio.FIRST_COMPLETED  # 第一个完成就返回
)

# 选项:
# - asyncio.ALL_COMPLETED: 所有完成(默认)
# - asyncio.FIRST_COMPLETED: 第一个完成
# - asyncio.FIRST_EXCEPTION: 第一个异常

3.5.2 并发原语(Lock、Semaphore、Event、Condition、Queue)

Lock(锁)

用于保护共享资源,防止并发访问。

python 复制代码
import asyncio

# 共享资源
counter = 0
lock = asyncio.Lock()

async def increment(name):
    global counter
    async with lock:  # 获取锁
        # 临界区
        old_value = counter
        await asyncio.sleep(0.1)  # 模拟I/O操作
        counter = old_value + 1
        print(f"{name}: counter = {counter}")

async def main():
    tasks = [increment(f"任务{i}") for i in range(5)]
    await asyncio.gather(*tasks)
    print(f"最终counter: {counter}")

asyncio.run(main())
Semaphore(信号量)

用于限制并发数量。

python 复制代码
import asyncio

# 限制最多3个并发
semaphore = asyncio.Semaphore(3)

async def limited_task(name):
    async with semaphore:  # 获取信号量
        print(f"{name} 开始")
        await asyncio.sleep(1)
        print(f"{name} 结束")

async def main():
    tasks = [limited_task(f"任务{i}") for i in range(10)]
    await asyncio.gather(*tasks)

asyncio.run(main())

执行流程:
0 0 0 0 0 0 0 0 0 0 1 任务0 任务1 任务2 任务3 任务4 任务5 任务6 任务7 任务8 任务9 并发组1 并发组2 并发组3 并发组4 信号量控制并发(最多3个)

Event(事件)

用于协程之间的通信。

python 复制代码
import asyncio

event = asyncio.Event()

async def waiter(name):
    print(f"{name} 等待事件")
    await event.wait()  # 等待事件
    print(f"{name} 收到事件")

async def setter():
    await asyncio.sleep(2)
    print("设置事件")
    event.set()  # 设置事件,唤醒所有等待者

async def main():
    await asyncio.gather(
        waiter("等待者1"),
        waiter("等待者2"),
        setter()
    )

asyncio.run(main())
Condition(条件变量)

用于复杂的同步场景。

python 复制代码
import asyncio

condition = asyncio.Condition()
items = []

async def producer():
    async with condition:
        items.append("item")
        print("生产者: 添加item")
        condition.notify_all()  # 通知所有等待者

async def consumer(name):
    async with condition:
        while not items:
            await condition.wait()  # 等待条件满足
        item = items.pop()
        print(f"{name}: 消费 {item}")

async def main():
    await asyncio.gather(
        consumer("消费者1"),
        consumer("消费者2"),
        asyncio.sleep(1),
        producer()
    )

asyncio.run(main())
Queue(队列)

用于协程之间的数据传递。

python 复制代码
import asyncio

queue = asyncio.Queue(maxsize=3)

async def producer():
    for i in range(5):
        await queue.put(f"item{i}")
        print(f"生产者: 放入 item{i}")
        await asyncio.sleep(0.5)

async def consumer(name):
    while True:
        item = await queue.get()
        print(f"{name}: 消费 {item}")
        queue.task_done()
        if item == "item4":
            break

async def main():
    await asyncio.gather(
        producer(),
        consumer("消费者1"),
        consumer("消费者2")
    )

asyncio.run(main())

3.5.3 TaskGroup(Python 3.11+)

TaskGroup是Python 3.11+引入的新特性,提供更好的任务管理。

基础用法:

python 复制代码
async def main():
    async with asyncio.TaskGroup() as tg:
        # 创建任务
        task1 = tg.create_task(task("任务1", 1.0))
        task2 = tg.create_task(task("任务2", 0.5))
        task3 = tg.create_task(task("任务3", 0.3))
    
    # 所有任务完成后才退出上下文
    print("所有任务完成")

asyncio.run(main())

TaskGroup的优势:

  1. 自动等待:退出上下文时自动等待所有任务
  2. 异常处理:任何任务异常会取消其他任务
  3. 代码简洁:不需要手动gather

对比gather:

python 复制代码
# 使用gather(旧方式)
async def old_way():
    try:
        await asyncio.gather(
            task("任务1", 1.0),
            task("任务2", 0.5),
            task("任务3", 0.3),
        )
    except Exception as e:
        # 需要手动处理异常
        print(f"错误: {e}")

# 使用TaskGroup(新方式)
async def new_way():
    async with asyncio.TaskGroup() as tg:
        tg.create_task(task("任务1", 1.0))
        tg.create_task(task("任务2", 0.5))
        tg.create_task(task("任务3", 0.3))
    # 自动处理异常和清理

3.5.4 uvloop事件循环

uvloop是asyncio的高性能替代实现,基于libuv。

安装:

bash 复制代码
pip install uvloop

使用:

python 复制代码
import uvloop
import asyncio

# 设置uvloop为事件循环策略
asyncio.set_event_loop_policy(uvloop.EventLoopPolicy())

async def main():
    # 使用uvloop运行
    await asyncio.sleep(1)
    print("使用uvloop")

asyncio.run(main())

性能对比:

场景 asyncio uvloop 提升
简单I/O 基准 2-4x 2-4倍
网络I/O 基准 2-4x 2-4倍
大量连接 基准 4-6x 4-6倍

注意事项:

  • uvloop仅支持Linux和macOS
  • Windows不支持uvloop

3.6 异步HTTP客户端:aiohttp和httpx

3.6.1 aiohttp的使用

基础用法:

python 复制代码
import aiohttp
import asyncio

async def fetch_url(session, url):
    """异步获取URL"""
    async with session.get(url) as response:
        return await response.text()

async def main():
    async with aiohttp.ClientSession() as session:
        tasks = [
            fetch_url(session, 'https://httpbin.org/get'),
            fetch_url(session, 'https://httpbin.org/post'),
        ]
        results = await asyncio.gather(*tasks)
        print(results)

asyncio.run(main())

高级用法:

python 复制代码
async def advanced_example():
    """aiohttp高级用法"""
    timeout = aiohttp.ClientTimeout(total=10)
    connector = aiohttp.TCPConnector(limit=100)
    
    async with aiohttp.ClientSession(
        timeout=timeout,
        connector=connector,
        headers={'User-Agent': 'MyBot/1.0'}
    ) as session:
        # 并发请求
        async with session.get('https://example.com') as resp:
            data = await resp.json()
            print(data)

asyncio.run(advanced_example())

3.6.2 httpx异步客户端的使用

基础用法:

python 复制代码
import httpx
import asyncio

async def fetch_url(client, url):
    """使用httpx异步获取URL"""
    response = await client.get(url)
    return response.text

async def main():
    async with httpx.AsyncClient() as client:
        tasks = [
            fetch_url(client, 'https://httpbin.org/get'),
            fetch_url(client, 'https://httpbin.org/post'),
        ]
        results = await asyncio.gather(*tasks)
        print(results)

asyncio.run(main())

httpx的优势:

  1. API一致性:同步和异步API相同
  2. HTTP/2支持:支持HTTP/2协议
  3. 类型提示:完整的类型提示支持

3.6.3 性能对比

python 复制代码
import asyncio
import aiohttp
import httpx
import time

urls = [f'https://httpbin.org/delay/0.5' for _ in range(100)]

async def aiohttp_benchmark():
    """aiohttp基准测试"""
    start = time.time()
    async with aiohttp.ClientSession() as session:
        tasks = [session.get(url) for url in urls]
        await asyncio.gather(*tasks)
    return time.time() - start

async def httpx_benchmark():
    """httpx基准测试"""
    start = time.time()
    async with httpx.AsyncClient() as client:
        tasks = [client.get(url) for url in urls]
        await asyncio.gather(*tasks)
    return time.time() - start

# 运行基准测试
print(f"aiohttp: {asyncio.run(aiohttp_benchmark()):.2f}秒")
print(f"httpx: {asyncio.run(httpx_benchmark()):.2f}秒")

典型结果:

  • aiohttp: 0.8秒
  • httpx: 0.9秒

选择建议:

  • aiohttp:更成熟,性能略好
  • httpx:API更现代,支持HTTP/2

3.7 实战演练:构建1000并发的高性能异步爬虫

3.7.1 步骤1:编写同步爬虫基准代码

python 复制代码
import requests
import time
from typing import List

def sync_crawler(urls: List[str]) -> List[str]:
    """同步爬虫基准代码"""
    results = []
    start_time = time.time()
    
    for url in urls:
        try:
            response = requests.get(url, timeout=10)
            results.append(response.text[:100])  # 只取前100字符
        except Exception as e:
            results.append(f"错误: {e}")
    
    elapsed = time.time() - start_time
    print(f"同步爬虫:")
    print(f"  总耗时: {elapsed:.2f}秒")
    print(f"  请求数: {len(urls)}")
    print(f"  成功率: {len([r for r in results if not r.startswith('错误')]) / len(urls) * 100:.1f}%")
    
    return results

# 测试
if __name__ == '__main__':
    urls = [f'https://httpbin.org/delay/0.5' for _ in range(100)]
    sync_crawler(urls)

3.7.2 步骤2:改写为异步爬虫代码

python 复制代码
import asyncio
import aiohttp
import time
from typing import List

async def async_crawler(urls: List[str]) -> List[str]:
    """异步爬虫代码"""
    results = []
    start_time = time.time()
    
    async with aiohttp.ClientSession() as session:
        tasks = [fetch_url(session, url) for url in urls]
        results = await asyncio.gather(*tasks, return_exceptions=True)
    
    # 处理异常结果
    results = [str(r) if isinstance(r, Exception) else r[:100] for r in results]
    
    elapsed = time.time() - start_time
    print(f"异步爬虫:")
    print(f"  总耗时: {elapsed:.2f}秒")
    print(f"  请求数: {len(urls)}")
    print(f"  成功率: {len([r for r in results if not r.startswith('错误')]) / len(urls) * 100:.1f}%")
    
    return results

async def fetch_url(session: aiohttp.ClientSession, url: str) -> str:
    """异步获取URL"""
    try:
        async with session.get(url, timeout=aiohttp.ClientTimeout(total=10)) as response:
            return await response.text()
    except Exception as e:
        return f"错误: {e}"

# 测试
if __name__ == '__main__':
    urls = [f'https://httpbin.org/delay/0.5' for _ in range(100)]
    asyncio.run(async_crawler(urls))

3.7.3 步骤3:使用信号量控制并发数

python 复制代码
import asyncio
import aiohttp
import time
from typing import List

async def async_crawler_with_semaphore(urls: List[str], max_concurrent: int = 100) -> List[str]:
    """使用信号量控制并发数的异步爬虫"""
    semaphore = asyncio.Semaphore(max_concurrent)
    results = []
    start_time = time.time()
    
    async def fetch_with_limit(session: aiohttp.ClientSession, url: str) -> str:
        async with semaphore:  # 限制并发数
            return await fetch_url(session, url)
    
    async with aiohttp.ClientSession() as session:
        tasks = [fetch_with_limit(session, url) for url in urls]
        results = await asyncio.gather(*tasks, return_exceptions=True)
    
    # 处理异常结果
    results = [str(r) if isinstance(r, Exception) else r[:100] for r in results]
    
    elapsed = time.time() - start_time
    print(f"异步爬虫(并发数={max_concurrent}):")
    print(f"  总耗时: {elapsed:.2f}秒")
    print(f"  请求数: {len(urls)}")
    print(f"  成功率: {len([r for r in results if not r.startswith('错误')]) / len(urls) * 100:.1f}%")
    
    return results

async def fetch_url(session: aiohttp.ClientSession, url: str) -> str:
    """异步获取URL"""
    try:
        async with session.get(url, timeout=aiohttp.ClientTimeout(total=10)) as response:
            return await response.text()
    except Exception as e:
        return f"错误: {e}"

# 测试
if __name__ == '__main__':
    urls = [f'https://httpbin.org/delay/0.5' for _ in range(1000)]
    asyncio.run(async_crawler_with_semaphore(urls, max_concurrent=100))

3.7.4 步骤4:实现异步DNS解析和代理轮换

python 复制代码
import asyncio
import aiohttp
import aiodns
import time
from typing import List, Optional
import random

class AsyncCrawlerWithProxy:
    """支持代理轮换的异步爬虫"""
    
    def __init__(self, proxies: Optional[List[str]] = None, max_concurrent: int = 100):
        self.proxies = proxies or []
        self.max_concurrent = max_concurrent
        self.resolver = aiodns.DNSResolver()
    
    async def resolve_dns(self, hostname: str) -> str:
        """异步DNS解析"""
        try:
            result = await self.resolver.gethostbyname(hostname, aiodns.AF_INET)
            return result.addresses[0]
        except Exception as e:
            print(f"DNS解析失败 {hostname}: {e}")
            return hostname
    
    def get_proxy(self) -> Optional[str]:
        """获取随机代理"""
        if self.proxies:
            return random.choice(self.proxies)
        return None
    
    async def fetch_url(self, session: aiohttp.ClientSession, url: str) -> str:
        """异步获取URL(支持代理)"""
        proxy = self.get_proxy()
        try:
            async with session.get(
                url,
                proxy=proxy,
                timeout=aiohttp.ClientTimeout(total=10)
            ) as response:
                return await response.text()
        except Exception as e:
            return f"错误: {e}"
    
    async def crawl(self, urls: List[str]) -> List[str]:
        """爬取URL列表"""
        semaphore = asyncio.Semaphore(self.max_concurrent)
        results = []
        start_time = time.time()
        
        async def fetch_with_limit(session: aiohttp.ClientSession, url: str) -> str:
            async with semaphore:
                return await self.fetch_url(session, url)
        
        async with aiohttp.ClientSession() as session:
            tasks = [fetch_with_limit(session, url) for url in urls]
            results = await asyncio.gather(*tasks, return_exceptions=True)
        
        # 处理异常结果
        results = [str(r) if isinstance(r, Exception) else r[:100] for r in results]
        
        elapsed = time.time() - start_time
        print(f"异步爬虫(代理轮换):")
        print(f"  总耗时: {elapsed:.2f}秒")
        print(f"  请求数: {len(urls)}")
        print(f"  成功率: {len([r for r in results if not r.startswith('错误')]) / len(urls) * 100:.1f}%")
        
        return results

# 使用示例
async def main():
    # 代理列表(示例)
    proxies = [
        'http://proxy1.example.com:8080',
        'http://proxy2.example.com:8080',
    ]
    
    crawler = AsyncCrawlerWithProxy(proxies=proxies, max_concurrent=100)
    urls = [f'https://httpbin.org/delay/0.5' for _ in range(1000)]
    await crawler.crawl(urls)

if __name__ == '__main__':
    asyncio.run(main())

3.7.5 步骤5:进行性能基准测试

python 复制代码
import asyncio
import aiohttp
import requests
import time
from concurrent.futures import ThreadPoolExecutor
from typing import List

def benchmark_sync(urls: List[str]) -> float:
    """同步基准测试"""
    start = time.time()
    for url in urls:
        requests.get(url, timeout=10)
    return time.time() - start

def benchmark_thread(urls: List[str], max_workers: int = 50) -> float:
    """多线程基准测试"""
    def fetch(url):
        return requests.get(url, timeout=10)
    
    start = time.time()
    with ThreadPoolExecutor(max_workers=max_workers) as executor:
        executor.map(fetch, urls)
    return time.time() - start

async def benchmark_async(urls: List[str], max_concurrent: int = 100) -> float:
    """异步基准测试"""
    semaphore = asyncio.Semaphore(max_concurrent)
    
    async def fetch(session, url):
        async with semaphore:
            async with session.get(url, timeout=aiohttp.ClientTimeout(total=10)) as resp:
                return await resp.text()
    
    start = time.time()
    async with aiohttp.ClientSession() as session:
        tasks = [fetch(session, url) for url in urls]
        await asyncio.gather(*tasks, return_exceptions=True)
    return time.time() - start

async def run_benchmarks():
    """运行所有基准测试"""
    urls = [f'https://httpbin.org/delay/0.5' for _ in range(100)]
    
    print("=== 性能基准测试 ===")
    print(f"测试URL数量: {len(urls)}")
    print(f"每个URL延迟: 0.5秒")
    print()
    
    # 同步测试
    print("1. 同步爬虫测试...")
    sync_time = benchmark_sync(urls[:10])  # 只测试10个,避免太慢
    print(f"   耗时: {sync_time:.2f}秒(10个URL)")
    print(f"   预估100个URL: {sync_time * 10:.2f}秒")
    print()
    
    # 多线程测试
    print("2. 多线程爬虫测试...")
    thread_time = benchmark_thread(urls)
    print(f"   耗时: {thread_time:.2f}秒")
    print()
    
    # 异步测试
    print("3. 异步爬虫测试...")
    async_time = await benchmark_async(urls)
    print(f"   耗时: {async_time:.2f}秒")
    print()
    
    # 性能对比
    print("=== 性能对比 ===")
    print(f"同步(预估): {sync_time * 10:.2f}秒")
    print(f"多线程: {thread_time:.2f}秒")
    print(f"异步: {async_time:.2f}秒")
    print()
    print(f"异步 vs 同步: {sync_time * 10 / async_time:.1f}x 倍速")
    print(f"异步 vs 多线程: {thread_time / async_time:.1f}x 倍速")

if __name__ == '__main__':
    asyncio.run(run_benchmarks())

3.7.6 步骤6:分析和对比不同方案的性能数据

完整的高性能异步爬虫:

python 复制代码
import asyncio
import aiohttp
import time
from typing import List, Dict, Optional
import json

class HighPerformanceAsyncCrawler:
    """高性能异步爬虫"""
    
    def __init__(
        self,
        max_concurrent: int = 1000,
        timeout: int = 30,
        proxies: Optional[List[str]] = None,
        headers: Optional[Dict[str, str]] = None
    ):
        self.max_concurrent = max_concurrent
        self.timeout = aiohttp.ClientTimeout(total=timeout)
        self.proxies = proxies or []
        self.headers = headers or {}
        self.stats = {
            'total': 0,
            'success': 0,
            'failed': 0,
            'start_time': None,
            'end_time': None,
        }
    
    async def fetch_url(
        self,
        session: aiohttp.ClientSession,
        url: str,
        semaphore: asyncio.Semaphore
    ) -> Dict:
        """异步获取URL"""
        async with semaphore:
            self.stats['total'] += 1
            try:
                proxy = random.choice(self.proxies) if self.proxies else None
                async with session.get(
                    url,
                    proxy=proxy,
                    timeout=self.timeout,
                    headers=self.headers
                ) as response:
                    text = await response.text()
                    self.stats['success'] += 1
                    return {
                        'url': url,
                        'status': response.status,
                        'success': True,
                        'length': len(text)
                    }
            except Exception as e:
                self.stats['failed'] += 1
                return {
                    'url': url,
                    'status': 0,
                    'success': False,
                    'error': str(e)
                }
    
    async def crawl(self, urls: List[str]) -> List[Dict]:
        """爬取URL列表"""
        self.stats['start_time'] = time.time()
        semaphore = asyncio.Semaphore(self.max_concurrent)
        
        async with aiohttp.ClientSession() as session:
            tasks = [
                self.fetch_url(session, url, semaphore)
                for url in urls
            ]
            results = await asyncio.gather(*tasks, return_exceptions=True)
        
        self.stats['end_time'] = time.time()
        self.print_stats()
        
        return results
    
    def print_stats(self):
        """打印统计信息"""
        elapsed = self.stats['end_time'] - self.stats['start_time']
        qps = self.stats['total'] / elapsed if elapsed > 0 else 0
        
        print("=== 爬虫统计 ===")
        print(f"总请求数: {self.stats['total']}")
        print(f"成功: {self.stats['success']}")
        print(f"失败: {self.stats['failed']}")
        print(f"成功率: {self.stats['success'] / self.stats['total'] * 100:.1f}%")
        print(f"总耗时: {elapsed:.2f}秒")
        print(f"QPS: {qps:.1f} 请求/秒")
        print(f"平均延迟: {elapsed / self.stats['total'] * 1000:.1f}ms")

# 使用示例
async def main():
    crawler = HighPerformanceAsyncCrawler(
        max_concurrent=1000,
        timeout=30,
        headers={'User-Agent': 'MyBot/1.0'}
    )
    
    urls = [f'https://httpbin.org/delay/0.1' for _ in range(1000)]
    results = await crawler.crawl(urls)
    
    # 保存结果
    with open('results.json', 'w') as f:
        json.dump(results, f, indent=2)

if __name__ == '__main__':
    asyncio.run(main())

3.8 常见坑点与排错

3.8.1 忘记await导致协程未执行

错误示例:

python 复制代码
async def fetch_data():
    return "数据"

async def main():
    # 错误:忘记await
    result = fetch_data()  # 这是一个协程对象,不是结果!
    print(result)  # 输出: <coroutine object fetch_data at 0x...>

asyncio.run(main())

正确做法:

python 复制代码
async def main():
    # 正确:使用await
    result = await fetch_data()
    print(result)  # 输出: 数据

asyncio.run(main())

Tips/坑点:

  • ⚠️ 忘记await会导致协程未执行,返回协程对象而不是结果

3.8.2 在协程中使用阻塞操作

错误示例:

python 复制代码
import time

async def bad_example():
    # 错误:使用time.sleep会阻塞整个事件循环
    time.sleep(1)  # 阻塞!
    return "完成"

async def main():
    tasks = [bad_example() for _ in range(10)]
    await asyncio.gather(*tasks)
    # 总耗时: 10秒(顺序执行)

asyncio.run(main())

正确做法:

python 复制代码
async def good_example():
    # 正确:使用asyncio.sleep
    await asyncio.sleep(1)  # 非阻塞
    return "完成"

async def main():
    tasks = [good_example() for _ in range(10)]
    await asyncio.gather(*tasks)
    # 总耗时: 1秒(并发执行)

asyncio.run(main())

常见阻塞操作:

阻塞操作 异步替代
time.sleep() asyncio.sleep()
requests.get() aiohttp.ClientSession.get()
open() aiofiles.open()
socket.recv() asyncio streams

Tips/坑点:

  • ⚠️ 在协程中使用阻塞操作(如time.sleep)会阻塞整个事件循环

3.8.3 异步上下文中使用同步库

错误示例:

python 复制代码
async def bad_example():
    # 错误:在异步上下文中使用同步库
    import requests
    response = requests.get('https://example.com')  # 阻塞!
    return response.text

正确做法:

python 复制代码
async def good_example():
    # 正确:使用异步库
    import aiohttp
    async with aiohttp.ClientSession() as session:
        async with session.get('https://example.com') as response:
            return await response.text()

Tips/坑点:

  • ⚠️ 异步上下文中使用同步库会失去异步优势

3.8.4 事件循环的关闭和清理

错误示例:

python 复制代码
# 错误:在已有事件循环中创建新的事件循环
async def bad_example():
    loop = asyncio.new_event_loop()
    asyncio.set_event_loop(loop)
    # 可能导致问题

正确做法:

python 复制代码
# 正确:使用asyncio.run
async def good_example():
    # 代码
    pass

asyncio.run(good_example())  # 自动管理事件循环

清理资源:

python 复制代码
async def example_with_cleanup():
    session = aiohttp.ClientSession()
    try:
        # 使用session
        async with session.get('https://example.com') as response:
            return await response.text()
    finally:
        # 清理资源
        await session.close()

3.9 总结

本章深入讲解了异步编程的核心概念和实战应用:

  1. 事件循环:理解了select/poll/epoll机制和任务调度
  2. 协程机制:理解了生成器、yield from和async/await的工作原理
  3. asyncio模块:掌握了create_task、gather、wait和并发原语的使用
  4. 异步HTTP客户端:学会了使用aiohttp和httpx
  5. 实战应用:构建了支持1000并发的高性能异步爬虫

关键要点:

  • ⚠️ 忘记await会导致协程未执行
  • ⚠️ 在协程中使用阻塞操作会阻塞整个事件循环
  • ⚠️ 异步上下文中使用同步库会失去异步优势
  • ✅ 异步编程适合I/O密集型任务,可以大幅提升性能

性能提升:

  • 同步爬虫:100秒(100个请求)
  • 异步爬虫:1.5秒(100个请求)
  • 提升约66倍!

在下一章中,我们将学习HTTP客户端库的深度定制,进一步提升爬虫的隐蔽性和性能。

相关推荐
阿巴~阿巴~1 分钟前
IPv4地址的边界与智慧:特殊用途、枯竭挑战与应对策略全景解析
运维·服务器·网络·网络协议·tcp/ip·ipv4·ipv4地址枯竭
海奥华25 分钟前
Golang Channel 原理深度解析
服务器·开发语言·网络·数据结构·算法·golang
源远流长jerry8 分钟前
TCP 与 TLS 层面 HTTP/1 升级到 HTTP/2
网络协议·tcp/ip·http
松涛和鸣11 分钟前
48、MQTT 3.1.1
linux·前端·网络·数据库·tcp/ip·html
希赛网15 分钟前
网工备考,华为ENSP基础配置命令
服务器·网络·网络工程师·华为认证·命令行·ensp命令·网工备考
三两肉19 分钟前
从明文到加密:HTTP与HTTPS核心知识全解析
网络协议·http·https
北京耐用通信22 分钟前
工业通信中的“工业战狼”!耐达讯自动化CAN转PROFIBUS网关
网络·人工智能·物联网·网络协议·自动化·信息与通信
晚枫歌F24 分钟前
基于DPDK实现UDP收发理解网络协议
网络·网络协议·udp
Tao____28 分钟前
物联网平台二开
java·网络·物联网·mqtt·网络协议
天天睡大觉30 分钟前
Python学习2
网络·python·学习