Python异步编程实战:用asyncio将爬虫效率提升10倍(附完整代码)

0 阅读5分钟

Python异步编程实战:用asyncio将爬虫效率提升10倍的完整指南(附代码)

在日常开发中,我们经常遇到这样的场景:需要同时请求多个API、爬取多个页面、或者并行处理大量IO任务。如果你还在用同步代码一个一个地执行,那效率可能低得离谱。

今天船长带你从0到1搞懂Python的asyncio,用真实案例演示如何把一个耗时60秒的同步爬虫,优化到只需6秒。

一、先看效果:同步 vs 异步的性能对比

我们先写一个简单的对比脚本:

import asyncio
import time
import aiohttp

# 模拟一个需要1秒的IO请求
async def fetch(session, url):
    async with session.get(url) as resp:
        return await resp.text()

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

def sync_main(urls):
    import requests
    results = []
    for url in urls:
        resp = requests.get(url)
        results.append(resp.text)
    return results

if __name__ == "__main__":
    urls = ["https://httpbin.org/delay/1"] * 10  # 模拟10个耗时请求

    # 同步方式
    start = time.time()
    sync_main(urls)
    print(f"同步耗时: {time.time() - start:.2f}秒")

    # 异步方式
    start = time.time()
    asyncio.run(async_main(urls))
    print(f"异步耗时: {time.time() - start:.2f}秒")

输出结果:

同步耗时: 10.23秒
异步耗时: 1.12秒

同样是10个请求,同步要10秒,异步只要1秒。10倍的效率提升,这就是asyncio的威力。

二、asyncio核心概念速通

很多人一看到asyncawait就头大,其实核心概念就三个:

① 协程(Coroutine)

async def定义的函数就是协程。它看起来像普通函数,但可以"暂停"执行,把控制权交还给事件循环。

async def my_task():
    # 这是一个协程
    result = await some_io_operation()  # 遇到IO就暂停
    return result

② 事件循环(Event Loop)

事件循环是asyncio的大脑,它负责调度所有协程的执行。当一个协程在等待IO时,事件循环会自动切换到另一个协程执行。

③ await关键字

await告诉Python:"这里有个IO操作,我先暂停,等结果回来了再继续。"在等待期间,事件循环可以去执行其他协程。

三、实战案例:异步批量请求API

下面是一个完整的实战案例——批量查询股票数据:

import asyncio
import aiohttp
import json
from datetime import datetime

# 模拟股票API接口(实际替换为你的接口)
API_URL = "https://httpbin.org/json"
STOCK_CODES = ["000001", "000002", "600519", "601318", "000858",
               "002714", "300750", "603259", "002415", "600036"]

async def fetch_stock_data(session, code):
    """异步获取单只股票数据"""
    try:
        async with session.get(f"{API_URL}?code={code}", timeout=10) as resp:
            if resp.status == 200:
                data = await resp.json()
                return {"code": code, "data": data, "status": "ok"}
            return {"code": code, "status": f"error_{resp.status}"}
    except asyncio.TimeoutError:
        return {"code": code, "status": "timeout"}
    except Exception as e:
        return {"code": code, "status": f"error: {str(e)[:50]}"}

async def batch_fetch_stocks(codes, concurrency=5):
    """批量获取股票数据(控制并发数)"""
    semaphore = asyncio.Semaphore(concurrency)  # 限制并发数

    async def limited_fetch(session, code):
        async with semaphore:
            return await fetch_stock_data(session, code)

    async with aiohttp.ClientSession() as session:
        tasks = [limited_fetch(session, code) for code in codes]
        results = await asyncio.gather(*tasks, return_exceptions=True)
        return results

def main():
    start = datetime.now()
    print(f"开始时间: {start.strftime('%H:%M:%S')}")
    print(f"查询{len(STOCK_CODES)}只股票...\n")

    results = asyncio.run(batch_fetch_stocks(STOCK_CODES, concurrency=5))

    success = sum(1 for r in results if isinstance(r, dict) and r.get("status") == "ok")
    failed = len(results) - success

    print(f"成功: {success}, 失败: {failed}")
    print(f"总耗时: {(datetime.now() - start).total_seconds():.2f}秒")

    # 输出失败详情
    for r in results:
        if isinstance(r, dict) and r.get("status") != "ok":
            print(f"  失败: {r['code']} - {r['status']}")

if __name__ == "__main__":
    main()

代码要点解析:

  1. asyncio.Semaphore(5) —— 控制并发数为5,避免瞬间发送太多请求被封IP

  2. asyncio.gather(*tasks) —— 并发执行所有任务

  3. return_exceptions=True —— 即使某个任务报错也不影响其他任务

  4. aiohttp.ClientSession() —— 异步HTTP客户端,替代同步的requests

四、asyncio常见踩坑指南

坑1:在async函数里调用同步IO

# 错误示范:在协程中调用同步requests
async def bad_example():
    import requests  # 这是同步库!会阻塞整个事件循环
    resp = requests.get("https://example.com")

# 正确做法:用aiohttp替代requests
async def good_example():
    async with aiohttp.ClientSession() as session:
        async with session.get("https://example.com") as resp:
            return await resp.text()

坑2:忘记await

# 错误:忘记await,协程不会被实际执行
async def wrong():
    result = fetch_data()  # 只是创建了协程对象,没有执行

# 正确:必须用await
async def right():
    result = await fetch_data()  # 实际执行并等待结果

坑3:并发数不控制导致被封

# 危险:1000个请求同时发出
tasks = [fetch(url) for url in 1000_urls]  # 容易被封IP

# 安全:用Semaphore控制并发
sem = asyncio.Semaphore(10)  # 最多同时10个请求
async def safe_fetch(session, url):
    async with sem:
        return await fetch(session, url)

坑4:Windows上event loop策略问题

# Windows上可能需要手动设置事件循环策略
import sys
if sys.platform == 'win32':
    asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy())

五、什么时候用asyncio,什么时候不用?

适合用asyncio的场景:

  • 网络请求(API调用、爬虫、WebSocket)

  • 文件IO(大量小文件读写)

  • 数据库查询(异步数据库驱动如asyncpg、aiomysql)

  • 任何"等待"类型的操作

不适合用asyncio的场景:

  • CPU密集型任务(数据分析、机器学习训练)

  • 需要顺序执行的逻辑

  • 简单脚本(异步的复杂度不值得)

**CPU密集型任务怎么办?**用concurrent.futures.ProcessPoolExecutor多进程:

from concurrent.futures import ProcessPoolExecutor
import numpy as np

def heavy_computation(n):
    return np.sum(np.random.rand(n))

with ProcessPoolExecutor(max_workers=4) as executor:
    results = list(executor.map(heavy_computation, [1000000] * 8))

六、总结

asyncio的核心价值就一句话:在等待IO的时候,去做别的事情

掌握asyncio后,你的Python代码可以轻松处理:

  • 批量API请求(从串行变并行)

  • 高并发爬虫(效率提升5-10倍)

  • 异步Web服务(FastAPI就是基于asyncio的)

  • 实时数据处理(WebSocket、消息队列)

代码在 GitHub 上,需要的朋友自取。

有问题评论区见,船长在线答疑。