Python异步编程实战:使用asyncio与aiohttp构建高性能爬虫

配图:标题:Python异步编程实战:使用asyncio与aiohttp构建高

环境准备:安装与验证

  • 安装 Python 3.8 或更高版本。你可以从 Python 官网下载安装包,或者使用包管理器如 Homebrew (macOS) 或 apt (Ubuntu)。安装后,在终端运行 `python --version` 验证版本。
  • 创建一个项目目录,例如 `async_crawler`,并进入该目录。这有助于隔离依赖。
  • 使用 pip 安装 aiohttp 库。在终端运行命令:`pip install aiohttp`。这会同时安装 aiohttp 及其依赖,包括 asyncio(Python 3.7+ 已内置)。
  • 验证安装。创建一个简单的 Python 脚本来检查 aiohttp 是否可用。在项目目录下创建一个名为 `check_aiohttp.py` 的文件,内容如下。
import aiohttp
import asyncio

async def check():
    async with aiohttp.ClientSession() as session:
        async with session.get('https://httpbin.org/get') as resp:
            print(f"Status: {resp.status}")
            data = await resp.json()
            print(f"Response JSON: {data}")

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

check_aiohttp.py

  • 运行脚本:`python check_aiohttp.py`。预期输出应显示 HTTP 状态码 200 和一个包含请求信息的 JSON 对象。这表明 aiohttp 已正确安装且能发起异步 HTTP 请求。
  • 如果遇到错误,常见问题包括网络连接问题或 aiohttp 版本不兼容。确保你的 Python 版本符合要求,并尝试重新安装:`pip install --upgrade aiohttp`。

步骤拆解:构建异步爬虫核心

  • 定义目标 URL 列表。为了演示,我们使用一个公开的测试 API 端点 `https://httpbin.org/get`,它会返回请求的详细信息。这避免了爬取真实网站可能带来的法律或技术问题。
  • 创建异步函数 `fetch_one` 来处理单个 URL 的请求。这个函数使用 `aiohttp.ClientSession` 发起 GET 请求,并读取响应内容。关键点是使用 `async/await` 语法,这允许事件循环在等待 I/O 时切换到其他任务。
  • 创建主异步函数 `main`,它创建一个任务列表,并使用 `asyncio.gather` 并发执行所有任务。这实现了高并发请求,显著提升效率。
  • 将代码保存为 `async_crawler.py`。完整的代码示例如下。
import aiohttp
import asyncio
import time

URLS = [
    'https://httpbin.org/get',
    'https://httpbin.org/get',
    'https://httpbin.org/get',
    'https://httpbin.org/get',
    'https://httpbin.org/get',
]

async def fetch_one(session, url):
    try:
        async with session.get(url) as resp:
            if resp.status == 200:
                data = await resp.json()
                return f"Success: {url} - {data['url']}"
            else:
                return f"Error: {url} - Status {resp.status}"
    except Exception as e:
        return f"Exception: {url} - {str(e)}"

async def main():
    start_time = time.time()
    async with aiohttp.ClientSession() as session:
        tasks = [fetch_one(session, url) for url in URLS]
        results = await asyncio.gather(*tasks)
        for result in results:
            print(result)
    end_time = time.time()
    print(f"Total time: {end_time - start_time:.2f} seconds")

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

async_crawler.py

  • 运行爬虫:`python async_crawler.py`。预期输出应显示每个任务的成功状态和 URL,以及总耗时。由于是并发请求,总耗时应远低于串行请求的总和(例如,5 个请求可能在 1-2 秒内完成,而串行可能需要 5 秒以上)。
  • 代码解释:`asyncio.gather` 是并发执行的核心,它接受多个协程并等待它们全部完成。`ClientSession` 会复用连接,减少开销,这是 aiohttp 高性能的关键 [来源#2]。
  • 扩展性:你可以修改 `URLS` 列表来测试更多 URL。注意,实际爬虫中应添加延迟和错误重试机制,以避免被目标服务器封禁。

结果验证:性能测试与分析

  • 性能对比:为了验证异步爬虫的优势,我们创建一个串行版本的爬虫进行对比。将以下代码保存为 `sync_crawler.py`。
import requests
import time

URLS = [
    'https://httpbin.org/get',
    'https://httpbin.org/get',
    'https://httpbin.org/get',
    'https://httpbin.org/get',
    'https://httpbin.org/get',
]

def fetch_one(url):
    try:
        resp = requests.get(url, timeout=10)
        if resp.status_code == 200:
            data = resp.json()
            return f"Success: {url} - {data['url']}"
        else:
            return f"Error: {url} - Status {resp.status_code}"
    except Exception as e:
        return f"Exception: {url} - {str(e)}"

def main():
    start_time = time.time()
    for url in URLS:
        result = fetch_one(url)
        print(result)
    end_time = time.time()
    print(f"Total time: {end_time - start_time:.2f} seconds")

if __name__ == "__main__":
    main()

sync_crawler.py

  • 运行串行爬虫:`python sync_crawler.py`。预期输出将显示每个任务依次完成,总耗时通常在 5 秒以上(取决于网络延迟)。
  • 对比结果:运行异步爬虫和串行爬虫后,记录总耗时。异步版本应显著更快,例如异步耗时 1.5 秒,串行耗时 6 秒。这证明了 asyncio 和 aiohttp 在 I/O 密集型任务中的性能优势 [来源#1]。
  • 进一步验证:你可以使用更大的 URL 列表(例如 50 个 URL)来放大差异。但注意,过度并发可能导致服务器拒绝请求,建议在实际爬虫中控制并发数,例如使用 `asyncio.Semaphore` 限制同时运行的任务数。

常见错误与排查

  • 错误 1:`RuntimeError: This event loop is already running`。原因:在已运行的事件循环中再次调用 `asyncio.run()` 或 `loop.run_forever()`。排查:确保每个脚本只调用一次 `asyncio.run()`,或者在 Jupyter Notebook 等环境中使用 `await main()` 而不是 `asyncio.run(main())`。修复:将代码改为 `if name == "main": asyncio.run(main())`,并避免嵌套调用。
  • 错误 2:`aiohttp.client_exceptions.ClientConnectorError: Cannot connect to host`。原因:网络问题、DNS 解析失败或目标服务器不可达。排查:检查网络连接,使用 `ping` 或 `curl` 测试目标 URL。修复:添加重试逻辑,例如使用 `tenacity` 库或手动重试。示例:在 `fetch_one` 函数中添加循环重试。
  • 错误 3:`asyncio.TimeoutError: Timeout while waiting for response`。原因:请求超时,可能由于网络慢或服务器响应慢。排查:检查目标服务器状态,或增加超时时间。修复:在 `aiohttp.ClientSession` 中设置超时参数,例如 `timeout=aiohttp.ClientTimeout(total=30)`。这确保了请求不会无限期等待 [来源#2]。
  • 错误 4:协程未正确等待,导致警告或未执行。原因:忘记在协程调用前加 `await`,或在非异步函数中调用协程。排查:运行时可能看到 `RuntimeWarning: coroutine '...' was never awaited`。修复:确保所有协程调用都使用 `await`,且主函数是异步的。

参考链接

阅读剩余
THE END