一、同步与异步:为何效率天差地别?
在开始代码之前,理解其背后的理念至关重要。
- 同步爬虫(阻塞式): 程序发送一个HTTP请求后,会一直“傻等”直到服务器返回响应。在此期间,CPU资源被闲置。就像一个收银员一次只服务一位顾客,结账、装袋、收款,完成后才服务下一位。队伍长时,总等待时间非常可观。
- 异步爬虫(非阻塞式): 程序发送一个请求后,不会原地等待,而是立刻去执行其他任务(例如发送下一个请求)。当某个请求的响应返回时,程序再回来处理它。这就像一个收银员同时服务多条队伍,一个顾客在掏钱时,他立刻为下一个顾客扫描商品,极大地提高了效率。
Python的**** **<font style="color:rgb(15, 17, 21);background-color:rgb(235, 238, 242);">asyncio</font>** 库提供了构建异步程序的底层基础设施。而 **<font style="color:rgb(15, 17, 21);background-color:rgb(235, 238, 242);">Aiohttp</font>** 则是基于 <font style="color:rgb(15, 17, 21);background-color:rgb(235, 238, 242);">asyncio</font> 的,专门用于处理HTTP请求的库,它使得编写异步爬虫变得异常简单和高效。
二、项目实战:构建异步星座运势爬虫
我们的目标是并发地抓取一个假设的星座网站上的12个星座运势。
1. 环境准备
首先,确保安装了必要的库。<font style="color:rgb(15, 17, 21);background-color:rgb(235, 238, 242);">aiohttp</font> 是核心,<font style="color:rgb(15, 17, 21);background-color:rgb(235, 238, 242);">asyncio</font> 是Python的内置库。我们还会使用 <font style="color:rgb(15, 17, 21);background-color:rgb(235, 238, 242);">beautifulsoup4</font> 来解析HTML。
2. 目标分析
假设我们的目标URL结构为:<font style="color:rgb(15, 17, 21);background-color:rgb(235, 238, 242);">https://example-astrology.com/aquarius.html</font>。每个星座一个页面,我们可以构建一个URL列表。
3. 代码实现
以下是完整的、带有详细注释的代码实现。
import aiohttp
import asyncio
from bs4 import BeautifulSoup
import time
import logging
# 配置日志,方便观察异步执行过程
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
logger = logging.getLogger(__name__)
# 目标星座列表和基础URL(此为示例,请替换为真实可用的URL)
ZODIAC_SIGNS = [
'aries', 'taurus', 'gemini', 'cancer', 'leo', 'virgo',
'libra', 'scorpio', 'sagittarius', 'capricorn', 'aquarius', 'pisces'
]
BASE_URL = "https://example-astrology.com/{sign}.html"
# 定义异步函数:获取并解析单个星座的页面
async def fetch_zodiac_forecast(session, sign):
"""
使用传入的aiohttp session异步抓取单个星座的运势。
Args:
session: aiohttp.ClientSession 实例,用于发起请求。
sign: 星座名称字符串。
Returns:
dict: 包含星座名称和运势文本的字典。
"""
url = BASE_URL.format(sign=sign)
try:
logger.info(f"正在发起请求: {sign}")
# 发起异步GET请求,with语句确保响应被正确关闭
async with session.get(url) as response:
# 确保请求成功,否则抛出异常
response.raise_for_status()
# 异步读取响应内容
html = await response.text()
# 使用BeautifulSoup同步解析HTML(注意:这里是同步操作)
# 由于解析通常很快,对整体性能影响不大。如果解析极复杂,可考虑异步方式。
soup = BeautifulSoup(html, 'html.parser')
# 假设运势文本在一个 <div class='forecast'> 标签内
# 请根据实际网页结构修改此选择器
forecast_element = soup.find('div', class_='forecast')
forecast_text = forecast_element.get_text(strip=True) if forecast_element else "运势未找到"
logger.info(f"成功抓取: {sign}")
return {
'sign': sign,
'forecast': forecast_text
}
except aiohttp.ClientError as e:
logger.error(f"请求星座 {sign} 时发生错误: {e}")
return {
'sign': sign,
'forecast': f"抓取失败: {str(e)}"
}
except Exception as e:
logger.error(f"解析星座 {sign} 时发生未知错误: {e}")
return {
'sign': sign,
'forecast': f"解析失败: {str(e)}"
}
# 定义主异步函数:创建任务并并发执行
async def main():
"""
主函数,创建会话和所有抓取任务,并并发执行。
"""
# 创建一个TCPConnector,可以限制总连接数和每台主机的连接数,避免对服务器造成过大压力
connector = aiohttp.TCPConnector(limit=10) # 限制同时连接数为10
# 创建一个ClientSession,所有请求共享这个session的连接池和其他资源
async with aiohttp.ClientSession(connector=connector) as session:
# 为每个星座创建一个异步任务(Task)
tasks = []
for sign in ZODIAC_SIGNS:
# 创建task对象,但此时尚未执行
task = asyncio.create_task(fetch_zodiac_forecast(session, sign))
tasks.append(task)
logger.info(f"已创建 {len(tasks)} 个异步任务,开始并发执行...")
# 使用 asyncio.gather 并发运行所有任务,并等待它们全部完成
# gather会返回一个结果列表,顺序与tasks列表一致
results = await asyncio.gather(*tasks)
# 所有任务完成后,打印结果
logger.info("所有任务已完成!以下是抓取结果:")
print("\n" + "="*50)
for result in results:
print(f"【{result['sign'].upper()}】")
print(f"运势:{result['forecast']}")
print("-" * 30)
# 程序入口点
if __name__ == "__main__":
start_time = time.time() # 记录开始时间
# 获取或创建事件循环并运行主程序
# asyncio.run() 是Python 3.7+的推荐方式,它负责管理事件循环
asyncio.run(main())
end_time = time.time() # 记录结束时间
elapsed_time = end_time - start_time
print(f"\n>>> 总耗时: {elapsed_time:.2f} 秒 <<<")
三、代码核心解析与最佳实践
**<font style="color:rgb(15, 17, 21);background-color:rgb(235, 238, 242);">async with aiohttp.ClientSession()</font>**:- 这是整个异步爬虫的核心。
<font style="color:rgb(15, 17, 21);background-color:rgb(235, 238, 242);">ClientSession</font>是一个连接池,它会在内部复用TCP连接,而不是为每个请求都创建新的连接。这极大地减少了建立和断开连接的开销。 <font style="color:rgb(15, 17, 21);background-color:rgb(235, 238, 242);">TCPConnector(limit=10)</font>用于控制并发量,既保护了目标网站,也避免了我们自己的程序因连接数过多而出现问题。
- 这是整个异步爬虫的核心。
**<font style="color:rgb(15, 17, 21);background-color:rgb(235, 238, 242);">asyncio.create_task()</font>**:- 它将要运行的协程(
<font style="color:rgb(15, 17, 21);background-color:rgb(235, 238, 242);">fetch_zodiac_forecast</font>)包装成一个<font style="color:rgb(15, 17, 21);background-color:rgb(235, 238, 242);">Task</font>对象,并立即将其加入事件循环等待调度。此时函数并不会马上执行,只是做好了准备。
- 它将要运行的协程(
**<font style="color:rgb(15, 17, 21);background-color:rgb(235, 238, 242);">await asyncio.gather(*tasks)</font>**:- 这是并发执行的关键。
<font style="color:rgb(15, 17, 21);background-color:rgb(235, 238, 242);">gather</font>等待所有传入的<font style="color:rgb(15, 17, 21);background-color:rgb(235, 238, 242);">Task</font>完成,并收集它们的结果。<font style="color:rgb(15, 17, 21);background-color:rgb(235, 238, 242);">*tasks</font>是解包操作,将任务列表展开为多个参数。 - 所有任务在此处真正开始并发执行。当某个任务在等待网络响应时,事件循环会立即切换到其他就绪的任务。
- 这是并发执行的关键。
- 错误处理:
- 在异步环境中,一个任务的异常不会直接导致整个程序崩溃,但可能会被
<font style="color:rgb(15, 17, 21);background-color:rgb(235, 238, 242);">gather</font>收集或抛出。因此,在每个独立的任务函数内部进行完善的<font style="color:rgb(15, 17, 21);background-color:rgb(235, 238, 242);">try...except</font>捕获至关重要,这确保了即使某个星座抓取失败,也不会影响其他任务的执行。
- 在异步环境中,一个任务的异常不会直接导致整个程序崩溃,但可能会被
- 性能对比:
- 你可以尝试将
<font style="color:rgb(15, 17, 21);background-color:rgb(235, 238, 242);">fetch_zodiac_forecast</font>函数和主逻辑用<font style="color:rgb(15, 17, 21);background-color:rgb(235, 238, 242);">requests</font>库重写为同步版本。你会发现,异步版本的耗时接近于最慢的那个单个请求的耗时(例如,如果每个请求都耗时1秒,异步总耗时约1秒),而同步版本的总耗时则是所有请求耗时的总和(12秒)。
- 你可以尝试将
四、总结与拓展
通过本次实践,我们成功构建了一个高效、健壮的异步星座运势爬虫。<font style="color:rgb(15, 17, 21);background-color:rgb(235, 238, 242);">Aiohttp</font> 与 <font style="color:rgb(15, 17, 21);background-color:rgb(235, 238, 242);">asyncio</font> 的结合,将I/O密集型任务的性能提升了一个数量级。
在实际应用中,你还可以考虑以下拓展方向:
- 速率限制: 使用
<font style="color:rgb(15, 17, 21);background-color:rgb(235, 238, 242);">asyncio.sleep()</font>在请求间加入延迟,做一个有道德的爬虫。 - 代理IP池: 在
<font style="color:rgb(15, 17, 21);background-color:rgb(235, 238, 242);">ClientSession</font>中集成代理IP,用于应对反爬策略。例如:www.16yun.cn/ - 更复杂的解析: 如果解析成为瓶颈,可以将解析工作丢到线程池中执行,避免阻塞事件循环。
- 数据存储: 将结果异步地写入数据库(如
<font style="color:rgb(15, 17, 21);background-color:rgb(235, 238, 242);">aiomysql</font>用于MySQL,<font style="color:rgb(15, 17, 21);background-color:rgb(235, 238, 242);">asyncpg</font>用于PostgreSQL)。