Python异步爬虫实战:从零到一构建高性能并发爬虫

随着互联网数据量的爆炸式增长,单线程的爬虫已经难以满足我们对数据采集效率的需求。传统的同步爬虫虽然代码简单,但每次请求都需要等待服务器响应,造成了大量的时间浪费在IO等待上。而异步爬虫能够充分利用等待时间,同时发起多个请求,极大地提升爬取效率。

本文将基于一段完整的异步爬虫代码,深入讲解Python异步IO在爬虫领域的应用,带你从零构建一个高性能的异步爬虫。

为什么要用异步爬虫?

在理解异步爬虫之前,我们先来看看同步爬虫的痛点。当我们使用同步的requests库发送HTTP请求时,程序会阻塞在response = requests.get(url)这行代码上,直到服务器返回响应才能继续执行。在等待的这段时间里,CPU其实是空闲的,这无疑是一种资源浪费。

异步爬虫通过事件循环机制,在等待响应的同时可以去做其他事情。当某个请求的响应回来后,事件循环会通知对应的协程继续执行。这种方式让我们能够用单线程实现并发,既避免了多线程的资源开销,又达到了类似并发的效果。

技术栈选择

在Python生态中,实现异步爬虫的最佳组合是asyncio加aiohttp。asyncio是Python内置的异步IO框架,提供了事件循环和协程支持。aiohttp则是基于asyncio的HTTP客户端库,能够与asyncio无缝配合,实现非阻塞的HTTP请求。

我们以一个实际的案例来进行讲解,该代码是从爱给游戏资源网站爬取页面数据。

代码逐层解析

1. 导入异步核心库

python 复制代码
# 导入异步IO核心库
import asyncio
# 导入异步http请求库,替代同步的requests
import aiohttp
from textual.demo import page

首先导入两个核心库。asyncio是异步编程的基础,提供了事件循环、协程任务管理等核心功能。aiohttp是我们实现异步HTTP请求的利器,它基于asyncio开发,所有的网络操作都是非阻塞的。

2. 并发控制与请求配置

python 复制代码
# 防止请求太快封ip,设置最大并发数
max_request = 3
# 创建信号量对象,用来限制协程并发数量
sem = asyncio.Semaphore(max_request)
# 请求头,模拟浏览器访问
request_headers = {
    "User-Agent":"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/151.0.0.0 Safari/537.36"
}
# 请求时长超过10秒直接抛出异常
request_timeout = aiohttp.ClientTimeout(total=10)

这部分代码体现了对目标网站的友好访问策略。通过asyncio.Semaphore信号量来控制并发数量,避免请求过快导致IP被封。信号量就像一个令牌桶,只有获取到令牌的协程才能执行请求,未获取到的协程会等待直到有令牌释放。

请求头中我们设置了User-Agent字段模拟浏览器访问,这是反爬虫的基本手段之一。aiohttp.ClientTimeout则设置了全局超时时间,防止某个请求因为网络问题无限期挂起。

3. 核心请求协程函数

python 复制代码
# 定义单个爬虫协程函数 task_url:目标网址, session:全局复用的会话对象
async def request_task(task_url:str,session:aiohttp.ClientSession):
    # async+信号量:进入受保护的代码块,控制并发坑位
    async with sem:
        try:
            # async with 异步上下文管理器,发起get请求
            async with session.get(
                url=task_url,
                headers = request_headers,
                timeout = request_timeout,
            ) as res:
                if res.status != 200:
                    # await 等待IO读取网页文本
                    # response.text() 属于网络IO 必须写await,否则拿到的携程对象不是网页内容
                    page_content = await res.text()
                    print(f"{task_url}访问成功,大小为{len(page_content)}字节")
                    return page_content
                else:
                    print(f"访问失败")
                    return None
        # 捕获全部网络相关异常:超时,断网,DNS解析失败
        except aiohttp.ClientError as e:
            print(f"网络异常,错误详情:{e}")
            return None
        # 兜底捕获其他未知异常,单个url失败程序不会中断
        except Exception as e:
            print(f"未知错误,错误详情:{e}")
            return None

这是异步爬虫的核心部分。函数使用async def定义,表明这是一个协程函数。函数接收两个参数:目标URL和全局会话对象。

async with sem这行代码是关键,它保证了每个请求都要先获取信号量,从而实现了并发数量的控制。当信号量被占用满时,新的协程会在这里阻塞等待。

在异步上下文中使用async with session.get()发起GET请求。与同步库不同,这里不需要手动关闭响应,异步上下文管理器会自动处理资源的释放。获取到响应后,通过await res.text()异步读取响应内容。这里的await是必需的,因为text()方法涉及到网络IO操作,如果不加await,得到的是一个协程对象而不是实际的文本内容。

异常处理方面,代码采用分层捕获策略。首先捕获aiohttp.ClientError,这涵盖了网络层面的各种异常如超时、连接失败等。然后再用Exception兜底,确保单个请求的失败不会影响整个爬虫的运行。

4. 主程序逻辑

python 复制代码
# 编写主程序函数
async def main():
    # 创建ClientSession会话对象
    # ClientSession 内置http连接池
    async with aiohttp.ClientSession() as session:
        # 爬取的url列表
        url_list = [f"https://www.aigei.com/game/index/?term=is_vip_false&page={page}"for page in range(5)]
        # 构建协程任务列表
        help_task =[]
        for url_one in url_list:
            # 调用协程函数,得到协程对象
            task_one = request_task(url_one,session)
            help_task.append(task_one)

        # asyncio.gather 并发执行全部协程
        # *help_task:解包列表,将列表中的每一个协程单独闯入gather
        all_results = await asyncio.gather(*help_task)
        # 失败返回None 成功统计数量
        success_results = [response for response in all_results if response is not None]
        print(f"爬取成功的页面数量:{len(success_results)}")
