Назад к статьям

Асинхронный парсинг: ускорение сбора данных

Привет! Сегодня поговорим о том, как парсить сайты в 10 раз быстрее. Звучит как реклама чудо-средства? Но это реально работает! Всё дело в асинхронности. ⚡

Проблема синхронного парсинга

Представь: тебе нужно спарсить 100 страниц. С обычным requests это займёт примерно 100 секунд (если каждая страница грузится 1 секунду). А что если страниц 1000? Уже 16 минут! А если 10000? 😱

Решение: asyncio + aiohttp

Асинхронный парсинг позволяет загружать несколько страниц одновременно. Вместо того чтобы ждать ответа от одной страницы, мы отправляем запросы ко всем сразу!

Базовый пример

Вот как выглядит простой асинхронный парсер:

import asyncio import aiohttp from bs4 import BeautifulSoup async def fetch_page(session, url): """Асинхронная загрузка страницы""" try: async with session.get(url) as response: html = await response.text() return html except Exception as e: print(f"Ошибка при загрузке {url}: {e}") return None async def parse_urls(urls): """Парсим список URL асинхронно""" async with aiohttp.ClientSession() as session: tasks = [fetch_page(session, url) for url in urls] results = await asyncio.gather(*tasks) return results # Использование urls = [ 'https://example.com/page1', 'https://example.com/page2', 'https://example.com/page3', # ... ещё 97 страниц ] # Запускаем results = asyncio.run(parse_urls(urls)) print(f"Загружено {len(results)} страниц!")

Парсим данные из HTML

Теперь добавим парсинг данных:

async def parse_page(session, url): """Загружаем и парсим страницу""" async with session.get(url) as response: html = await response.text() soup = BeautifulSoup(html, 'html.parser') # Извлекаем данные title = soup.find('h1') title_text = title.text if title else 'Нет заголовка' return { 'url': url, 'title': title_text } async def parse_multiple_pages(urls): """Парсим несколько страниц параллельно""" async with aiohttp.ClientSession() as session: tasks = [parse_page(session, url) for url in urls] results = await asyncio.gather(*tasks) return results # Использование urls = ['https://example.com/page1', 'https://example.com/page2'] results = asyncio.run(parse_multiple_pages(urls)) for result in results: print(f"{result['title']}: {result['url']}")

Ограничение скорости (rate limiting)

Не стоит делать слишком много запросов одновременно — сайт может заблокировать. Ограничим количество одновременных запросов:

import asyncio from asyncio import Semaphore async def fetch_with_limit(semaphore, session, url): """Загрузка с ограничением""" async with semaphore: # Ограничиваем количество одновременных запросов async with session.get(url) as response: return await response.text() async def parse_with_limit(urls, max_concurrent=10): """Парсим с ограничением на количество одновременных запросов""" semaphore = Semaphore(max_concurrent) # Максимум 10 одновременно async with aiohttp.ClientSession() as session: tasks = [ fetch_with_limit(semaphore, session, url) for url in urls ] results = await asyncio.gather(*tasks) return results # Использование urls = [f'https://example.com/page{i}' for i in range(100)] results = asyncio.run(parse_with_limit(urls, max_concurrent=10))

Обработка ошибок

В асинхронном коде ошибки обрабатываются немного иначе:

async def safe_fetch(session, url, retries=3): """Безопасная загрузка с повторами""" for attempt in range(retries): try: async with session.get(url, timeout=aiohttp.ClientTimeout(total=10)) as response: if response.status == 200: return await response.text() else: print(f"Ошибка {response.status} для {url}") except asyncio.TimeoutError: print(f"Таймаут для {url}, попытка {attempt + 1}") except Exception as e: print(f"Ошибка для {url}: {e}") if attempt < retries - 1: await asyncio.sleep(2 ** attempt) # Экспоненциальная задержка return None

Сравнение скорости

Давай сравним синхронный и асинхронный подходы:

import time # Синхронный подход def sync_parse(urls): start = time.time() for url in urls: response = requests.get(url) # обработка... print(f"Синхронно: {time.time() - start:.2f} секунд") # Асинхронный подход async def async_parse(urls): start = time.time() async with aiohttp.ClientSession() as session: tasks = [fetch_page(session, url) for url in urls] await asyncio.gather(*tasks) print(f"Асинхронно: {time.time() - start:.2f} секунд") # Для 100 страниц: # Синхронно: ~100 секунд # Асинхронно: ~10 секунд (в 10 раз быстрее!)

Итоги

Асинхронный парсинг — это не магия, а просто умное использование времени ожидания. Вместо того чтобы простаивать, пока грузится одна страница, мы загружаем десятки одновременно. Результат — парсинг в 10 раз быстрее! 🚀