当前位置: 首页 > article >正文

异步爬虫之协程的基本原理

我们知道爬虫是 IO 密集型任务,例如使用 requests 库来爬取某个站点,当发出一个请求后,程序必须等待网站返回响应,才能接着运行,而在等待响应的过程中,整个爬虫程序是一直在等待的,实际上没有做任何事情。对于这种情况,我们有没有优化方案呢?

当然有,本篇博客我们就来了解一下异步爬虫的基本概念和实现。

要实现异步机制的爬虫,那自然和协程脱不了关系。

案例引入

在介绍协程之前,先来看一个案例网站,地址为 https://www.httpbin.org/delay/5,访问这个链接需要先等待五秒才能得到结果,这是因为服务器强制等待了5秒时间才返回响应。

平时我们浏览网页的时候,绝大部分网页的响应速度还是很快的,如果写爬虫来爬取,那么从发出请求到接收响应的时间不会很长,因此需要我们等待的时间并不多。

然而像上面这个网站,发出一次请求至少需要5秒才能得到响应,如果用requests 库写爬虫来爬取,那么每次都要等待5秒及以上才能拿到结果。

下面来测试一下,我们用requests 写一个遍历程序,直接遍历100 次案例网站,试试看有什么效果,实现代码如下:

import requests
import logging
import time

logging.basicConfig(level=logging.INFO, format='%(asctime)s-%(levelname)s:%(message)s')
TOTAL_NUMBER = 100
URL = 'https://www.httpbin.org/delay/5'
start_time = time.time()
for _ in range(1, TOTAL_NUMBER + 1):
    logging.info('scraping %s', URL)
    response = requests.get(URL)
end_time = time.time()
logging.info('total time %s seconds', end_time - start_time)

这里我们直接用循环的方式构造了100个请求,使用的是requests 单线程,在爬取之前和爬取之后分别记录了时间,最后输出了爬取 100个页面消耗的总时间。

运行结果如下:

2024-12-29 14:16:19,061-INFO:scraping https://www.httpbin.org/delay/5
2024-12-29 14:16:25,566-INFO:scraping https://www.httpbin.org/delay/5
2024-12-29 14:16:31,690-INFO:scraping https://www.httpbin.org/delay/5
2024-12-29 14:16:37,881-INFO:scraping https://www.httpbin.org/delay/5
2024-12-29 14:16:44,076-INFO:scraping https://www.httpbin.org/delay/5
...
2024-12-29 14:26:59,819-INFO:scraping https://www.httpbin.org/delay/5
2024-12-29 14:27:06,561-INFO:scraping https://www.httpbin.org/delay/5
2024-12-29 14:27:13,264-INFO:total time 654.2034454345703 seconds

由于每个页面都至少要等待5秒才能加载出来,因此100个页面至少要花费500秒时间,加上网站本身的负载问题,总的爬取时间最终约为654 秒,大约11分钟。

这在实际情况中是很常见的,有些网站本身加载速度就比较慢,稍慢的可能1~3秒,更慢的说不定10秒以上。如果我们就用requests单线程这么爬取,总耗时将会非常大。此时要是打开多线程或多进程来爬取,其爬取速度确实会成倍提升,那么是否有更好的解决方案呢?

本节就来了解一下使用协程实现加速的方法,这种方法对IO密集型任务非常有效。如过将其应用到网络爬虫中,那么爬取效率甚至可以提升成百倍。

基础知识

了解协程需要先了解一些基础概念,如阻塞和非阻塞、同步和异步、多进程和协程。

  • 阻塞

阻塞状态指程序未得到所需计算资源时被挂起的状态。程序在等待某个操作完成期间,自身无法继续干别的事情,则称该程序在该操作上是阻塞的。

常见的阻塞形式有:网络 IO 阻塞、磁盘 IO阻塞、用户输入阻塞等。阻塞是无处不在的,包括在 CPU切换上下文时,所有进程都无法真正干事情,它们也会被阻塞。在多核 CPU 的情况下,正在执行上下文切换操作的核不可被利用。

  • 非阻塞

程序在等待某操作的过程中,自身不被阻塞,可以继续干别的事情,则称该程序在该操作上是非阻塞的。

非阻塞并不是在任何程序级别、任何情况下都存在的。仅当程序封装的级别可以囊括独立的子程序单元时,程序才可能存在非阻塞状态。

非阻塞因阻塞的存在而存在,正因为阻塞导致程序运行的耗时增加与效率低下,我们才要把它变成非阻塞的。

  • 同步

