python3网络爬虫开发实战 第2版:使用aiohttp

复制代码
import asyncio
import aiohttp
import time

start = time.time()

async def get(url):
    session = aiohttp.ClientSession()
    response = await session.get(url)
    await response.text()
    await session.close()
    return response
async def request():
    url = 'https://www.httpbin.org/delay/5'
    print('Waiting for',url)
    response = await get(url)
    print('Get response from',url,'response',response)
tasks = [asyncio.ensure_future(request()) for _ in range(10)]
loop = asyncio.get_event_loop()
loop.run_until_complete(asyncio.wait(tasks))

end = time.time()
print('Cost time:',end - start)

aiohttp流程分析:

1. session = aiohttp.ClientSession()

这行代码的作用是创建一个 aiohttp 库的客户端会话实例 session ,该实例是发起异步 HTTP 请求的核心载体,后续的 GET 请求都通过这个会话对象执行。

  • 该会话会管理底层的连接池、TCP 连接复用等资源,是 aiohttp 异步 HTTP 请求的基础入口。
  • 此处仅完成实例化,尚未建立实际的网络连接,网络操作会在后续的 get 方法中触发。
2. response = await session.get(url)

这行代码是发起异步的 HTTP GET 请求,并等待请求完成以获取响应对象 response

  • 关键字 await 表明这是一个可等待(awaitable)的异步操作,会暂停当前协程的执行,直到 GET 请求完成(获取到服务端响应或请求失败),再恢复协程执行并返回响应对象。
  • 执行该方法时,会利用第一步创建的会话 session 管理的资源,建立网络连接、发送 GET 请求、接收服务端响应头,最终返回封装了响应状态、响应头、响应体等信息的 ClientResponse 实例。
  • 此时仅获取了响应的元数据(状态码、响应头等),响应体(正文内容)并未被完整读取到内存中,只是建立了读取响应体的通道。
3. await response.text()

这行代码是异步读取并解析 HTTP 响应的正文内容为字符串格式 ,但存在一个关键特点:仅执行了读取操作,却没有将读取到的字符串结果进行保存或使用

  • response.text() 是异步方法,需要通过 await 等待读取完成,它会自动根据响应头中的 Content-Type 推断编码格式(默认 utf-8),将二进制的响应体数据解码为字符串。
  • 该方法执行时,才会完整读取服务端返回的响应体数据到内存中,但由于代码中没有赋值给任何变量(如 content = await response.text()),读取完成后的字符串结果会被立即丢弃,后续无法访问该响应内容。
  • 补充:若响应体较大,该方法会将全部内容加载到内存,此处仅体现 "读取但不保存" 的行为特征。
4. await session.close()

这行代码是异步关闭之前创建的 ClientSession 会话实例,释放该会话占用的所有资源

  • 会话关闭操作是异步的,需要 await 等待关闭完成,确保资源(连接池、空闲 TCP 连接、文件句柄等)被正确释放,避免资源泄露。
  • 关闭后的 session 实例无法再用于发起新的 HTTP 请求,若后续尝试调用 session.get() 等方法,会抛出异常。

代码优化分析:

首先,这几行代码是 Python 3.7 及更早版本的异步任务执行写法,在 Python 3.7+ 中提供了更简洁、更推荐的现代写法,同时需要先明确原代码的核心功能:创建 10 个异步任务,提交到事件循环并等待所有任务执行完成。

优化方案(分版本,推荐现代写法)
方案 1:Python 3.7+ 推荐(简洁、优雅,无需手动操作事件循环)

使用 asyncio.run() 替代手动获取 / 管理事件循环,这是 Python 3.7 引入的高层级 API,会自动完成「创建事件循环 → 运行任务 → 关闭事件循环」的全流程,避免手动操作的遗漏。

同时,原代码中的 asyncio.ensure_future() 也可替换为 asyncio.create_task()(Python 3.7+),语义更清晰,专门用于创建异步任务。

优化后的对应代码:

复制代码
# 替代原有的 3 行异步任务执行代码
async def main():
    # 创建10个异步任务(替代 asyncio.ensure_future())
    tasks = [asyncio.create_task(request()) for _ in range(10)]
    # 等待所有任务执行完成(等价于原 asyncio.wait(tasks))
    await asyncio.wait(tasks)