if __name__ == '__main__':
    asyncio.run(main())

主函数中,我们首先创建了ClientSession对象。这个会话对象非常关键,它内置了连接池,可以复用TCP连接,避免每次请求都重新建立连接,从而显著提高性能。

然后我们构建了一个包含5个页面的URL列表。通过循环调用request_task函数得到协程对象,并存入列表中。这里有一个重要概念需要理解:调用异步函数只是得到一个协程对象,并不会执行函数体内的代码。真正的执行需要将这个协程对象放到事件循环中运行。

asyncio.gather(*help_task)是并发执行的关键。*help_task将列表解包,让gather能够接收多个协程作为参数。gather会并发运行所有协程,并等待它们全部完成后返回结果列表。结果的顺序与传入协程的顺序一致。

最后我们过滤掉失败的请求(返回None的),统计成功爬取的数量。

5. 全部代码

python 复制代码
# 导入异步IO核心库
import asyncio
# 导入异步http请求库,替代同步的requests
import aiohttp
from textual.demo import page


# 防止请求太快封ip,设置最大并发数
max_request = 3
# 创建信号量对象,用来限制协程并发数量
sem = asyncio.Semaphore(max_request)
# 请求头,模拟浏览器访问
request_headers = {
    "User-Agent":"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/151.0.0.0 Safari/537.36"
}
# 请求时长超过10秒直接抛出异常
request_timeout = aiohttp.ClientTimeout(total=10)
# 定义单个爬虫协程函数 task_url:目标网址, session:全局复用的会话对象
async def request_task(task_url:str,session:aiohttp.ClientSession):
    # async+信号量:进入受保护的代码块,控制并发坑位
    async with sem:
        try:
            # async with 异步上下文管理器,发起get请求
            async with session.get(
                url=task_url,
                headers = request_headers,
                timeout = request_timeout,
            ) as res:
                if res.status != 200:
                    # await 等待IO读取网页文本
                    # response.text() 属于网络IO 必须写await,否则拿到的携程对象不是网页内容
                    page_content = await res.text()
                    print(f"{task_url}访问成功,大小为{len(page_content)}字节")
                    return page_content
                else:
                    print(f"访问失败")
                    return None
        # 捕获全部网络相关异常:超时,断网,DNS解析失败
        except aiohttp.ClientError as e:
            print(f"网络异常,错误详情:{e}")
            return None
        # 兜底捕获其他未知异常,单个url失败程序不会中断
        except Exception as e:
            print(f"未知错误,错误详情:{e}")
            return None
# 编写主程序函数
async def main():
    # 创建ClientSession会话对象
    # ClientSession 内置http连接池
    async with aiohttp.ClientSession() as session:
        # 爬取的url列表
        url_list = [f"https://www.aigei.com/game/index/?term=is_vip_false&page={page}"for page in range(5)]
        # 构建协程任务列表
        help_task =[]
        for url_one in url_list:
            # 调用协程函数,得到协程对象
            task_one = request_task(url_one,session)
            help_task.append(task_one)

        # asyncio.gather 并发执行全部协程
        # *help_task:解包列表,将列表中的每一个协程单独闯入gather
        all_results = await asyncio.gather(*help_task)
        # 失败返回None 成功统计数量
        success_results = [response for response in all_results if response is not None]
        print(f"爬取成功的页面数量:{len(success_results)}")
if __name__ == '__main__':
    asyncio.run(main())

异步编程的注意事项

在实际开发中,有几个容易踩坑的地方需要注意。

第一个是必须使用支持异步的库。在异步函数中不能使用requests库,因为它是同步阻塞的,会阻塞整个事件循环。必须使用aiohttp这类异步友好的库。

第二个是不要忘记await关键字。当调用异步函数或异步方法时,必须使用await才能获取实际的返回值。忘记await会导致得到一个协程对象,而不是预期的数据。

第三个是异常处理要全面。网络请求涉及的因素很多,DNS解析失败、连接超时、服务器返回错误码等等情况都要考虑。建议使用分层捕获的方式,先捕获特定的网络异常,再捕获通用异常作为兜底。

相关推荐
青禾8371 小时前
Linux 系统信息、权限管理与 Python 并发编程(协程与线程)完全指南
linux·python
caimouse1 小时前
ReactOS 图形系统分析(47):GDI 批处理 — gdibatch.c
c语言·开发语言
曲无忆1 小时前
密码学三大核心技术解析
python·密码学
qq21084629531 小时前
在python中什么是 self 和 cls?
开发语言·前端·python
nanawinona1 小时前
2026年手工思路量化后,工具重点会怎样变化
人工智能·python
杨充1 小时前
05.多用组合和少继承
开发语言·bash
今天AI了吗2 小时前
从聊天到委派:AI Agent 如何推进长期任务
数据库·人工智能·python·sql·rust
傻啦嘿哟2 小时前
王者荣耀爬虫:爬取全英雄皮肤数据,用Python做可视化分析
开发语言·爬虫·python
AI直播技术杂谈2 小时前
直播画面突然模糊了,排查了三个小时
开发语言·php