不同程序单元为了共同完成某个任务,在执行过程中需要靠某种通信方式保持协调一致,此时这些程序单元是同步执行的。

例如在购物系统中更新商品库存时,需要用“行锁”作为通信信号,强制让不同的更新请求排队并按顺序执行,这里的更新库存操作就是同步的。

简言之,同步意味着有序

  • 异步

为了完成某个任务,有时不同程序单元之间无须通信协调也能完成任务,此时不相关的程序单元之间可以是异步的。

例如,爬取下载网页。调度程序调用下载程序后,即可调度其他任务,无须与该下载任务保持通信以协调行为。不同网页的下载、保存等操作都是无关的,也无须相互通知协调。这些异步操作的完成时刻并不确定。

简言之,异步意味着无序

  • 多进程

多进程就是利用 CPU 的多核优势,在同一时间并行执行多个任务,可以大大提高执行效率。

  • 协程

协程,英文叫作 coroutine,又称微线程、纤程,是一种运行在用户态的轻量级线程。

协程拥有自己的寄存器上下文和栈。协程在调度切换时,将寄存器上下文和栈保存到其他地方,等切回来的时候,再恢复先前保存的寄存器上下文和栈。因此,协程能保留上一次调用时的状态,即所有局部状态的一个特定组合,每次过程重人,就相当于进人上一次调用的状态。

协程本质上是个单进程,相对于多进程来说,它没有线程上下文切换的开销,没有原子操作锁定及同步的开销,编程模型也非常简单。

我们可以使用协程来实现异步操作,例如在网络爬虫场景下,我们发出一个请求之后,需要等待一定时间才能得到响应,但其实在这个等待过程中,程序可以干许多其他事情,等得到响应之后再切换回来继续处理,这样可以充分利用CPU和其他资源,这就是协程的优势。

协程的用法

接下来,我们了解一下协程的实现。从Python3.4开始,Python 中加入了协程的概念,但这个版本的协程还是以生成器对象为基础。Python 3.5 中增加了 async、await,使得协程的实现更为方便。

Python 中使用协程最常用的库莫过于 asyncio,所以本节会以它为基础来介绍协程的用法。

首先,需要了解下面几个概念。

  • event loop:事件循环,相当于一个无限循环,我们可以把一些函数注册到这个事件循环上,当满足发生条件的时候,就调用对应的处理方法。

  • coroutine:中文翻译叫协程,在Python 中常指代协程对象类型,我们可以将协程对象注册到事件循环中,它会被事件循环调用。我们可以使用async 关键字来定义一个方法,这个方法在调用时不会立即被执行,而是会返回一个协程对象。

  • task:任务,这是对协程对象的进一步封装,包含协程对象的各个状态。

  • future:代表将来执行或者没有执行的任务的结果,实际上和task 没有本质区别。

另外,我们还需要了解 async、await 关键字,它们是从 Python3.5才开始出现的,专门用于定义协程。其中,前者用来定义一个协程,后者用来挂起阻塞方法的执行。

准备工作

在本节开始之前,请确保安装的 Python 版本为 3.5 及以上,如果版本是 3.4 及以下,则下方的案例是不能运行的。

定义协程

我们来定义一个协程,体验一下它和普通进程在实现上的不同之处,代码如下:

import asyncio


async def execute(x):
    print('Number:', x)


coroutine = execute(1)
print('Coroutine:', coroutine)
print('After calling execute')
loop = asyncio.get_event_loop()
loop.run_until_complete(coroutine)
print('After calling loop')

运行结果如下:

Coroutine: <coroutine object execute at 0x000001F01B840740>
After calling execute
Number: 1
After calling loop

首先,我们引人了 asyncio 包,这样才可以使用 async 和 await 关键字。然后使用 async 定义了一个 execute 方法,该方法接收一个数字参数x,执行之后会打印这个数字。

随后我们直接调用了 execute 方法,然而这个方法并没有执行,而是返回了一个 coroutine 协程对象。之后我们使用 get_event_loop 方法创建了一个事件循环 loop,并调用 loop 对象的run_until_complete 方法将协程对象注册到了事件循环中,接着启动。最后,我们才看到 execute 方法打印出了接收的数字。

可见,async 定义的方法会变成一个无法直接执行的协程对象,必须将此对象注册到事件循环中才可以执行。