# 自动管理事件循环,运行主协程
asyncio.run(main())
方案 2:兼容 Python 3.7 以下(优化原写法,更严谨)

若需兼容低版本,原写法可优化两点:

  1. 手动获取事件循环后,最终关闭事件循环(原代码缺失 loop.close(),可能导致资源泄露)
  2. 保持 asyncio.ensure_future(),完善资源回收

优化后的对应代码:

复制代码
tasks = [asyncio.ensure_future(request()) for _ in range(10)]
loop = asyncio.get_event_loop()
try:
    loop.run_until_complete(asyncio.wait(tasks))
finally:
    # 新增:关闭事件循环,释放资源
    loop.close()
完整优化后代码(Python 3.7+ 完整版)

结合上述优化,同时保留原代码的核心业务逻辑,完整代码如下:

复制代码
import asyncio
import aiohttp
import time

start = time.time()

async def get(url):
    session = aiohttp.ClientSession()
    response = await session.get(url)
    await response.text()
    await session.close()
    return response

async def request():
    url = 'https://www.httpbin.org/delay/5'
    print('Waiting for', url)
    response = await get(url)
    print('Get response from', url, 'response', response)

# 新增主协程,统一管理任务
async def main():
    tasks = [asyncio.create_task(request()) for _ in range(10)]
    await asyncio.wait(tasks)

if __name__ == "__main__":
    # 现代异步任务执行入口
    asyncio.run(main())
    
    end = time.time()
    print('Cost time:', end - start)
关键优化点解析
  1. asyncio.run() 的核心优势

    • 高层级 API,无需手动调用 asyncio.get_event_loop()loop.run_until_complete()loop.close()
    • 自动处理事件循环的创建与销毁,避免资源泄露
    • 语义清晰,一行代码完成异步任务的入口执行,是 Python 3.7+ 异步编程的标准入口
  2. asyncio.create_task()asyncio.ensure_future() 的区别

    • 两者均可创建异步任务,返回 Task 对象
    • asyncio.create_task()(Python 3.7+):语义更明确,专门用于「创建新任务」,推荐优先使用
    • asyncio.ensure_future():兼容更早版本,不仅能创建任务,还能将任意可等待对象(awaitable)包装为 Future 对象,适用场景更广泛

总结

  1. Python 3.7+ 优先使用 asyncio.run() + asyncio.create_task(),代码更简洁、更安全
  2. asyncio.run() 自动管理事件循环,避免手动操作的遗漏
  3. 低版本兼容可保留原写法,补充 loop.close() 完善资源回收
  4. 优化后的异步任务执行代码,功能与原代码一致,但可读性和健壮性更强

代码升级:

复制代码
import aiohttp
import asyncio


async def fetch(session,url):
    async with session.get(url) as response:
        return await response.text(),response.status

async def main():
    async with aiohttp.ClientSession() as session:
        html,status = await fetch(session,'https://cuiqingcai.com')
        print(f'html:{html[:100]}...')
        print(f'status:{status}')

if __name__=='__main__':
    loop = asyncio.get_event_loop()
    loop.run_until_complete(main())

关键知识点:

1、异步场景下的 async with...as

async with,这是 Python 3.5+ 引入的异步上下文管理器 ,专门用于异步编程场景,与普通 with...as 功能一致(自动管理异步资源),但有明显区别:

核心区别
特性 普通 with...as 异步 async with...as
适用场景 同步编程(普通函数) 异步编程(async def 异步函数)
实现协议 实现 __enter__()__exit__() 实现 __aenter__()__aexit__()(异步魔法方法)
关键字 withas async withas(多了 async 关键字)
执行方式 同步执行(阻塞) 异步执行(不阻塞事件循环)

2、理解 html,status = await fetch(session,'https://cuiqingcai.com') 这行代码的含义、语法

一、整体语法拆解:多重核心语法的组合

