Перейти к содержанию

192 уроков, 14 библиотек и челлендж «Что выведет код?» — бесплатно, код прямо в браузере

Начать обучение
Урок 8 из 10 Средний 45 мин 150 XP

Этичный парсинг: robots.txt, задержки и чтобы не забанили

Парсер собрал три тысячи страниц — а утром каждый запрос возвращает 403. Читаем robots.txt, представляемся по имени, держим вежливый темп и отвечаем на 429 так, чтобы дверь не закрылась навсегда.

Редакция Питоники

Ваш парсер из прошлого урока честно прошёл три тысячи страниц за ночь. Утром вы запускаете его снова — и каждый запрос возвращает 403 Forbidden. Сервер вас узнал и закрыл дверь. Никакой мистики: слишком быстрые, слишком одинаковые запросы неотличимы от атаки, и защита сайта сработала как задумано. Сегодня научимся ходить по чужим сайтам так, чтобы дверь не закрывалась: читать robots.txt, представляться по имени, держать вежливый темп и правильно реагировать на 403 и 429. И коротко — о правовой стороне: где проходит граница между сбором публичных данных и нарушением закона.

robots.txt: правила, написанные до вас

У любого приличного сайта в корне лежит файл robots.txt. Он адресован роботам: поисковым краулерам, аналитическим ботам — и вашим скриптам тоже. Файл не имеет юридической силы, это договор вежливости: поисковые системы считают его обязательным, а парсер-нарушитель рискует и репутацией, и доступом. Прежде чем писать код, откройте в браузере адрес сайт/robots.txt — вы удивитесь, сколько сайтов открыто пишут, что можно и нельзя.

robots.txt — файл магазина shop.example
# shop.example - правила для роботов
User-agent: *
Disallow: /cart
Disallow: /profile
Disallow: /orders
Disallow: /search
Allow: /catalog/
Crawl-delay: 2

User-agent: Googlebot
Disallow: /orders

Sitemap: https://shop.example/sitemap.xml
Это не Python, а содержимое текстового файла robots.txt — он всегда лежит в корне сайта: https://shop.example/robots.txt

Директивы построчно

  • User-agent: \* — к какой группе роботов относятся правила ниже; звёздочка означает «все», отдельно можно адресовать Googlebot или YandexBot;
  • Disallow: /cart — запрещённый префикс: под правило попадает любой адрес, начинающийся с /cart;
  • Allow: /catalog/ — исключение из запрета: этот префикс можно даже там, где общий блок запрещает больше;
  • Crawl-delay: 2 — минимальная пауза в секундах между запросами одного робота;
  • Sitemap: ... — карта сайта: готовый список адресов, который часто экономит целый парсер.

Правила читаются внутри одного блока User-agent, а блоки разделяются пустой строкой. Совпадение — префиксное: Disallow: /cart накроет и /cart/42, и /carts-archive. К чему это приводит на практике — разберём на живом примере.

slash_demo.py — что значит одинокая косая черта
from urllib.robotparser import RobotFileParser

rp = RobotFileParser()
rp.parse(["User-agent: *", "Disallow: /"])

print(rp.can_fetch("*", "/"))              # главная
print(rp.can_fetch("*", "/catalog/page/2/"))
print(rp.can_fetch("*", "/blog"))
Вывод
False
False
False

Проверяем правила кодом: urllib.robotparser

Глаза — плохой инструмент для robots.txt: правила пересекаются, и вручную легко ошибиться. В стандартной библиотеке Python есть парсер urllib.robotparser: скормите ему строки файла — и спрашивайте разрешения по каждому адресу. Сеть не нужна, разбор работает офлайн:

robots_check.py — RobotFileParser в деле
from urllib.robotparser import RobotFileParser

robots_txt = """
User-agent: *
Disallow: /cart
Disallow: /profile
Disallow: /search
Allow: /catalog/
Crawl-delay: 2
"""

rp = RobotFileParser()
rp.parse(robots_txt.splitlines())

print(rp.can_fetch("*", "/catalog/page/2/"))
print(rp.can_fetch("*", "/cart"))
print(rp.can_fetch("*", "/profile/orders/123"))
print(rp.can_fetch("*", "/catalog"))
print(rp.crawl_delay("*"))
Вывод
True
False
False
True
2

Разбираем вывод. Каталог можно, корзина и профиль нельзя — как и записано. Метод can_fetch получает имя вашего агента и путь и отвечает True или False. А /catalog без слэша разрешён, но не из-за Allow: правило Allow: /catalog/ со слэшем его не покрывает — просто ни один Disallow на него не попадает, а всё не запрещённое разрешено по умолчанию. Метод crawl_delay достаёт паузу из файла: сервер просил 2 секунды между запросами.

