Python: от базы до продвинутого

Парсинг сайтов на Python в 2026: requests, BeautifulSoup и обход блокировок

Парсинг в 2026 году — это не «скачать HTML и достать div». Любой сайт с трафиком ставит Cloudflare, считает запросы и банит по паттернам поведения. В этой статье — стек, который я использую в production: базовый сбор на requests + BeautifulSoup, обход типовых блокировок и переход на асинхронный сбор, когда объёмы вырастают.

Версии, на которых проверен код: Python 3.12, requests 2.32, beautifulsoup4 4.13.

База: requests + BeautifulSoup

Для статических страниц этого достаточно. Весь классический стек умещается в 20 строк:

import time

import requests
from bs4 import BeautifulSoup

HEADERS = {
    "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) "
                  "AppleWebKit/537.36 (KHTML, like Gecko) "
                  "Chrome/130.0.0.0 Safari/537.36",
    "Accept-Language": "ru-RU,ru;q=0.9",
}

def fetch(url: str) -> str:
    for attempt in range(3):
        try:
            response = requests.get(url, headers=HEADERS, timeout=15)
            response.raise_for_status()
            return response.text
        except requests.RequestException:
            if attempt == 2:
                raise
            time.sleep(2 ** attempt)  # экспоненциальный бэкофф: 1с, 2с, 4с
    return ""


def parse_product(html: str) -> dict:
    soup = BeautifulSoup(html, "lxml")
    return {
        "title": soup.select_one("h1").get_text(strip=True),
        "price": soup.select_one(".price").get_text(strip=True),
        "in_stock": bool(soup.select_one(".in-stock")),
    }

html = fetch("https://example.com/catalog/item-1")
print(parse_product(html))

Два момента, которые новички пропускают, а потом теряют полсуток:

  1. Таймаут обязателен. Без timeout один зависший запрос уложит весь сборщик — requests ждёт вечно. Всегда ставьте таймаут на connect и read (лучше через requests.get(..., timeout=(5, 15))).
  2. raise_for_status() и ретраи. 5xx и обрывы сети — норма при больших объёмах, не падать, а повторять с бэкоффом.

Ротация User-Agent и прокси: как не получить бан на 500-й странице

Первое, что проверяет любая защита — не похожи ли ваши запросы на человека. Пул User-Agent'ов и residential-прокси решают большинство проблем до того, как они появятся.

import random

import requests

USER_AGENTS = [
    "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36",
    "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.0.0 Safari/537.36",
    "Mozilla/5.0 (X11; Linux x86_64; rv:131.0) Gecko/20100101 Firefox/131.0",
]

PROXIES = [
    "http://user:pass@proxy1.provider.com:8000",
    "http://user:pass@proxy2.provider.com:8000",
]


def fetch_anonymous(url: str) -> str:
    response = requests.get(
        url,
        headers={"User-Agent": random.choice(USER_AGENTS), "Accept-Language": "ru-RU,ru;q=0.9"},
        proxies={"http": random.choice(PROXIES), "https": random.choice(PROXIES)},
        timeout=(5, 15),
    )
    response.raise_for_status()
    return response.text

Практические правила из реальных сборок:

  • Паузы важнее прокси. Человек не запрашивает 30 страниц в секунду. Рандомная пауза 1–5 секунд между запросами снимает 80% блокировок без всяких прокси: time.sleep(random.uniform(1, 5)).
  • Ротация прокси по попыткам, а не по запросу. Если IP уже забанен, менять прокси нужно после неудачной попытки, а не заранее — иначе сжигаете пул впустую.
  • Сессии через один прокси. Если сайт привязывает показы к сессии, держите requests.Session() до конца «визита» пользователя.

Обход защиты: Cloudflare, капчи, JS-рендеринг

Когда базовых мер недостаточно, смотрите по диагностике:

Симптом Причина Решение
403 с первого запроса Cloudflare / WAF curl_cffi (имитация TLS-фингерпринта Chrome) или прокси с поддержкой residential IP
403 после N запросов Rate limiting Паузы, ротация IP, снижение RPS
Капча на странице PerimeterX, DataDome Сервисы распознавания (2captcha) — как крайняя мера, или Playwright
Пустой HTML, данные видны в браузере Контент грузится через JS Playwright либо поиск внутреннего API

Проверенный способ узнать, есть ли скрытый API: откройте страницу в Chrome → DevTools → Network → фильтр Fetch/XHR → перезагрузите. Видите запрос, возвращающий JSON с нужными данными? Парсите его напрямую — это в разы быстрее и стабильнее, чем рендерить HTML:

