异步爬虫之aiohttp的使用

在上一篇博客我们介绍了异步爬虫的基本原理和 asyncio 的基本用法,并且在最后简单提及了使用aiohttp 实现网页爬取的过程。本篇博客我们介绍一下 aiohttp 的常见用法。

基本介绍

前面介绍的 asyncio模块,其内部实现了对 TCP、UDP、SSL协议的异步操作,但是对于 HTTP 请求来说,就需要用 aiohttp 实现了。

aiohttp 是一个基于 asyncio 的异步 HTTP 网络模块,它既提供了服务端,又提供了客户端。其中我们用服务端可以搭建一个支持异步处理的服务器,这个服务器就是用来处理请求并返回响应的,类似于 Django、Flask、Tormado 等一些 Web服务器。而客户端可以用来发起请求,类似于使用 requests 发起一个 HTTP 请求然后获得响应,但 requests 发起的是同步的网络请求,aiohttp 则是异步的。

本篇博客我们主要了解一下 aiohttp 客户端部分的用法。

基本实例

我们来看一个基本的 aiohttp 请求案例,代码如下:

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())

这里使用 aiohttp 爬取了本书作者的个人博客,获得了源码和响应状态码,并打印出来,运行结果如下:

html:<!DOCTYPE html>
<html lang="zh-CN">

<head>
  <meta charset="UTF-8">
  <meta name="viewport" content...
status:200

由于网页源码过长,这里只截取了输出的一部分。可以看到,我们成功获取了网页的源代码及响应状态码 200,也就是完成了一次基本的 HTTP请求,即我们成功使用 aiohttp 通过异步的方式完成了网页爬取。当然,这个操作用之前讲的 requests 也可以做到。

能够发现,aiohttp 的请求方法的定义和之前有明显的区别,主要包括如下几点。

  • 首先在导人库的时候,除了必须引人 aiohttp这个库,还必须引人asyncio 库。因为要实现异步爬取,需要启动协程,而协程则需要借助于asyncio 里面的事件循环才能执行。除了事件循环asyncio 里面也提供了很多基础的异步操作。
  • 异步爬取方法的定义和之前有所不同,每个异步方法的前面都要统一加 async 来修饰。
  • with as 语句前面同样需要加 async 来修饰。在 Pvthon 中,with as 语句用于声明一个上下文管理器,能够帮我们自动分配和释放资源。而在异步方法中,withas前面加上async代表声明一个支持异步的上下文管理器。
  • 对于一些返回协程对象的操作,前面需要加 await 来修饰。例如 response 调用 text 方法,查询 API可以发现,其返回的是协程对象,那么前面就要加 await;而对于状态码来说,其返回值就是一个数值,因此前面不需要加 await。所以,这里可以按照实际情况做处理,参考官方文档说明,看看其对应的返回值是怎样的类型,然后决定加不加await 就可以了。
  • 最后,定义完爬取方法之后,实际上是 main 方法调用了 fetch 方法。要运行的话,必须启用事件循环,而事件循环需要使用 asyncio库,然后调用 run_until_complete 方法来运行。

注意:在 Python 3.7 及以后的版本中,我们可以使用 asyncio.run(main())代替最后的启动操作,不需要显示声明事件循环,run 方法内部会自动启动一个事件循环。但这里为了兼容更多的Python 版本,依然显式声明了事件循环。

URL参数设置

对于 URL 参数的设置,我们可以借助 params 参数,传入一个字典即可,实例如下:

import aiohttp
import asyncio


async def main():
    params = {'name': 'germey', 'age': 25}
    async with aiohttp.ClientSession() as session:
        async with session.get('https://www.httpbin.org/get', params=params) as response:
            print(await response.text())


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

运行结果如下:

{
  "args": {
    "age": "25", 
    "name": "germey"
  }, 
  "headers": {
    "Accept": "*/*", 
    "Accept-Encoding": "gzip, deflate", 
    "Host": "www.httpbin.org", 
    "User-Agent": "Python/3.9 aiohttp/3.11.11", 
    "X-Amzn-Trace-Id": "Root=1-677217f5-3bc6954d23f245465ccbb806"
  }, 
  "origin": "111.18.92.1", 
  "url": "https://www.httpbin.org/get?name=germey&age=25"
}

