Python中关于使用协程(asyncio+aiohttp)进行爬虫的疑惑

由于需要爬取大量数据,所以采用 代理+协程(asyncio+aiohttp) 的方式爬虫获取数据。爬的时候有两点疑惑:
1. 协程的数量有上限吗?我已经把协程数量调到 300 了,但是服务器内存也没有显著增高。大家一般设置多少个协程爬取数据?(被爬的网站是个常用的大厂,肯定不会被我搞崩掉。我知道不节制的爬虫是不道德的,只是好奇协程数量可以到多大)
2. 爬虫的速度还不错,但是在总是会在协程结束前的最后几个待爬取网页卡住等很长时间才能抓到,这种情况正常吗?
Python中关于使用协程(asyncio+aiohttp)进行爬虫的疑惑

4 回复

内存开销主要就是 response 和 socket 吧,response 大小不确定,socket 估计 1G 内存能开将近 100 万个吧


帖子标题是关于使用协程(asyncio + aiohttp)进行爬虫的疑惑。这是一个非常经典且高效的爬虫架构。核心疑惑点通常在于如何正确组织异步代码、处理并发请求以及管理会话。

下面是一个完整、可直接运行的示例,它演示了如何使用 asyncioaiohttp 构建一个基础的异步爬虫。这个爬虫会并发获取多个网页的标题。

import asyncio
import aiohttp
from bs4 import BeautifulSoup

async def fetch_title(session, url):
    """
    异步获取单个URL的网页标题。
    """
    try:
        async with session.get(url, timeout=10) as response:
            # 确保请求成功
            response.raise_for_status()
            html = await response.text()
            # 使用BeautifulSoup解析HTML并提取标题
            soup = BeautifulSoup(html, 'html.parser')
            title = soup.title.string.strip() if soup.title else 'No title found'
            print(f"URL: {url} -> Title: {title}")
            return title
    except aiohttp.ClientError as e:
        print(f"请求 {url} 时发生客户端错误: {e}")
        return None
    except asyncio.TimeoutError:
        print(f"请求 {url} 超时")
        return None
    except Exception as e:
        print(f"处理 {url} 时发生未知错误: {e}")
        return None

async def main(urls):
    """
    主协程,创建一个aiohttp客户端会话,并发执行所有抓取任务。
    """
    # 创建一个TCP连接器,可以限制总连接数和每主机连接数,这对爬虫很重要
    connector = aiohttp.TCPConnector(limit=10, limit_per_host=2)
    # 在异步上下文管理器中创建会话
    async with aiohttp.ClientSession(connector=connector) as session:
        # 为每个URL创建一个fetch_title协程任务
        tasks = [fetch_title(session, url) for url in urls]
        # 使用asyncio.gather并发运行所有任务,并等待它们全部完成
        titles = await asyncio.gather(*tasks, return_exceptions=False)
        # 返回结果列表
        return titles

if __name__ == '__main__':
    # 要抓取的URL列表
    target_urls = [
        'https://www.python.org',
        'https://www.github.com',
        'https://stackoverflow.com',
        # 可以继续添加更多URL
    ]

    # 获取或创建事件循环并运行主协程
    # 在Python 3.7+中,可以直接使用asyncio.run(main(urls))
    # 这里使用更兼容的方式
    loop = asyncio.get_event_loop()
    try:
        all_titles = loop.run_until_complete(main(target_urls))
        print("\n所有任务完成。")
        # print(all_titles) # 可以打印出所有返回的标题列表
    finally:
        loop.close()

代码关键点解释:

  1. async def: 定义异步函数(协程)。协程内部可以使用 await 来挂起自身,等待耗时的I/O操作(如网络请求)完成,而不会阻塞整个事件循环。
  2. aiohttp.ClientSession: 这是核心对象,务必在异步上下文管理器 (async with) 中使用。 它为所有请求提供了一个高效的连接池,重用TCP连接,能极大提升性能。为会话配置 TCPConnector 可以控制并发度,避免对目标服务器造成过大压力或被封禁。
  3. asyncio.gather: 这是并发执行多个协程任务的主要方法。它接收一系列协程对象(或任务),并发运行它们,并返回一个包含所有结果的列表。return_exceptions=False 确保任何任务中的异常都会立即抛出,方便调试。
  4. 事件循环 (asyncio.runloop.run_until_complete): 这是异步程序的发动机。它负责调度和执行所有协程。在脚本入口点,我们需要运行主协程来启动整个异步流程。Python 3.7+ 推荐使用 asyncio.run(main()),它负责创建、运行和关闭循环。

常见疑惑解答:

  • requests + ThreadPoolExecutor 的区别? 异步模型在I/O密集型任务(如爬虫)中效率更高。它用单线程在等待响应时切换任务,避免了多线程的上下文切换开销和GIL限制,能轻松管理成千上万个并发连接,而线程池通常受限于线程数量。
  • 如何控制并发速度? 除了上面提到的 TCPConnector(limit=...),还可以使用 asyncio.Semaphore 信号量来更精细地控制同时运行的任务数,或者在任务中加入 await asyncio.sleep(delay) 来避免请求过于频繁。
  • 遇到SSL/证书错误? 可以在创建 ClientSession 时传入 connector=aiohttp.TCPConnector(ssl=False),但不推荐用于生产环境,这会降低安全性。更好的方法是确保系统证书有效或指定证书路径。

总结建议:理解事件循环和 await 的协作是关键。

超时分链接超时和读取超时,有的网站你访问的时候直接给你一个超长时间的读取…

协程的开销是函数级的,很小

回到顶部