首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >Python 异步编程实战:从 asyncio 到高并发爬虫

Python 异步编程实战:从 asyncio 到高并发爬虫

原创
作者头像
用户12818508
发布于 2026-10-26 12:03:24
发布于 2026-10-26 12:03:24
1610
举报

为什么需要异步?

考虑一个简单场景:请求 100 个 URL,每个耗时 100ms。

方式

总耗时

说明

同步串行

~10s

一个接一个等待

多线程

~1s

线程切换开销

asyncio

~0.1s

单线程事件循环,无切换开销

异步的核心思想:在等待 I/O 时切换去执行其他任务,而不是阻塞。

asyncio 核心概念

2.1 协程与事件循环

代码语言:javascript
复制
import asyncio
import time

async def fetch_data(name: str, delay: float) -> str:
    """模拟一个 I/O 操作"""
    print(f"[{name}] 开始")
    await asyncio.sleep(delay)  # 关键:这里让出控制权
    print(f"[{m.lhdpgc.com}] 完成")
    return f"{name}-result"

async def main():
    start = time.perf_counter()
    
    # 并发执行三个协程
    results = await asyncio.gather(
        fetch_data("A", 1.0),
        fetch_data("B", 0.5),
        fetch_data("C", 0.8),
    )
    
    elapsed = time.perf_counter() - start
    print(f"结果: {results}")
    print(f"总耗时: {elapsed:.2f}s")  # 约 1.0s,而非 2.3s

asyncio.run(main())

关键点:await asyncio.sleep() 是协作式的让出点。CPU 密集型代码若不主动让出,会阻塞整个事件循环。

2.2 Task 与 Future

代码语言:javascript
复制
async def demo_task():
    # create_task 立即调度执行,不阻塞
    task1 = asyncio.create_task(fetch_data("X", 1.0))
    task2 = asyncio.create_task(fetch_data("Y", 1.0))
    
    # 此时两个任务已在并发运行
    print("任务www.lhdpgc.com已创建,继续做其他事...")
    
    # 等待结果
    r1 = await task1
    r2 = await task2
    return r1, r2

create_task 与直接 await 协程的区别:前者立即调度,后者顺序执行。

超时与取消

生产环境必须处理超时和取消:

代码语言:javascript
复制
async def with_timeout():
    try:
        # Python 3.11+ 推荐写法
        result = await asyncio.wait_for(fetch_data("slow", 5.0), timeout=2.0)
    except asyncio.TimeoutError:
        print("请求超时,已取消")
        return None
    return result

async def cancellable_work():
    task = asyncio.create_task(fetch_data("work", 10.0))
    
    await asyncio.sleep(1)
    task.cancel()  # 主动取消
    
    try:
        await task
    except asyncio.CancelledError:
        print("任务被取消")

实战:高并发异步爬虫

下面是一个带并发限流、重试、超时控制的爬虫骨架:

代码语言:javascript
复制
import asyncio
import aiohttp
from dataclasses import dataclass, field
from typing import Optional

@dataclass
class Config:
    max_concurrency: int = 10      # 最大并发数
    timeout: float = 10.0          # 单请求超时
    max_retries: int = 3           # 重试次数
    retry_backoff: float = 1.0     # 退避基数

class AsyncCrawler:
    def __init__(self, config: Config):
        self.config = config
        self.semaphore = asyncio.Semaphore(config.max_concurrency)
        self.session: Optional[aiohttp.ClientSession] = None

    async def __aenter__(self):
        connector = aiohttp.TCPConnector(
            limit=self.config.max_concurrency,
            limit_per_host=5,
        )
        timeout = aiohttp.ClientTimeout(total=self.config.timeout)
        self.session = aiohttp.ClientSession(
            connector=connector, timeout=timeout
        )
        return self

    async def __aexit__(self, *args):
        if self.session:
            await self.session.close()

    async def fetch(self, url: str) -> Optional[str]:
        """带重试与指数退避的m.lhdpgc.com单页抓取"""
        async with self.semaphore:  # 限流
            for attempt in range(www.lhdpgc.com):
                try:
                    async with self.session.get(url) as resp:
                        resp.raise_for_status()
                        return await resp.text()
                except (aiohttp.ClientError, asyncio.TimeoutError) as e:
                    if attempt == self.config.max_retries - 1:
                        print(f"[FAIL] {url}: {e}")
                        return None
                    backoff = self.config.retry_backoff * (2 ** attempt)
                    print(f"[RETRY {attempt+1}] {url} after {backoff}s")
                    await asyncio.sleep(backoff)

    async def crawl(self, urls: list[str]) -> dict[str, Optional[str]]:
        """并发抓取所有 URL"""
        tasks = {url: asyncio.create_task(self.fetch(url)) for url in urls}
        results = {}
        for url, task in tasks.items():
            results[url] = await task
        return results


