Введение в веб-скрапинг и его подводные камни

Представьте: пятница вечер, вы закрываете ноутбук в предвкушении выходных, а в три часа ночи ваш телефон разрывается от уведомлений. Парсер цен конкурентов, который полгода работал как часы, снова лег из-за незаметного обновления верстки на одном единственном сайте (вполне классический сценарий для пятничного деплоя). Знакомо? Веб-скрапинг на Python остается базовым инструментом для сбора данных, маркетинговых исследований и формирования датасетов для ML. Однако любой разработчик знает: парсеры ломаются регулярно. Целевые ресурсы меняют верстку, внедряют динамический JavaScript, выставляют капчи и блокируют подобию ботов IP-адреса.

Когда скрапер падает в production посреди ночи из-за изменения одного CSS-селектора, это останавливает бизнес-процессы. В этой статье мы разберем классические ошибки веб-скрапинга на Python, методы их диагностики и лучшие практики проектирования отказоустойчивых конвейеров данных.

Выбор стека: почему неверный инструмент рождает баги

Большинство архитектурных проблем закладываются еще на этапе выбора библиотек. Использование статических инструментов для рендеринга сложных SPA (Single Page Applications) гарантированно приводит к пустым результатам.

Чтобы не переписывать код на продакшене из-за того, что ваш скрипт «не видит» таблицу с ценами (или потому что решение из первой ссылки на Stack Overflow пятилетней давности наконец окончательно сгнило), важно сразу соотнести масштаб задачи и возможности инструментов. Современный стек Python для сбора данных делится на несколько уровней:

  • requests + BeautifulSoup: Идеально для статических сайтов. Работает быстро, потребляет минимум памяти, но бесполезно для страниц с тяжелым JS.
  • Selenium / Playwright: Автоматизация реальных браузеров. Справляются с динамическим контентом, авторизацией и кликами, но требуют больше ресурсов CPU и RAM.
  • Scrapy: Асинхронный фреймворк корпоративного уровня. Подходит для масштабного скрапинга, содержит встроенные механизмы задержек и Middleware.

Топ-4 типичных ошибки в скрапинге и методы их исправления

1. Ошибки сетевого уровня и HTTP-статусы (403, 429, 503)

Частая проблема — скрипт падает с исключением при получении кодов ответа вроде 403 Forbidden или 429 Too Many Requests. Это значит, что защиту сайта (Cloudflare, WAF) триггерит ваш User-Agent или слишком высокая частота запросов.

Решение: Добавьте ротацию User-Agent, настройте случайные задержки (throttling) между запросами и используйте пулы прокси.

import requests
import time
import random

headers_list = [
    {'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36'},
    {'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15'}
]

url = 'https://example.com/data'
try:
    response = requests.get(url, headers=random.choice(headers_list), timeout=10)
    response.raise_for_status()
except requests.exceptions.HTTPError as err:
    print(f'HTTP ошибка: {err}')
except requests.exceptions.ConnectionError:
    print('Ошибка соединения с сервером')
time.sleep(random.uniform(1.5, 4.0))

2. Падения из-за изменения структуры HTML (KeyError / AttributeError)

Сайт обновил дизайн, тег <h1 class="title"> исчез или сменил имя класса. BeautifulSoup возвращает None, а попытка вызвать метод .text у объекта NoneType крашит весь скрипт.

Решение: Всегда проверяйте элементы на существование перед извлечением данных и используйте конструкции безопасного доступа.

from bs4 import BeautifulSoup

html = '<div class="content"><p>Пример текста</p></div>'
soup = BeautifulSoup(html, 'html.parser')

# Небезопасно: title_elem.text вызовет AttributeError, если элемент не найден
# title_elem = soup.find('h1', class_='missing')

# Безопасно:
title_elem = soup.find('h1', class_='missing')
title_text