Самая долгожданная часть асинхронного модуля. В этом разделе мы наконец-то напишем свой первый асинхронный парсер, который ускорит сбор информации в десятки раз.
Для начала напишем самый простой парсер, который соберёт с одной страницы нашего сайта-тренажера всего лишь названия и цены. Большую часть кода вы уже видели, и в этом примере всё будет вам очень знакомо.
Этот пример важен для понимания того, как мы будем строить дальнейшее обучение.
import aiohttp import asyncio from bs4 import BeautifulSoup async def main(): url = 'https://mob25.com/index1_page_1.html' async with aiohttp.ClientSession() as session: async with session.get(url=url, timeout=1) as response: soup = BeautifulSoup(await response.text(), 'lxml') name = soup.find_all('a', class_='name_item') price = soup.find_all('p', class_='price') for n, p in zip(name, price): print(n.text, p.text) asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy()) asyncio.run(main())
Всё очень просто и понятно. К этому этапу курса вы уже умеете работать с «супом», и в асинхронном коде это почти не отличается от синхронного стиля, за некоторыми исключениями. К примеру, появилось ключевое слово await, которое располагается перед ответом переменной await response.text(). В этом месте происходит отправка запроса и получение ответа от сервера, поэтому нам нужно написать ключевое слово await, чтобы дать понять нашему циклу событий, где нам нужно переключаться, пока мы ожидаем ответ от сервера.
soup = BeautifulSoup(await response.text(), 'lxml')
Следующий пример будет немного сложнее, так как мы будем получать с нашего сайта-тренажера информацию с карточек, которых там 160 штук: цену и наименование товара.
import asyncio import time import aiohttp from bs4 import BeautifulSoup # ---------------------start block 1------------------------ category = ['watch', 'mobile', 'mouse', 'hdd', 'headphones'] urls = [f'https://mob25.com/{cat}/{i}/{i}_{x}.html' for cat, i in zip(category, range(1, len(category) + 1)) for x in range(1, 33)] # ---------------------end block 1------------------------ # ---------------------start block 2------------------------ async def run_tasks(url, session): async with session.get(url) as resp: soup = BeautifulSoup(await resp.text(), 'lxml') price = soup.find('span', id='price').text name = soup.find('p', id='p_header').text print(resp.url, price, name) # ---------------------end block 2------------------------ # ---------------------start block 3------------------------ async def main(): async with aiohttp.ClientSession() as session: tasks = [run_tasks(link, session) for link in urls] await asyncio.gather(*tasks) # ---------------------end block 3------------------------ # ---------------------start block 4------------------------ if __name__ == '__main__': asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy()) start = time.time() asyncio.run(main()) print(time.time()-start)
Блок кода №1.
В этом блоке мы проанализировали наш сайт и поняли, что проще всего будет подготовить все ссылки заранее и передать их все сразу в цикл событий. Это самый простой, но не совсем надежный способ, потому что этот код может собрать только то, что мы сгенерировали. Если на сайте появится дополнительная категория или страница пагинации, код до них не доберётся. Тем не менее, этот подход имеет право на существование из-за своей простоты.
Блок кода № 2.
Здесь всё очень знакомо и просто: вы видели это в предыдущем разделе, посвящённом aiohttp, и по предыдущему примеру кода всё должно быть понятно. Вы уже неоднократно работали с BeautifulSoup, так что вопросов быть не должно.
Блок кода № 3.
А вот тут начинается самое интересное.
Создание клиентской сессии aiohttp:
async with aiohttp.ClientSession() as session: Здесь создается асинхронный контекстный менеджер, который открывает сессию клиента aiohttp. Объект session представляет собой сессию клиента, которая может быть использована для выполнения HTTP-запросов. Использование сессии в контекстном менеджере гарантирует, что сессия будет корректно закрыта после выхода из блока async with.
Создание списка корутин:
tasks = [run_tasks(link, session) for link in urls]. В этой строке создается список корутин. Каждый элемент списка — это объект корутины, который получается в результате вызова асинхронной функции run_tasks с соответствующими параметрами(ссылка на страницу и объект сессии). Эти объекты корутин еще не выполняются, они представляют собой запланированные для выполнения операции.
Корутина в Python — это функция, определенная с использованием async def, и она при вызове возвращает объект корутины. Однако, корутина сама по себе не выполняется автоматически, она должна быть запущена в цикле событий.
Задача (asyncio.Task) — это обертка вокруг корутины, которая позволяет ей выполняться асинхронно.
async def run_tasks(): tasks = [main(link) for link in urls] for x in tasks: print(type(x)) await asyncio.gather(*tasks) Результат: <class 'coroutine'> <class 'coroutine'> ... <class 'coroutine'> <class 'coroutine'>
Планирование выполнения корутин:
await asyncio.gather(*tasks): Здесь asyncio.gather принимает список корутин и планирует их параллельное выполнение. Корутины из списка tasks автоматически преобразуются в задачи.
Функция await asyncio.gather(*tasks) запускает все задачи одновременно, а ключевое слово await говорит программе, что необходимо дождаться результата выполнения каждой задачи.
Блок кода № 4.
if __name__ == '__main__': asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy()) start = time.time() if __name__ == '__main__': asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy()) start = time.time() asyncio.run(main()) print(time.time()-start) asyncio.run(main()) print(time.time()-start)
Функция asyncio.run(main()), запускает цикл событий с корутиной main(), в качестве основной задачи.
Так же добавлено измерение времени работы скрипта.
Если вы запускали код у себя в терминале, то, возможно, обратили внимание на то, что ссылки в консоли печатаются в случайном порядке. Случайный порядок возвращаемых результатов обусловлен спецификой асинхронной обработки задач.
Результат печатается сразу, как только задача окажется выполненной.
Если вам нужен список результатов расположенных в строгом порядке, то нужно изменить run_tasks() так, чтобы вместо print() был использован return:
async def run_tasks(url, session): async with session.get(url) as resp: soup = BeautifulSoup(await resp.text(), 'lxml') price = soup.find('span', id='price').text name = soup.find('p', id='p_header').text return resp.url, price, name
А в main() сохраним результаты работы всех задач в список result и распечатаем содержимое этого списка:
async def main(): async with aiohttp.ClientSession() as session: tasks = [run_tasks(link, session) for link in urls] # Сохраняем результаты работы всех задач в result result = await asyncio.gather(*tasks) # Распечатываем содержимое списка result for x in result: print(*x, sep=', ')