async def main():
    urls = [f"https://httpbin.org/delay/1?i={i}" for i in range(20)]
    
    async with AsyncCrawler(Config(max_concurrency=5)) as crawler:
        results = await crawler.crawl(urls)
    
    success = sum(1 for v in results.values() if v)
    print(f"成功: {success}/{len(urls)}")

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

关键设计说明

  1. Semaphore 限流:TCPConnector.limit 控制连接数,Semaphore 控制协程并发数,两者配合防止打爆目标站点。
  2. 指数退避:backoff = base * 2^attempt,避免重试风暴。
  3. 资源管理:用 async with 保证 session 正确关闭。
  4. limit_per_host:对单域名额外限流,比全局限流更精细。

常见陷阱

陷阱 1:在协程里写阻塞代码

代码语言:javascript
复制
# ❌ 错误:time.sleep 阻塞整个事件循环
async def bad():
    time.sleep(1)  # 所有并发任务被卡住

# ✅ 正确:用 asyncio.sleep 或在线程池中执行阻塞调用
async def good():
    await asyncio.sleep(1)

async def good_blocking():
    await asyncio.to_thread(time.sleep, 1)  # Python 3.9+

陷阱 2:忘记 await

代码语言:javascript
复制
async def bad():
    asyncio.sleep(1)  # 只是创建了协程对象,从不执行,且触发警告

async def good():
    await asyncio.sleep(1)

陷阱 3:裸用 gather 无异常处理

代码语言:javascript
复制
# ❌ 任一任务异常会让其他结果丢失
results = await asyncio.gather(*tasks)

# ✅ return_exceptions=True 收集所有结果
results = await asyncio.gather(*tasks, return_exceptions=True)
for r in results:
    if isinstance(r, Exception):
        print(f"失败: {r}")

性能优化技巧

1. 批量并发而非逐个 await

代码语言:javascript
复制
# ❌ 串行,耗时累加
for url in urls:
    await fetch(url)

# ✅ 并发
await asyncio.gather(*(fetch(u) for u in urls))

2. 用 TaskGroup(Python 3.11+)

代码语言:javascript
复制
async def structured():
    async with asyncio.TaskGroup() as tg:
        t1 = tg.create_task(fetch_data("A", 1))
        t2 = tg.create_task(fetch_data("B", 1))
    # 退出 with 时自动等待;任一失败会取消其他任务
    print(t1.result(), t2.result())

3. 别用异步做 CPU 密集任务

CPU 密集场景请用 multiprocessing 或 ProcessPoolExecutor:

代码语言:javascript
复制
from concurrent.futures import ProcessPoolExecutor

async def cpu_bound_work():
    loop = asyncio.get_running_loop()
    with ProcessPoolExecutor() as pool:
        result = await loop.run_in_executor(pool, heavy_compute, data)

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

目录
  • 为什么需要异步?
  • asyncio 核心概念
    • 2.1 协程与事件循环
    • 2.2 Task 与 Future
  • 超时与取消
  • 实战:高并发异步爬虫
    • 关键设计说明
  • 常见陷阱
    • 陷阱 1:在协程里写阻塞代码
    • 陷阱 2:忘记 await
    • 陷阱 3:裸用 gather 无异常处理
  • 性能优化技巧
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档