在 Python 的协程中使用 `await` 来等待 I/O 操作的完成,其原理是协程在 I/O 操作执行期间将会被挂起,阻塞占用的 CPU 资源,直到 I/O 操作完成后再从挂起的地方恢复协程的执行。
在协程中使用 `await` 操作会将协程从事件循环中移除,让出 CPU 资源,等待 I/O 操作完成后再重新加入到事件循环中。当调用 `await` 操作时,协程会自动创建一个 `Future` 对象,并将其返回。这个 `Future` 对象的状态是 `PENDING`,表示当前操作正在等待处理。当 I/O 操作完成后,操作系统会向 Python 解释器发送一个通知,然后通过事件循环将 `Future` 对象状态设置为 `DONE`,表示操作已完成。这会唤醒挂起的协程并让其继续执行。
在协程中使用 `await` 操作来等待 I/O 操作的完成,避免了线程阻塞的问题,提高了 I/O 操作的效率和并发性能,让程序能够更好地利用 CPU 资源。同时,协程和事件循环的机制也能够让程序的编写和维护更加简单明了,避免了复杂的线程同步和锁机制,也让代码的可读性和可维护性更高。
简单爬虫代码案例:
import asyncio
import aiohttp
from asyncio.queues import Queue
# 声明一些常量
MAX_TASKS = 100
TIMEOUT = 10
class Crawler:
def __init__(self):
self._seen_urls = set()
self._queue = Queue()
self._session = None
async def crawl(self, urls):
tasks = []
# 创建链接会话
self._session = aiohttp.ClientSession()
# 初始化队列
for url in urls:
await self._queue.put(url)
# 创建任务
for i in range(MAX_TASKS):
task = asyncio.create_task(self._worker())
tasks.append(task)
# 等待任务完成
await self._queue.join()
# 取消任务
for task in tasks:
task.cancel()
# 关闭链接会话
await self._session.close()
async def _fetch(self, url):
try:
async with self._session.get(url, timeout=TIMEOUT) as response:
if response.status == 200:
content = await response.text()
self._seen_urls.add(url)
return content
except Exception as e:
print(f"Error fetching {url}: {e}")
return None
async def _worker(self):
while True:
# 获取并处理队列中的 URL
url = await self._queue.get()
# 如果 URL 已经被处理过了,就跳过
if url in self._seen_urls:
self._queue.task_done()
continue
# 抓取 URL
html = await self._fetch(url)
if html is not None:
# 处理 HTML,提取所需的数据
# ...
# 将新发现的 URL 放入队列
urls = self._extract_links(html)
for new_url in urls:
await self._queue.put(new_url)
# 标记这个 URL 已经处理过了
self._queue.task_done()
def _extract_links(self, html):
# 从 HTML 中提取链接
# ...
return []
该示例程序通过使用 asyncio 和 aiohttp,实现了异步并发的爬虫功能,并且使用了队列来管理已访问的 URL,以避免重复访问。程序中的 Crawler
类定义了以下方法:
__init__()
: 构造函数,初始化一些数据结构crawl(urls)
: 爬虫程序的入口函数,接收一个 URL 列表并开始爬取_fetch(url)
: 异步获取 URL 对应的网页内容_worker()
: 异步处理队列中的 URL,并从其中提取新的 URL 放入队列_extract_links(html)
: 从 HTML 中提取链接
程序首先通过创建 Queue
对象创建了一个队列,然后将要访问的 URL 添加到了队列中。程序同时控制了最大的并行任务数,防止太多的并发 HTTP 请求导致网站负载暴涨。接着,程序使用了多个协程( _worker()
函数)并行地从队列中获取 URL,进行访问和处理。每个协程会从队列中获取一个 URL,如果这个 URL 已经被访问过就跳过,否则就使用 _fetch()
函数异步获取网页内容,提取所需的数据,并将新的 URL 放入队列中。当队列为空时,协程退出。最后,等待所有的队列都完成,并将取消所有的任务。
线程池版本:
import asyncio
import aiohttp
import concurrent.futures
from asyncio.queues import Queue
# 声明一些常量
MAX_TASKS = 100
MAX_THREADS = 10
TIMEOUT = 10
class Crawler:
def __init__(self):
self._seen_urls = set()
self._queue = Queue()
self._session = None
async def crawl(self, urls):
tasks = []
# 创建链接会话
self._session = aiohttp.ClientSession()
# 初始化队列
for url in urls:
await self._queue.put(url)
# 创建任务
for i in range(MAX_TASKS):
task = asyncio.create_task(self._worker())
tasks.append(task)
# 设置线程池
executor = concurrent.futures.ThreadPoolExecutor(max_workers=MAX_THREADS)
# 等待任务完成
await self._queue.join()
# 取消任务
for task in tasks:
task.cancel()
# 关闭链接会话
await self._session.close()
async def _fetch(self, url):
try:
async with self._session.get(url, timeout=TIMEOUT) as response:
if response.status == 200:
content = await response.text()
self._seen_urls.add(url)
return content
except Exception as e:
print(f"Error fetching {url}: {e}")
return None
async def _worker(self):
loop = asyncio.get_running_loop()
while True:
# 获取并处理队列中的 URL
url = await self._queue.get()
# 如果 URL 已经被处理过了,就跳过
if url in self._seen_urls:
self._queue.task_done()
continue
# 抓取 URL
html = await loop.run_in_executor(None, self._fetch, url)
if html is not None:
# 处理 HTML,提取所需的数据
# ...
# 将新发现的 URL 放入队列
urls = self._extract_links(html)
for new_url in urls:
await self._queue.put(new_url)
# 标记这个 URL 已经处理过了
self._queue.task_done()
def _extract_links(self, html):
# 从 HTML 中提取链接
# ...
return []
该示例程序通过使用 asyncio 和 aiohttp 实现了异步并发的爬虫功能,并且使用了队列来管理已访问的 URL,以避免重复访问。同时,程序也使用了线程池,将 HTTP 请求和处理操作转移到了单独的线程中,并且使用 run_in_executor()
函数来执行。
除了之前提到的常量之外,程序中新增了一个常量 MAX_THREADS
,它指定了线程池的最大大小。程序同样通过创建 Queue
对象创建了一个队列,然后将要访问的 URL 添加到了队列中。接着创建了多个协程( _worker()
函数)并行地从队列中获取 URL,使用 run_in_executor()
函数将网页内容获取和数据提取操作放到异步的线程池中运行,提高了程序的并发度和效率。最后,等待所有的队列都完成,并将取消所有的任务。