User-Agent: кто стучится в дверь

Каждый HTTP-запрос подписывается заголовком User-Agent. Библиотека requests по умолчанию подписывается как python-requests/2.31.0 — и именно такие запросы админы режут первыми: под этим именем ходят в основном дешёвые скрипты, от которых не ждут пользы. В уроке про requests мы уже передавали словарь headers — теперь понятно, зачем он нужен на каждом запросе.

ua_honest.py — честная подпись парсера
user_agent = "CatalogWatcher/1.0 (+https://example.com/bot; ivan@example.com)"

headers = {
    "User-Agent": user_agent,
    "Accept-Language": "ru-RU,ru;q=0.9",
}

print(headers["User-Agent"])
print("Бот представился:", "Watcher" in user_agent)
Вывод
CatalogWatcher/1.0 (+https://example.com/bot; ivan@example.com)
Бот представился: True

Формат «Имя/версия (+адрес страницы с описанием; контакт)» — конвенция из мира поисковых роботов: по адресу из скобок админ находит, кто вы и как вас отключить. Честность окупается: на запрос «дайте разрешение или скажите нет» отвечают, а молчаливый скрипт под чужим именем банят молча.

fetch_headers.py — заголовки в запросе
import requests

headers = {
    "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36",
    "Accept-Language": "ru-RU,ru;q=0.9",
}

resp = requests.get("https://shop.example/catalog/", headers=headers, timeout=10)
print(resp.status_code)
print(resp.headers["Content-Type"])
Вывод
200
text/html; charset=utf-8
Блок для локального запуска: в песочнице нет сети. Вывод зависит от сайта — здесь показан типичный ответ на корректный запрос.

Второй путь — маскировка под браузер: строка Mozilla/5.0 ... якобы от Chrome на Windows. Работает, но это серая зона: сайт уверен, что к нему пришёл человек. Для публичных каталогов и новостей разница невелика — страницы всё равно отдаются всем желающим. А вот если контент живёт за авторизацией, маскировка не спасёт и не должна: это уже не парсинг, а обход защиты.

Задержки: темп, который не злит сервер

Скорость — главный триггер банов. Человек листает каталог со скоростью страницы в несколько секунд; скрипт без пауз выдаёт десятки запросов в секунду. Для сервера второе выглядит как атака, и ответ приходит в виде 403. В уроке про пагинацию мы уже ставили time.sleep в цикл по страницам — теперь разберём, как выбирать паузу грамотно.

polite_pace.py — пауза с разбросом
import time
import random

random.seed(42)   # фиксированный сид - чтобы демо было повторяемым
pages = ["/catalog/page/1/", "/catalog/page/2/", "/catalog/page/3/"]

for page in pages:
    # в бою: random.uniform(1, 3); здесь укорочено для демо
    delay = round(random.uniform(0.1, 0.3), 2)
    time.sleep(delay)
    print(page, "пауза", delay, "c")
Вывод
/catalog/page/1/ пауза 0.23 c
/catalog/page/2/ пауза 0.11 c
/catalog/page/3/ пауза 0.16 c
Паузы укорочены до долей секунды, чтобы блок запускался мгновенно. В реальном парсере берите диапазон 1-3 секунды или Crawl-delay из robots.txt, что больше.

Зачем разброс, если можно sleep(1)? Потому что ровная периодичность — подпись робота: сто запросов строго через секунду видны в логах как метроном. random.uniform(1, 3) даёт рваный, живой темп. Если в robots.txt заявлен Crawl-delay — это нижняя граница: сервер явно попросил темп, и нарушать просьбу, которую вы уже прочитали, особенно некрасиво.

429 Too Many Requests и Retry-After

Прежде чем забанить, приличный сервер обычно предупреждает кодом 429 и заголовком Retry-After: столько-то секунд подождите. Это ещё не бан, а жёлтая карточка — и от вашей реакции зависит, станет ли она красной.

КодЧто значитЧто делать
200всё хорошоработаем дальше
403доступ запрещён: вас узнали и не хотят видетьостановить прогон; проверить robots.txt, User-Agent и темп; вернуться через дни, а не минуты
429слишком много запросов — лимит темпапрочитать Retry-After, ждать ровно столько, продолжить медленнее
503сервер перегруженто же, что при 429: пауза и повтор с меньшей скоростью
retry_after.py — реакция на 429
import time

status = 429          # то, что вернул сервер
retry_after = 30      # из заголовка Retry-After, секунд

for attempt in range(1, 4):
    if status == 429:
        print(f"Попытка {attempt}: лимит. Жду {retry_after} c")
        time.sleep(retry_after)
        status = 200  # после паузы сервер ответил нормально
    else:
        print("Успех:", status)
        break
Вывод
Попытка 1: лимит. Жду 30 c
Успех: 200
Сеть в песочнице недоступна, поэтому статус зашит в переменной. Локально берите status из resp.status_code, а retry_after — из resp.headers.get("Retry-After", 30).

Шаблон: парсер с вежливыми привычками

Соберём приёмы в каркас, с которого можно начинать свой скрипт: честный User-Agent, пауза с разбросом, таймаут, реакция на ошибки и сохранение из прошлого урока. В песочнице он не запустится — сеть и файлы недоступны, — но как заготовка для локальной работы работает целиком:

polite_parser.py — каркас вежливого парсера
import csv
import random
import time

import requests
from bs4 import BeautifulSoup

HEADERS = {
    "User-Agent": "CatalogWatcher/1.0 (+https://example.com/bot; ivan@example.com)",
}
MIN_DELAY, MAX_DELAY = 1.5, 3.0


def fetch(url):
    # вежливая пауза перед каждым запросом
    time.sleep(random.uniform(MIN_DELAY, MAX_DELAY))
    resp = requests.get(url, headers=HEADERS, timeout=10)
    resp.raise_for_status()
    return resp.text


def parse_catalog(pages):
    rows = []
    for num in range(1, pages + 1):
        html = fetch(f"https://shop.example/catalog/page/{num}/")
        soup = BeautifulSoup(html, "html.parser")
        for card in soup.select("div.card"):
            rows.append({
                "name": card.select_one("h3").get_text(strip=True),
                "price": card.select_one(".price").get_text(strip=True),
            })
    return rows


def save_csv(rows):
    with open("output/catalog.csv", "w", encoding="utf-8-sig", newline="") as f:
        writer = csv.DictWriter(f, fieldnames=["name", "price"], delimiter=";")
        writer.writeheader()
        writer.writerows(rows)


if __name__ == "__main__":
    save_csv(parse_catalog(pages=5))
    print("Готово: output/catalog.csv")
Вывод
Готово: output/catalog.csv
Шаблон для локального запуска: requests и файлы в песочнице недоступны. Здесь собраны приёмы всего урока — подпись, темп, таймаут, raise_for_status и сохранение с utf-8-sig.

Что парсить нельзя: красные линии

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

  1. Персональные данные: ФИО, телефоны, адреса, email. Их сбор и хранение регулируются законом, публичность страницы ничего не отменяет.
  2. Страницы за авторизацией: аккаунт даёт право смотреть данные глазами, а не утаскивать их скриптом по логину и паролю.
  3. Обход защиты: взлом капчи, подмена сессий, сканирование скрытых адресов — это уже не парсинг, а неправомерный доступ.
  4. Уникальный контент в промышленных объёмах: выкачивать целые базы статей и объявлений — нарушение чужих прав на контент.

API вместо парсинга

Прежде чем писать парсер, потратьте десять минут на поиск официального способа: у многих сайтов есть API, у новостных — RSS-лента, у маркетплейсов — выгрузки для партнёров. API стабильнее разметки (её меняют без предупреждения, контракт API — нет), быстрее и легитимнее: вам буквально говорят «берите данные вот тут».

Правило API-first я бы поставил первым в любом курсе парсинга: сначала официальный источник, потом RSS, и только затем разбор HTML. BeautifulSoup остаётся для случаев, когда API нет или он платный, а данные — публичные и простые.

Чек-лист вежливого парсера

  • Прочитал robots.txt — запреты для группы * соблюдаю.
  • Ставлю Crawl-delay из файла или паузу 1-3 секунды с разбросом, что больше.
  • Подписываюсь в User-Agent с контактом — или понимаю, почему беру браузерный.
  • На 429 отвечаю паузой по Retry-After, а не повтором.
  • У каждого запроса есть timeout и raise_for_status.
  • Не хожу за авторизацией и не собираю персональные данные.
  • Сначала поискал API и RSS — HTML-парсер только запасной план.

Фреймворк Scrapy, к которому мы переходим в следующем уроке, зашил этот чек-лист в настройки: ROBOTSTXT_OBEY, DOWNLOAD_DELAY и автотроттлинг включаются строчками конфига — вежливость там по умолчанию, а не по памяти.

Что дальше

Краткий итог: robots.txt читаем до первого запроса и проверяем правилами RobotFileParser, представляемся User-Agent с контактом, держим паузу 1-3 секунды с разбросом, на 429 ждём Retry-After, персональные данные и страницы за авторизацией не трогаем, а перед парсером ищем API. Так вы соберёте нужные данные и не увидите ни одного 403. Это восьмая ступень из десяти; весь маршрут от первого тега до Scrapy — самоучитель BeautifulSoup.

Когда скрипт перестаёт помещаться в один файл, а страниц в обходе — в тысячи, приходит время фреймворка: в следующем уроке заводим первый проект Scrapy и смотрим, чем паук со встроенными pipeline и автотроттлингом отличается от нашего каркаса на requests и BeautifulSoup.

Что выведет код?

Сначала предскажи ответ в голове — это главный навык программиста.

from urllib.robotparser import RobotFileParser

robots_txt = """
User-agent: *
Disallow: /cart
Crawl-delay: 2

User-agent: GoodBot
Disallow: /
Crawl-delay: 5
"""
rp = RobotFileParser()
rp.parse(robots_txt.splitlines())

print(rp.can_fetch("GoodBot", "/catalog/"))
print(rp.can_fetch("GoodBot", "/cart"))
print(rp.can_fetch("Mozilla", "/cart"))
print(rp.crawl_delay("GoodBot"))
from urllib.robotparser import RobotFileParser

rp = RobotFileParser()
rp.parse(["User-agent: *", "Disallow: /cart"])

print(rp.can_fetch("*", "/cart/page/2/"))
print(rp.can_fetch("*", "/carts/"))
print(rp.can_fetch("*", "/catalog/"))
Проверь себя
0 / 5

1. Что означает запись User-agent: * с последующим Disallow: /?

2. Что говорит сайту директива Crawl-delay: 2?

3. Сервер ответил кодом 429. Что делать?

4. Почему запросы с User-Agent python-requests/2.31.0 часто получают 403?

5. Какие данные нельзя собирать парсером без правового основания?

Карточки терминов
Запомнено: 0 / 6
Практика

Разберите robots.txt магазина через urllib.robotparser и напечатайте вердикты для трёх адресов и задержку. Проверьте /catalog/page/1/, /profile/settings/ и /admin/login/ методом can_fetch для агента * и выведите каждую строку в формате «путь - True/False», затем «Задержка: N» из crawl_delay.

practice.py
Вопросы и ответы по уроку

Нужно ли учитывать robots.txt при парсинге?

Да. Это общепринятый договор между сайтом и роботами: его соблюдают поисковые системы, а игнорирование легко превращает вас из гостя в нарушителя — от бана по IP до претензий со стороны владельца. Разбирать файл удобно стандартным urllib.robotparser: скормили строки — спрашиваете can_fetch по каждому адресу.

Как задать задержку между запросами при парсинге?

time.sleep перед каждым запросом, лучше со случайным разбросом: time.sleep(random.uniform(1, 3)). Ровный метроном из одинаковых пауз выглядит как робот в логах. Если сайт указал Crawl-delay в robots.txt — берите максимум из него и вашей паузы.

Как обойти ошибку 403 при парсинге?

Легальные пути: поставить осмысленный User-Agent вместо дефолтного python-requests, снизить темп запросов, проверить robots.txt и добавить таймаут с cookies сессии. Если 403 остаётся — сайт сознательно не хочет видеть автоматические запросы, и «обходить» это не стоит: ищите API или пишите владельцу за разрешением.

Законен ли парсинг в России?

Сбор публичных неконфиденциальных данных — цен, названий, заголовков — обычно правомерен при соблюдении robots.txt и разумной нагрузки. Красные линии: персональные данные (152-ФЗ требует правового основания даже для публичных ФИО и телефонов), страницы за авторизацией, обход защиты и копирование больших массивов чужого контента. При сомнениях — письменное разрешение владельца сайта.

Как проверить robots.txt кодом перед парсингом?

Стандартным urllib.robotparser: RobotFileParser().parse(robots_txt.splitlines()), затем rp.can_fetch("*", "/catalog/") по каждому адресу — вернёт True или False, а rp.crawl_delay("*") достанет требуемую паузу. Сеть не нужна: строки файла скармливаются парсеру офлайн.

Понравился урок? Сошлитесь на него

«Правило API-first я бы поставил первым в любом курсе парсинга: сначала официальный источник, потом RSS, и только затем разбор HTML.»

Скопируйте готовую ссылку в формате HTML, Markdown или чистый адрес и вставьте в статью на Habr, VC, Telegram-канал или свой блог — так о проекте узнают новые читатели.

TelegramVK

Похожие уроки по темам

Подобраны автоматически по пересечению тем и ключевых слов.

Проверьте знания по BeautifulSoup / Scrapy

В челлендже — 20 задач по BeautifulSoup / Scrapy, по 2 из каждого урока этого раздела. Формат: фрагмент кода и четыре варианта — что напечатает. После ответа — вердикт и объяснение со ссылкой на урок-источник.

Тест по BeautifulSoup / Scrapy: 20 задач