这行代码同时包含了 3 个 Python 核心语法,缺一不可,整体作用是获取异步函数 fetch 的返回结果,并分别赋值给 htmlstatus 两个变量

  1. 解包赋值(元组解包):html, status = ...
  2. 异步等待关键字:await
  3. 函数调用:fetch(session,'https://cuiqingcai.com')
二、逐部分详解

1. 右侧:await fetch(session,'https://cuiqingcai.com')

(1)fetch(session,'https://cuiqingcai.com'):调用异步函数

fetch异步函数(用 async def 声明),这里传入了两个参数:

  • 第一个参数 sessionaiohttp.ClientSession() 实例(异步 HTTP 会话对象),用于发起异步 HTTP 请求,保证请求的高效性和会话复用。
  • 第二个参数 'https://cuiqingcai.com':目标请求 URL,即要获取内容的网页地址。

⚠️ 注意:直接调用异步函数(不加 await),不会执行函数内部的逻辑,只会返回一个 coroutine(协程对象),无法得到函数的实际返回结果。

(2)await:等待异步函数执行完成并获取返回值

await 是 Python 异步编程中的核心关键字,它的使用有严格限制(只能在 async def 声明的异步函数内部使用 ,这里 main 函数正是 async def 声明的,符合要求)。

它的核心作用是:

  • 暂停当前异步函数(main)的执行流程,不会阻塞整个事件循环(这是异步编程高效的关键)。
  • 等待被调用的异步函数(fetch)执行完毕,直到获取到 fetch 的实际返回结果。
  • fetch 执行完成后,恢复 main 函数的执行流程,继续向下执行后续代码(打印 htmlstatus)。

简单说:await 就是 "等待异步函数执行完成,拿到它的返回值",且不会阻塞其他异步任务的执行。

2. 左侧:html, status = ...:元组解包赋值(序列解包)

这是 Python 的 ** 元组解包(也叫序列解包)** 语法,专门用于将一个可迭代对象(这里是 fetch 函数返回的元组)的元素,分别赋值给多个变量,要求「变量个数」与「可迭代对象的元素个数」完全一致。

对应到 fetch 函数的返回值:

复制代码
# fetch 函数的返回语句,返回了两个值,本质上是一个隐式元组 (response.text(), response.status)
return await response.text(), response.status
  • Python 中,多个值用逗号分隔返回时,会自动打包成一个 元组(tuple) (即使没有加小括号 ())。
  • 左侧的 htmlstatus 两个变量,会按照顺序依次接收元组中的两个元素:
    • 第一个变量 html:接收 await response.text() 的结果(目标网页的完整 HTML 源代码,字符串类型)。
    • 第二个变量 status:接收 response.status 的结果(HTTP 响应状态码,整数类型,例如 200 表示请求成功,404 表示页面不存在)。

等价于显式元组解包(更易理解):

复制代码
# 先获取 fetch 返回的元组
response_result = await fetch(session,'https://cuiqingcai.com')
# 再解包赋值
html = response_result[0]
status = response_result[1]
相关推荐
m0_672656542 小时前
JavaScript性能优化实战技术文章大纲
开发语言·javascript·性能优化
Yang-Never2 小时前
Android 内存泄漏 -> LiveData如何解决ViewMode和Activity/Fragment之间的内存泄漏
android·java·开发语言·kotlin·android studio
Smartdaili China2 小时前
如何在桌面和移动设备上修复YouTube错误400
开发语言·php·error·youtube·移动·住宅ip·错误400
持梦远方2 小时前
持梦行文本编辑器(cmyfEdit):架构设计与十大核心功能实现详解
开发语言·数据结构·c++·算法·microsoft·visual studio
HeDongDong-2 小时前
Kotlin 协程(Coroutines)详解
android·开发语言·kotlin
阿里嘎多学长2 小时前
2025-12-29 GitHub 热点项目精选
开发语言·程序员·github·代码托管
dagouaofei2 小时前
写 2026 年工作计划,用 AI 生成 PPT 哪种方式更高效
人工智能·python·powerpoint
鹿角片ljp2 小时前
深入理解Java集合框架:核心接口与实现解析
java·开发语言·windows
Hello.Reader2 小时前
Flink ML OneHotEncoder 把类别索引变成稀疏 one-hot 向量
python·机器学习·flink