# Вместо парсинга HTML — прямой запрос к внутреннему API сайта
response = requests.get(
    "https://example.com/api/catalog",
    params={"category": "phones", "page": 1},
    headers={"User-Agent": random.choice(USER_AGENTS)},
    timeout=(5, 15),
)
items = response.json()["items"]

Асинхронный сбор: когда объёмы вырастают

Синхронный цикл тратит 95% времени на ожидание сети. asyncio + aiohttp убирают это ожидание — на том же железе в 10–20 раз быстрее. Тот же сборщик, асинхронный:

import asyncio

import aiohttp
from bs4 import BeautifulSoup

CONCURRENCY = 20  # одновременных запросов; дальше — только баны
SEMAPHORE = asyncio.Semaphore(CONCURRENCY)


async def fetch_one(session: aiohttp.ClientSession, url: str) -> str | None:
    async with SEMAPHORE:
        for attempt in range(3):
            try:
                async with session.get(url, timeout=aiohttp.ClientTimeout(total=20)) as response:
                    if response.status == 200:
                        return await response.text()
            except aiohttp.ClientError:
                await asyncio.sleep(2 ** attempt)
        return None


async def main(urls: list[str]) -> None:
    connector = aiohttp.TCPConnector(limit=CONCURRENCY, ttl_dns_cache=300)
    async with aiohttp.ClientSession(connector=connector) as session:
        results = await asyncio.gather(*(fetch_one(session, url) for url in urls))
    parsed = [parse_product(html) for html in results if html]


if __name__ == "__main__":
    urls = [f"https://example.com/catalog/item-{i}" for i in range(1, 10001)]
    asyncio.run(main(urls))

Семафор — критично: без него gather откроет тысячи соединений разом, и вы забаните себя быстрее, чем успеете собрать данные. Начинайте с 10–20 одновременных запросов и поднимайте, пока не появятся 429/403.

Дальше — горизонтальное масштабирование: разбить список URL между несколькими воркерами (по процессам или машинам) и складывать результаты в общую очередь. На этом этапе обычно добавляют Celery + Redis — про это отдельная статья в треке Python.

Подводные камни, о которых молчат туториалы

  • Кодировки. Сайты из 2008 года отдают windows-1251. requests обычно справляется сам, но если в тексте «кракозябры» — проверяйте response.encoding или парсите response.content через BeautifulSoup: он умеет meta charset.
  • Пагинация «показать ещё». Не гонитесь за кликом по кнопке — почти всегда под ним обычный GET с ?page=2. Смотрите в Network.
  • Дедупликация. При перезапусках обязательно храните обработанные URL, иначе соберёте одно и то же по три раза. Простейший вариант — таблица в SQLite с unique по URL.
  • Юридическая сторона. Соблюдайте robots.txt, не долбите сайт, не собирайте персональные данные. Коммерческий парсинг — с юристом.

Итог

Стек по ступеням роста объёма:

  1. До ~50К страниц/сутки: requests + BeautifulSoup + паузы. Минимум кода, максимум простоты.
  2. До ~500К: ротация User-Agent и прокси, ретраи, хранение прогресса в SQLite.
  3. До миллионов: asyncio + aiohttp, семафор конкурентности, распределение по воркерам, Celery при необходимости.

Следующий шаг в кластере — разбор Telegram-бота на Python за час: собранные данные неплохо было бы куда-то отправлять.

FAQ

Чем парсинг на Python лучше готовых сервисов типа Octoparse?

Гибкостью и ценой. Сервисы хороши для разового сбора простых страниц, но ломаются на защите, пагинации с JS и кастомной логике. Скрипт на Python вы контролируете полностью: ретраи, прокси, формат выгрузки — и не платите за каждый запуск.

Легален ли парсинг сайтов?

Парсятся данные, а не базы: публично доступные страницы — обычно да, но с оговорками. Соблюдайте robots.txt, не нагружайте сайт (паузы!), не собирайте персональные данные без оснований. Для коммерческих проектов — консультация юриста по вашей юрисдикции.

Что делать, если сайт отдаёт данные через JavaScript и requests видит пустую страницу?

Три пути: найти внутренний API, к которому обращается фронтенд (DevTools → Network → XHR), использовать Playwright для рендеринга страницы, либо искать эти же данные в другом источнике. В 70% случаев достаточно первого.

Какой объём парсинга выдерживает один скрипт на requests?

Синхронный requests с паузами комфортно собирает 50–200 тысяч страниц в сутки. Дальше упираетесь в сеть и блокировки — время переходить на asyncio (в 10–20 раз быстрее на том же железе) и пул прокси.

Один разбор в неделю на YouTube

Каждой статье здесь — пара-видео на канале @PythonSimple: та же задача, живой код, коротко.

Подписаться на @PythonSimple