前面我们还提到了 task,它是对协程对象的进一步封装,比协程对象多了运行状态,例如 running、finished 等,我们可以利用这些状态获取协程对象的执行情况。

在上面的例子中,当我们把协程对象 coroutine 传递给 run_until_complete 方法的时候,实际上它进行了一个操作,就是将 coroutine 封装成task对象。对此,我们也可以显式地进行声明,代码如下所示:

import asyncio


async def execute(x):
    print('Number:', x)
    return x


coroutine = execute(1)
print('Coroutine:', coroutine)
print('After calling execute')
loop = asyncio.get_event_loop()
task = loop.create_task(coroutine)
print('Task:', task)
loop.run_until_complete(task)
print('Task:', task)
print('After calling loop')

运行结果如下:

Coroutine: <coroutine object execute at 0x000001D0096E0740>
After calling execute
Task: <Task pending name='Task-1' coro=<execute() running at D:\projects\scrapy-demo\test.py:4>>
Number: 1
Task: <Task finished name='Task-1' coro=<execute() done, defined at D:\projects\scrapy-demo\test.py:4> result=1>
After calling loop

这里我们定义了 loop 对象之后,紧接着调用了它的create_task方法,将协程对象转化为 task 对象,随后打印输出一下,发现它处于 pending 状态。然后将 task对象添加到事件循环中执行,并再次打印出 task 对象,发现它的状态变成了 finished,同时还可以看到其 result 变成了 1,也就是我们定义的 execute 方法的返回结果。

定义 task 对象还有另外一种方式,就是直接调用 asyncio 包的 ensure_future 方法,返回结果也是 task对象,这样的话我们就可以不借助 loop 对象。即使还没有声明 loop,也可以提前定义好 task对象,这种方式的写法如下:

import asyncio


async def execute(x):
    print('Number:', x)
    return x


coroutine = execute(1)
print('Coroutine:', coroutine)
print('After calling execute')
loop = asyncio.get_event_loop()
task = asyncio.ensure_future(coroutine)
print('Task:', task)
loop.run_until_complete(task)
print('Task:', task)
print('After calling loop')

运行结果如下:

Coroutine: <coroutine object execute at 0x00000295CC910740>
After calling execute
Task: <Task pending name='Task-1' coro=<execute() running at D:\projects\scrapy-demo\test.py:4>>
Number: 1
Task: <Task finished name='Task-1' coro=<execute() done, defined at D:\projects\scrapy-demo\test.py:4> result=1>
After calling loop

可以发现,运行效果都是一样的。

绑定回调

我们也可以为某个 task 对象绑定一个回调方法。来看下面这个例子:

import asyncio
import requests


async def request():
    url = 'https://www.baidu.com'
    status = requests.get(url)
    return status


def callback(task):
    print('Status:', task.result())


coroutine = request()
task = asyncio.ensure_future(coroutine)
task.add_done_callback(callback)
print('Task:', task)
loop = asyncio.get_event_loop()
loop.run_until_complete(task)
print('Task:', task)

这里我们定义了 request 方法,在这个方法里请求了百度,并获取了其状态码,但是没有编写任何 print 语句。随后我们定义了 callback 方法,这个方法接收一个参数,参数是 task 对象,在这个方法中调用 print 方法打印出了 task对象的结果。这样就定义好了一个协程对象和一个回调方法。我们现在希望达到的效果是,当协程对象执行完毕之后,就去执行声明的callback 方法。

那么两者怎样关联起来呢?很简单,只要调用 add_done_callback方法就行。我们将 callback 方法传递给封装好的 task对象,这样当task执行完毕之后,就可以调用 callback方法了。同时 task对象还会作为参数传递给 callback 方法,调用task 对象的result 方法就可以获取返回结果了。

运行结果如下:

Task: <Task pending name='Task-1' coro=<request() running at D:\projects\scrapy-demo\test.py:5> cb=[callback() at D:\projects\scrapy-demo\test.py:11]>
Status: <Response [200]>
Task: <Task finished name='Task-1' coro=<request() done, defined at D:\projects\scrapy-demo\test.py:5> result=<Response [200]>>

实际上,即使不使用回调方法,在 task运行完毕之后,也可以直接调用result 方法获取结果代码如下所示:

import asyncio
import requests


async def request():
    url = 'https://www.baidu.com'
    status = requests.get(url)
    return status


coroutine = request()
task = asyncio.ensure_future(coroutine)
print('Task:', task)
loop = asyncio.get_event_loop()
loop.run_until_complete(task)
print('Task:', task)
print('Task Result:', task.result())