这里可以看到,实际请求的 URL 为 https://www.httpbin.org/get?name=germey&age-25,其中的参数对应于 params 的内容。

其他请求类型

aiohttp 还支持其他请求类型,如 POST、PUT、DELETE 等,这些和 requests 的使用方式有点类
似,实例如下:

session.post('http://www.httpbin.org/post',data=b'data')
session.put('http://www.httpbin.org/put', data=b'data')
session.delete('http://www.httpbin.org/delete')
session.head("http://www.httpbin.org/get')
session.options('http://www.httpbin.org/get')
session.patch('http://www.httpbin.org/patch', data=b'data')

要使用这些方法,只需要把对应的方法和参数替换一下。

POST请求

对于 POST表单提交,其对应的请求头中的Content-Type为application/x-www-form-urlencoded.我们可以用如下方式来实现:

import aiohttp
import asyncio


async def main():
    data = {'name': 'germey', 'age': 25}
    async with aiohttp.ClientSession() as session:
        async with session.post('https://www.httpbin.org/post', data=data) as response:
            print(await response.text())


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

运行结果如下:

{
  "args": {}, 
  "data": "", 
  "files": {}, 
  "form": {
    "age": "25", 
    "name": "germey"
  }, 
  "headers": {
    "Accept": "*/*", 
    "Accept-Encoding": "gzip, deflate", 
    "Content-Length": "18", 
    "Content-Type": "application/x-www-form-urlencoded", 
    "Host": "www.httpbin.org", 
    "User-Agent": "Python/3.9 aiohttp/3.11.11", 
    "X-Amzn-Trace-Id": "Root=1-67721981-6891ead2152d4e70567cd9ab"
  }, 
  "json": null, 
  "origin": "111.18.92.1", 
  "url": "https://www.httpbin.org/post"
}

对于 POST JSON 数据提交,其对应的请求头中的 Content-Type 为 application/json,我们只需要将 post 方法里的 data 参数改成 json 即可,实例代码如下:

import aiohttp
import asyncio


async def main():
    data = {'name': 'germey', 'age': 25}
    async with aiohttp.ClientSession() as session:
        async with session.post('https://www.httpbin.org/post', json=data) as response:
            print(await response.text())


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

运行结果如下:

{
  "args": {}, 
  "data": "{\"name\": \"germey\", \"age\": 25}", 
  "files": {}, 
  "form": {}, 
  "headers": {
    "Accept": "*/*", 
    "Accept-Encoding": "gzip, deflate", 
    "Content-Length": "29", 
    "Content-Type": "application/json", 
    "Host": "www.httpbin.org", 
    "User-Agent": "Python/3.9 aiohttp/3.11.11", 
    "X-Amzn-Trace-Id": "Root=1-67721ce2-0439155d5dce282c3803cca6"
  }, 
  "json": {
    "age": 25, 
    "name": "germey"
  }, 
  "origin": "111.18.92.1", 
  "url": "https://www.httpbin.org/post"
}

可以发现,其实现也和requests 非常像,不同的参数支持不同类型的请求内容。

响应

对于响应来说,我们可以用如下方法分别获取其中的状态码、响应头、响应体、响应体二进制内容、响应体 JSON 结果,实例代码如下:

import aiohttp
import asyncio


async def main():
    data = {'name': 'germey', 'age': 25}
    async with aiohttp.ClientSession() as session:
        async with session.post('https://www.httpbin.org/post', data=data) as response:
            print('status:', response.status)
            print('headers:', response.headers)
            print('body:', await response.text())
            print('bytes:', await response.read())
            print('json:', await response.json())


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

运行结果如下:

status: 200
headers: <CIMultiDictProxy('Date': 'Mon, 30 Dec 2024 04:40:46 GMT', 'Content-Type': 'application/json', 'Content-Length': '510', 'Connection': 'keep-alive', 'Server': 'gunicorn/19.9.0', 'Access-Control-Allow-Origin': '*', 'Access-Control-Allow-Credentials': 'true')>
body: {
  "args": {}, 
  "data": "", 
  "files": {}, 
  "form": {
    "age": "25", 
    "name": "germey"
  }, 
  "headers": {
    "Accept": "*/*", 
    "Accept-Encoding": "gzip, deflate", 
    "Content-Length": "18", 
    "Content-Type": "application/x-www-form-urlencoded", 
    "Host": "www.httpbin.org", 
    "User-Agent": "Python/3.9 aiohttp/3.11.11", 
    "X-Amzn-Trace-Id": "Root=1-6772244e-191cbb78229cbd5412777470"
  }, 
  "json": null, 
  "origin": "111.18.92.1", 
  "url": "https://www.httpbin.org/post"
}

bytes: b'{\n  "args": {}, \n  "data": "", \n  "files": {}, \n  "form": {\n    "age": "25", \n    "name": "germey"\n  }, \n  "headers": {\n    "Accept": "*/*", \n    "Accept-Encoding": "gzip, deflate", \n    "Content-Length": "18", \n    "Content-Type": "application/x-www-form-urlencoded", \n    "Host": "www.httpbin.org", \n    "User-Agent": "Python/3.9 aiohttp/3.11.11", \n    "X-Amzn-Trace-Id": "Root=1-6772244e-191cbb78229cbd5412777470"\n  }, \n  "json": null, \n  "origin": "111.18.92.1", \n  "url": "https://www.httpbin.org/post"\n}\n'
json: {'args': {}, 'data': '', 'files': {}, 'form': {'age': '25', 'name': 'germey'}, 'headers': {'Accept': '*/*', 'Accept-Encoding': 'gzip, deflate', 'Content-Length': '18', 'Content-Type': 'application/x-www-form-urlencoded', 'Host': 'www.httpbin.org', 'User-Agent': 'Python/3.9 aiohttp/3.11.11', 'X-Amzn-Trace-Id': 'Root=1-6772244e-191cbb78229cbd5412777470'}, 'json': None, 'origin': '111.18.92.1', 'url': 'https://www.httpbin.org/post'}

可以看到,这里有些字段前面需要加 await,有些则不需要。其原则是,如果返回的是一个协程对象(如 async修饰的方法),那么前面就要加 await,具体可以看 aiohttp的API,其链接为:https://docs.aiohttp.org/en/stable/client_reference.html.

超时设置

我们可以借助 clientTimeout 对象设置超时,例如要设置1秒的超时时间,可以这么实现:

import aiohttp
import asyncio


async def main():
    timeout = aiohttp.ClientTimeout(total=1)
    async with aiohttp.ClientSession(timeout=timeout) as session:
        async with session.get('https://www.httpbin.org/get') as response:
            print('status:', response.status)


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

如果在1秒之内成功获取响应,那么运行结果如下:

200

如果超时,则会抛出 TimeoutError 异常,其类型为 asyncio.TimeoutError,我们进行异常捕获即可。另外,声明clientTimeout对象时还有其他参数,如connect、socket、connect等,详细可以参考官方文档:https://docs.aiohttp.org/en/stable/client_quickstart.html#timeouts。

并发限制

由于 aiohttp 可以支持非常高的并发量,如几万、十万、百万都是能做到的,但面对如此高的并发量,目标网站很可能无法在短时间内响应,而且有瞬间将目标网站爬挂掉的危险,这提示我们需要控制一下爬取的并发量。

一般情况下,可以借助 asyncio 的 Semaphore 来控制并发量,实例代码如下:

import aiohttp
import asyncio

CONCURRENCY = 5
URL = 'https://www.baidu.com'
semaphore = asyncio.Semaphore(CONCURRENCY)
session = None


async def scrape_api():
    async with semaphore:
        print('scraping', URL)
    async with session.get(URL) as response:
        await asyncio.sleep(1)
        return await response.text()


async def main():
    global session
    session = aiohttp.ClientSession()
    scrape_index_tasks = [asyncio.ensure_future(scrape_api()) for _ in range(10000)]
    await asyncio.gather(*scrape_index_tasks)


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

参考文献

https://docs.aiohttp.org/

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/946802.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

基于微信小程序的校园点餐平台的设计与实现(源码+SQL+LW+部署讲解)

文章目录 摘 要1. 第1章 选题背景及研究意义1.1 选题背景1.2 研究意义1.3 论文结构安排 2. 第2章 相关开发技术2.1 前端技术2.2 后端技术2.3 数据库技术 3. 第3章 可行性及需求分析3.1 可行性分析3.2 系统需求分析 4. 第4章 系统概要设计4.1 系统功能模块设计4.2 数据库设计 5.…

安卓入门十一 常用网络协议四

MQTT&#xff08;Message Queuing Telemetry Transport&#xff09; MQTT是一种轻量级的、发布/订阅模式的消息传输协议。它被设计用于在低带宽或不稳定网络环境下&#xff0c;实现物联网设备之间的可靠通信。 4.1 MQTT详细介绍 发布/订阅模式&#xff1a;MQTT 使用发布/订…

前端多个项目部署在同一个nginx下,前缀不同,配置编写方式

我们前端是微前端的项目&#xff0c;不同模块是分开的不同项目&#xff0c;用访问前缀区分。开发环境部署为了节约资源&#xff0c;直接使用一个nginx当做静态资源服务器&#xff0c;服务多个微前端&#xff0c;示意图如下&#xff1a; 下面是nginx使用的配置(server部分) ser…

Yolo11 基于DroneVehicle数据集的无人机视角下车辆目标检测

1、关于DroneVehicle数据集介绍 DroneVenicle数据集是由天津大学收集、标注的大型无人机航拍车辆数据集。 DroneVehicle 数据集由无人机采集的共 56,878 幅图像组成&#xff0c;其中一半为 RGB 图像&#xff0c;其余为红外图像。我们对五个类别进行了带有方向性边界框的丰富标…

Requests库01|使用Requests库发送 get/post/put/delete请求

学习目标&#xff1a; 能够使用Requests库发送 get/post/put/delete请求&#xff0c;获取响应状态码、数据能够使用UnitTest管理测试用例。 目录 一、Requests库安装和简介 二、设置http请求语法&#xff08;重要&#xff09; 三、应用案例&#xff08;重要&#xff09; …

[有用教程]从 Pixel 快速传输到 Android

概括 更换新手机很容易&#xff0c;但数据迁移却不容易。目前&#xff0c;用户喜欢转换品牌&#xff0c;应用市场上的转换工具也越来越多。然而&#xff0c;它们并不都是安全的。因此&#xff0c;选择一款简单、安全的迁移工具至关重要。 今天我们将讨论如何从 Pixel 转移到 …

【蓝桥杯研究生组】第15届Java试题答案整理

D 题 试题 D: 商品库存管理 时间限制: 3.0s 内存限制: 512.0MB 本题总分&#xff1a;10 分 【问题描述】 在库存管理系统中&#xff0c;跟踪和调节商品库存量是关键任务之一。小蓝经营的仓库中存有多种商品&#xff0c;这些商品根据类别和规格被有序地分类并编号&#xff0c;…

BUUCTF sqli-labs 1

这里就是单纯的找一下flag在哪&#xff0c;通关整个靶场在sql注入分区&#xff0c;虽然还没有通关。 这里要先看一下数据库都有哪些&#xff0c;用到语句&#xff1a;?id-1 union select 1,(select group_concat(schema_name) from information_schema.schemata),3-- 发现这个…

python实现自动登录12306抢票 -- selenium

提示&#xff1a;文章写完后&#xff0c;目录可以自动生成&#xff0c;如何生成可参考右边的帮助文档 python实现自动登录12306抢票 -- selenium 前言其实网上也出现了很多12306的代码&#xff0c;但是都不是最新的&#xff0c;我也是从网上找别人的帖子&#xff0c;看B站视频&…

Spring自动化创建脚本-解放繁琐的初始化配置!!!(自动化SSM整合)

一、实现功能(原创&#xff0c;转载请告知) 1.自动配置pom配置文件 2.自动识别数据库及数据表&#xff0c;创建Entity、Dao、Service、Controller等 3.自动创建database.properties、mybatis-config.xml等数据库文件 4.自动创建spring-dao.xml spring-mvc.xml …

[微服务] - MQ高级

在昨天的练习作业中&#xff0c;我们改造了余额支付功能&#xff0c;在支付成功后利用RabbitMQ通知交易服务&#xff0c;更新业务订单状态为已支付。 但是大家思考一下&#xff0c;如果这里MQ通知失败&#xff0c;支付服务中支付流水显示支付成功&#xff0c;而交易服务中的订单…

MySQL(面试题 - 同类型归纳面试题)

目录 一、MySQL 数据类型 1. 数据库存储日期格式时&#xff0c;如何考虑时区转换问题&#xff1f; 2. Blob和text有什么区别&#xff1f; 3. mysql里记录货币用什么字段类型比较好&#xff1f; 4. MySQL如何获取当前日期&#xff1f; 5. 你们数据库是否支持emoji表情存储…

aws(学习笔记第二十一课) 开发lambda应用程序

aws(学习笔记第二十一课) 开发lambda应用程序 学习内容&#xff1a; lambda的整体概念开发lambda应用程序 1. lambda的整体概念 借助AWS Lambda&#xff0c;无需预置或管理服务器即可运行代码。只需为使用的计算时间付费。借助 Lambda&#xff0c;可以为几乎任何类型的应用进…

【优选算法】查找总价格为目标值的两个商品

链接&#xff1a;LCR 179. 查找总价格为目标值的两个商品 - 力扣&#xff08;LeetCode&#xff09; 解法&#xff1a;利用单调性&#xff0c;使用双指针算法解决问题 1.先从小到大排序 2. sum > t : right--; sum < t : left; sum t : return class Solution {public…

VUE echarts 教程二 折线堆叠图

VUE echarts 教程一 折线图 import * as echarts from echarts;var chartDom document.getElementById(main); var myChart echarts.init(chartDom); var option {title: {text: Stacked Line},tooltip: {trigger: axis},legend: {data: [Email, Union Ads, Video Ads, Dir…

bilibili 哔哩哔哩小游戏SDK接入

小游戏的文档 简介 bilibili小游戏bilibili小游戏具有便捷、轻量、免安装的特点。游戏包由云端托管&#xff0c;在哔哩哔哩APP内投放和运行&#xff0c;体验流畅&#xff0c;安全可靠。https://miniapp.bilibili.com/small-game-doc/guide/intro/ 没想过接入这个sdk比ios还难…

2024年中国新能源汽车用车发展怎么样 PaperGPT(二)

用车趋势深入分析 接上文&#xff0c;2024年中国新能源汽车用车发展怎么样 PaperGPT&#xff08;一&#xff09;-CSDN博客本文将继续深入探讨新能源汽车的用车强度、充电行为以及充电设施的现状。 用车强度 月均行驶里程&#xff1a;2024年纯电车辆月均行驶超过1500公里&…

自从学会Git,感觉打开了一扇新大门

“同事让我用 Git 提交代码&#xff0c;我居然直接把项目文件压缩发过去了……”相信很多初学者都经历过类似的窘境。而当你真正掌握 Git 时&#xff0c;才会发现它就像一本魔法书&#xff0c;轻松解决代码管理的种种难题。 为什么 Git 能成为程序员的标配工具&#xff1f;它究…

简易屏幕共享工具-基于WebSocket

前面写了两个简单的屏幕共享工具&#xff0c;不过那只是为了验证通过截屏的方式是否可行&#xff0c;因为通常手动截屏的频率很低&#xff0c;而对于视频来说它的帧率要求就很高了&#xff0c;至少要一秒30帧率左右。所以&#xff0c;经过实际的截屏工具验证&#xff0c;我了解…

yakit-靶场-高级前端加解密与验签实战(for嵌套纯享版)

高级前端加解密与验签实战 一、前端验证签名&#xff08;验签&#xff09;表单&#xff1a;HMAC-SHA256 使用hmac-sha256的十六进制key值可以加密 与页面加密后的值相同 热加载&#xff1a; encryptData func(p) { //sha256key值key codec.DecodeHex("313233343132333…