Привет! Сегодня поговорим о том, как парсить сайты в 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 раз быстрее! 🚀