运行结果是一样的:

Task: <Task pending name='Task-1' coro=<request() running at D:\projects\scrapy-demo\test.py:5>>
Task: <Task finished name='Task-1' coro=<request() done, defined at D:\projects\scrapy-demo\test.py:5> result=<Response [200]>>
Task Result: <Response [200]>

多任务协程

在上面的例子中,我们都只执行了一次请求,如果想执行多次请求,应该怎么办呢?可以定义一个 task 列表,然后使用 asyncio 包中的 wait 方法执行。看下面的例子:

import asyncio
import requests


async def request():
    url = 'https://www.baidu.com'
    status = requests.get(url)
    return status


tasks = [asyncio.ensure_future(request()) for _ in range(5)]
print('Tasks:', tasks)
loop = asyncio.get_event_loop()
loop.run_until_complete(asyncio.wait(tasks))

for task in tasks:
    print('Task Result:', task.result())

这里我们使用一个 for 循环创建了5个 task,它们组成一个列表,然后把这个列表首先传递给asyncio 包的 wait 方法,再将其注册到事件循环中,就可以发起5个任务了。最后,输出任务的执行结果,具体如下:

Tasks: [<Task pending name='Task-1' coro=<request() running at D:\projects\scrapy-demo\test.py:5>>, <Task pending name='Task-2' coro=<request() running at D:\projects\scrapy-demo\test.py:5>>, <Task pending name='Task-3' coro=<request() running at D:\projects\scrapy-demo\test.py:5>>, <Task pending name='Task-4' coro=<request() running at D:\projects\scrapy-demo\test.py:5>>, <Task pending name='Task-5' coro=<request() running at D:\projects\scrapy-demo\test.py:5>>]
Task Result: <Response [200]>
Task Result: <Response [200]>
Task Result: <Response [200]>
Task Result: <Response [200]>
Task Result: <Response [200]>

可以看到,5个任务被顺次执行,并得到了执行结果。

协程实现

前面说了好一通,又是 async 关键字,又是 coroutine,又是task,又是 callback 的,似乎并没有从中看出协程的优势,反而写法上更加奇怪和麻烦了?别急,上述案例只是为后面的使用作铺垫。接下来,我们正式看看协程在解决 IO 密集型任务方面到底有怎样的优势。

在前面的代码中,我们用一个网络请求作为例子,这本身就是一个耗时等待操作,因为在请求网页之后需要等待页面响应并返回结果。耗时等待操作一般都是IO操作,例如文件读取、网络请求等。协程在处理这种操作时是有很大优势的,当遇到需要等待的情况时,程序可以暂时挂起,转而执行其他操作,从而避免因一直等待一个程序而耗费过多的时间,能够充分利用资源。

为了表现协程的优势,我们还是以本节开头介绍的网站 https://www.httpbin.org/delay/5 为例,因为该网站响应比较慢,所以可以通过爬取时间让大家直观感受到爬取速度的提升。

为了让大家更好地理解协程的正确使用方法,这里先来看看大家使用协程时常犯的错误,后面再给出正确的例子作为对比。

首先,还是拿之前的requests 库进行网页请求,之后再重新使用上面的方法请求一遍:

import asyncio
import requests
import time

start = time.time()


async def request():
    url = 'https://www.httpbin.org/delay/5'
    print('Waiting for', url)
    response = requests.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)

这里我们还是创建了 10个 task,然后将 task 列表传给 wait 方法并注册到事件循环中执行。

运行结果如下:

Waiting for https://www.httpbin.org/delay/5
Get response from https://www.httpbin.org/delay/5 response <Response [200]>
Waiting for https://www.httpbin.org/delay/5
Get response from https://www.httpbin.org/delay/5 response <Response [200]>
...
Waiting for https://www.httpbin.org/delay/5
Get response from https://www.httpbin.org/delay/5 response <Response [200]>
Cost time: 76.17514300346375

可以发现,这和正常的请求并没有什么区别,各个任务依然是顺次执行的,耗时66秒,平均一个请求耗时 6.6秒,说好的异步处理呢?

其实,要实现异步处理,先得有挂起操作,当一个任务需要等待IO 结果的时候,可以挂起当前任务,转而执行其他任务,这样才能充分利用好资源。上面的方法都是一本正经地串行执行下来,连个挂起都没有,怎么可能实现异步?莫不是想太多了。

要实现异步,我们再了解一下 await 关键字的用法,它可以将耗时等待的操作挂起,让出控制权。如果协程在执行的时候遇到 await,事件循环就会将本协程挂起,转而执行别的协程,直到其他协程挂起或执行完毕。

所以,我们可能会将代码中的request 方法改成如下这样:

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

仅仅是在 requests 前面加了一个关键字 await。然而此时执行代码,会得到如下报错信息:

Waiting for https://www.httpbin.org/delay/5
...
Cost time: 70.10251641273499
Task exception was never retrieved
future: <Task finished name='Task-1' coro=<request() done, defined at D:\projects\scrapy-demo\test.py:8> exception=TypeError("object Response can't be used in 'await' expression")>
Traceback (most recent call last):
  File "D:\projects\scrapy-demo\test.py", line 11, in request
    response = await requests.get(url)
TypeError: object Response can't be used in 'await' expression

这次协程遇到 await 时确实挂起了,也等待了,但是最后却报出以上错误信息。这个错误的意思是requests 返回的 Response 对象不能和 await 一起使用,为什么呢?因为根据官方文档说明,await后面的对象必须是如下格式之一:

  • 一个原生协程对象;
  • 一个由types.coroutine 修饰的生成器,这个生成器可以返回协程对象;
  • 由一个包含 await 方法的对象返回的一个迭代器。

这里 regeusts 返回的 Response 对象以上三种格式都不符合,因此报出了上面的错误。

有的读者可能已经发现,既然 await 后面可以跟一个协程对象,那么 async 把请求的方法改成协程对象不就可以了吗?于是就代码被改写成如下的样子:

import asyncio
import requests
import time

start = time.time()


async def get(url):
    return requests.get(url)


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)

这里将请求页面的方法独立出来,并用 async 修饰,就得到了一个协程对象。运行一下看看:

Waiting for https://www.httpbin.org/delay/5
Get response from https://www.httpbin.org/delay/5 response <Response [200]>
...
Get response from https://www.httpbin.org/delay/5 response <Response [200]>
Waiting for https://www.httpbin.org/delay/5
Get response from https://www.httpbin.org/delay/5 response <Response [200]>
Cost time: 77.01466417312622

协程还不是异步执行的,也就是说我们仅仅将涉及IO 操作的代码封装到 async 修饰的方法里是不可行的。只有使用支持异步操作的请求方式才可以实现真正的异步,这里 aiohttp 就派上用场了。

使用 aiohttp

aiohttp 是一个支持异步请求的库,它和 asyncio 配合使用,可以使我们非常方便地实现异步请求操作。
我们使用 pip3 安装即可:

pip3 install aiohttp

aiohtp 的官方文档链接为 https://aiohttp.readthedocs.io/,它分为两部分,一部分是 Client,一部分
是 Server。

下面我们将 aiohttp 投入使用,将代码改写成如下样子:

import asyncio
import aiohttp
import requests
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)

这里将请求库由requests 改成了 aiohttp,利用 aiohttp库里 ClientSession 类的get 方法进行请求返回结果如下:

Waiting for https://www.httpbin.org/delay/5
Waiting for https://www.httpbin.org/delay/5
...
Get response from https://www.httpbin.org/delay/5 response <ClientResponse(https://www.httpbin.org/delay/5) [200 OK]>
<CIMultiDictProxy('Date': 'Sun, 29 Dec 2024 15:34:39 GMT', 'Content-Type': 'application/json', 'Content-Length': '367', 'Connection': 'keep-alive', 'Server': 'gunicorn/19.9.0', 'Access-Control-Allow-Origin': '*', 'Access-Control-Allow-Credentials': 'true')>

Cost time: 8.169023752212524

成功了!我们发现这次请求的耗时直接由76秒变成了8秒,耗费时间减少了非常多。这里我们使用了 await,其后面跟着 get 方法。在执行 10 个协程的时候,如果遇到 await,就会将当前协程挂起,转而执行其他协程,直到其他协程也挂起或执行完毕,再执行下一个协程。

开始运行时,事件循环会运行第一个 task。对于第一个 task 来说,当执行到第一个 await 跟着的 get 方法时,它会被挂起,但这个get方法第一步的执行是非阻塞的,挂起之后会立马被唤醒。立即又进人执行,并创建了 clientSession对象。接着遇到第二个 await,调用 session.get 请求方法,然后就被挂起了。由于请求需要耗时很久,所以一直没有被唤醒,好在第一个 task 被挂起了那么接下来该怎么办呢?事件循环会寻找当前未被挂起的协程继续执行,于是转而去执行第二个task,流程操作和第一个 task 也是一样的,以此类推,直到执行第十个 task 的 session.get 方法之后,全部的 task 都被挂起了。所有 task 都已经处于挂起状态,那怎么办?只好等待了。5 秒之后,几个请求几乎同时有了响应,然后几个 task 也被唤醒接着执行,并输出请求结果,最后总耗时是8秒!

怎么样?这就是异步操作的便捷之处,当遇到阻塞式操作时,task被挂起,程序接着去执行其他task,而不是傻傻地等着,这样可以充分利用 CPU,而不必把时间浪费在等待IO 上。

有人会说,在上面的例子中,发出网络请求后,接下来的5秒都是在等待,这5秒之内,CPU可以处理的 task数量远不止这些,既然这样的话,那么我们放 10个、20个、50个、100个、1000个 task一起执行,最后得到所有结果的耗时不都是差不多的吗?因为这些任务被挂起后都是一起等待的。

从理论上来说,确实是这样,不过有个前提,就是服务器即使在同一时刻接收无限次请求,依然要能保证正常返回结果,也就是服务器应该无限抗压,另外还要忽略 0 传输时延。满足了这两点,确实可以做到无限个 task 一起执行,并且在预想时间内得到结果。但由于不同服务器处理 task 的实现机制不同,可能某些服务器并不能承受那么高的并发量,因此响应速度也会减慢。

这里我们以百度为例,测试一下并发量分别为1、3、5、10、…、500 时的耗时情况,代码如下:

import asyncio
import time

import aiohttp


def test(number):
    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.baidu.com'
        await get(url)

    tasks = [asyncio.ensure_future(request()) for _ in range(number)]
    loop = asyncio.get_event_loop()
    loop.run_until_complete(asyncio.wait(tasks))
    end = time.time()
    print('Number:', number, 'Cost time:', end - start)


for number in [1, 3, 5, 10, 15, 30, 50]:
    test(number)

运行结果如下:

Number: 1 Cost time: 0.16419363021850586
Number: 3 Cost time: 0.13008475303649902
Number: 5 Cost time: 0.13481712341308594
Number: 10 Cost time: 0.14773201942443848
Number: 15 Cost time: 0.1420140266418457
Number: 30 Cost time: 0.15010547637939453
Number: 50 Cost time: 0.17553138732910156

可以看到,在服务器能够承受高并发的前提下,即使我们增加了并发量,其爬取速度也几乎不会太受影响。

综上所述,使用了异步请求之后,我们几乎可以在相同时间内实现成百上千倍次的网络请求,把这个运用在爬虫中,速度提升可谓非常可观。


http://www.kler.cn/a/457800.html

相关文章:

  • PyTorch快速入门教程【小土堆】之完整模型训练套路
  • 安卓系统主板_迷你安卓主板定制开发_联发科MTK安卓主板方案
  • LeetCode - 初级算法 数组(只出现一次的数字)
  • 数据中台与数据治理服务方案[50页PPT]
  • 关于easy-es对时间范围查询遇到的小bug
  • Go语言学习路线
  • 基于Android的校园导航系统
  • 科技驱动|暴雨信息赋能金融行业数智升级
  • RCCL/NCCL中的Transports方式选择:P2P or SHM or NET
  • 部署SenseVoice
  • React 组件的通信方式
  • RAG挑战及其解决方案:实践中的应对策略
  • 嵌入式单片机窗口看门狗控制与实现
  • 【AIGC-ChatGPT副业提示词指令 - 动图】命运织图:一个创新的个人发展分析工具
  • Ajax笔记
  • SCAU高程期末课后习题复习(只放了易错自用)
  • 探索城市空中交通的未来:基于Python的仿真与优化
  • Zookeeper JavaAPI操作(Curator API常用操作)
  • 探索 JavaBean(实体类)的奇妙世界
  • VSCode outline显示异常的解决方法——清除VSCode的配置和用户文件
  • AI生成PPT,效率革命的新时代
  • 数据的简单处理——pandas模块——查看数据
  • RedisDesktopManager新版本不再支持SSH连接远程redis后
  • PCIe和DMA:数据传输的“双子星“
  • 集成学习(模型融合)
  • 解锁仓颉编程语言的奥秘:枚举类型、模式匹配与类接口全解析