Перейти к содержимому
PD
AI-автоматизация 8 мин чтения

Парсинг с расчётом на аудит: слой разрешений внутри пайплайна

У большинства парсеров вообще нет понятия «разрешено». Показываю policy-файл, fetch-гейт, rate-бюджеты и провенанс-логи, которые я добавляю в каждый парсинг-пайплайн, чтобы он выжил после жалобы, бана или аудита от клиента.

PD

Pavel Duglas

AI Automation & MVP Architect

Каждый парсер, который мне доставался в наследство, страдал одной и той же болезнью. Он знал, как забрать страницу. Он знал, что с неё вытащить. И он совершенно не представлял, можно ли ему там находиться. Разрешение жило в голове разработчика, в сообщении в Slack полугодовой давности или вообще нигде.

Пока парсинг был нишевой темой, с этим можно было жить. Сейчас нельзя. Владельцы сайтов стали агрессивно резать автоматический сбор, и инструменты на их стороне развиваются быстрее, чем на нашей. Один неаккуратный краулер способен положить в бан весь IP-диапазон клиента, набить дата-продукт контентом, который клиент не имеет права перепродавать, или притащить письмо от чьего-то юриста ровно в пятницу вечером.

Поэтому я закладываю слой разрешений в каждый парсинг-пайплайн. Это не генератор юридических заключений. Это инженерный артефакт, который превращает вопрос «почему вы забрали эту страницу, с такой частотой, и что вы из неё сохранили?» из археологической экспедиции в обычный SQL-запрос.

Три сценария, которые реально бьют по карману

Прежде чем писать код, надо понимать, от чего вы защищаетесь. По моему опыту это никогда не абстрактная «этика». Это три конкретных события.

Вас забанили на середине проекта. Парсер долбит сайт, сайт добавляет правило в WAF, и пайплайн начинает тихо собирать страницы с ошибками. Без rate-бюджета и без сигнала о здоровье домена вы узнаете об этом через три недели, когда клиент спросит, почему графики стали плоскими.

Вы собрали то, что не имеете права переиспользовать. «Публично доступно» и «можно использовать» это разные вещи. Забрать страницу, чтобы посчитать агрегат, это одно. Перепубликовать её текст это другое. Скормить её в обучающий датасет это третье. Пайплайн, который не фиксирует, под какой сценарий использования собирались данные, потом просто не сможет ответить на вопрос.

Кто-то пожаловался, а у вас нет записей. Дорого стоит не жалоба. Дорого стоит момент, когда вы понимаете, что не можете доказать, что именно делали. Нет логов запросов, нет истории политики, нечем показать, что вы уважали заявленные владельцем правила.

Всё три пункта это проблемы архитектуры. Значит, решаются кодом.

Правило 1: один policy-файл на проект вместо россыпи if

Правила доступа обычно расползаются по аргументам крона, дикту HEADERS, захардкоженному time.sleep(2) и комментарию в коде. Соберите всё в один декларативный файл, который сможет прочитать даже не-разработчик.

# policy/domains.yml
defaults:
  respect_robots: true
  max_rps: 0.5
  max_pages_per_day: 2000
  user_agent: "PavelDuglasBot/1.0 (+https://example.com/bot; ops@example.com)"
  allowed_use: ["aggregate_metrics"]
  retain_raw_html_days: 14

domains:
  shop.example.com:
    source: html
    max_rps: 0.3
    allowed_use: ["aggregate_metrics", "internal_dashboard"]
    notes: "ToS разрешает только личное использование. Тексты карточек не перепубликуем."
    reviewed_at: "2026-01-14"
    reviewed_by: "pavel"

  partner-api.example.net:
    source: api
    api_key_env: PARTNER_API_KEY
    max_rps: 5
    allowed_use: ["aggregate_metrics", "client_delivery"]
    contract: "docs/contracts/partner-2026.pdf"

Что это даёт сразу. Во-первых, добавление нового домена становится осознанным действием с именем ревьюера, а не побочным эффектом wildcard-обхода. Во-вторых, allowed_use даёт нижестоящему коду то, что можно проверить программно. В-третьих, когда клиент спрашивает «откуда эти данные и можем ли мы их продавать?», ответом становится файл в git с историей изменений.

Правило 2: fetch-гейт, через который проходит всё

Ни один модуль пайплайна не вызывает requests.get и не открывает вкладку браузера напрямую. Всё идёт через одну функцию, которая имеет право сказать нет.

def fetch(url, *, use_case, ctx):
    domain = urlparse(url).netloc
    policy = ctx.policy.for_domain(domain)

    if policy is None:
        raise PolicyError(f"{domain} отсутствует в policy/domains.yml")

    if use_case not in policy.allowed_use:
        raise PolicyError(f"{use_case} не разрешён для {domain}")

    if policy.respect_robots and not ctx.robots.allowed(url, policy.user_agent):
        ctx.log_skip(url, reason="robots_disallow")
        return None

    if not ctx.budget.consume(domain):
        ctx.log_skip(url, reason="budget_exhausted")
        return None

    ctx.limiter.wait(domain, policy.max_rps)
    resp = ctx.session.get(url, headers={"User-Agent": policy.user_agent}, timeout=30)

    if resp.status_code in (429, 503):
        ctx.budget.cool_down(domain, retry_after(resp, default=900))
        ctx.log_skip(url, reason=f"backoff_{resp.status_code}")
        return None

    ctx.log_fetch(url, resp.status_code, len(resp.content), use_case)
    return resp

Неизвестный домен это жёсткая ошибка, а не default-allow. Это одно решение сэкономило мне больше нервов, чем всё остальное в списке. Краулеры дрейфуют. Относительная ссылка на сороковой странице уводит вас на сабдомен, который никто не ревьюил, и вы внезапно собираете данные с сайта с совершенно другими условиями.

В Browser Automation Studio работает та же идея, только живёт она в функции, а не в Python-модуле. Я держу функцию PolicyCheck, которая принимает URL и use case, читает JSON-ресурс с политикой и либо даёт зелёный свет, либо валит поток с понятной причиной. Каждый блок навигации сначала зовёт её. Скучно. В этом и смысл.

Правило 3: представляйтесь честно

Я понимаю соблазн. Настоящий Chrome-овский user agent проходит там, где ботовская строка получает 403. И да, в антибот-задачах на целях, которые ждут браузерный трафик, вы всё равно гоняете реальный браузер с реальным отпечатком.

Но между «выглядеть как обычный браузер» и «активно врать о том, кто ты, сайту, который прямо попросил тебя не приходить» есть содержательная разница. По вторую сторону этой линии у вас не будет никакой защиты, если разговор всё-таки случится. Моё правило: если сайт публикует сигналы для контакта (robots с директивами, API, заявленная дата-политика), я представляю бота с URL и e-mail. Если кто-то хочет меня притормозить или попросить остановиться, я хочу, чтобы он дотянулся до меня раньше, чем до юриста.

Практический бонус: идентифицированных ботов удивительно часто пускают в белый список. У меня было два случая, когда владелец сайта поднял мне лимит после одного письма. Это оказалось дешевле трёх недель возни с ротацией прокси.

Правило 4: rate-бюджет принадлежит домену, а не задаче

Классический баг: пять отдельных job вежливо спят по две секунды между запросами, а сайт видит десять параллельных краулеров. Rate limiting обязан работать на уровне домена и быть общим для всех процессов.

Redis более чем достаточно. Один ключ на домен для token bucket, один для дневного счётчика страниц, один для таймстемпа cool-down. Когда домен отдал 429 или 503, все воркеры видят cool-down и отступают вместе. Когда дневной бюджет исчерпан, пайплайн останавливается, а не деградирует в комбайн по сбору страниц с ошибками.

Отдельно я алертю на тихие сбои. Домен, у которого success rate за час упал ниже 80%, либо вас блокирует, либо сменил вёрстку. И то и другое требует человека. И то и другое не должен обнаруживать клиент.

Правило 5: провенанс рядом с каждой строкой

Данные без происхождения это обязательство, замаскированное под актив. Каждая извлечённая запись в моих пайплайнах несёт с собой:

{
  "source_url": "https://shop.example.com/p/1042",
  "domain": "shop.example.com",
  "fetched_at": "2026-01-20T09:14:02Z",
  "http_status": 200,
  "content_hash": "sha256:9f2c...",
  "policy_version": "domains.yml@a71c3f2",
  "use_case": "aggregate_metrics",
  "parser_version": "shop_v4"
}

policy_version это поле, которое пропускают чаще всех и которое важнее всех. Когда вы в марте ужесточаете правило, вам нужно знать, какие строки были собраны под старым. Без этого поля изменение политики означает либо перепарсить всё заново, либо надеяться, что никто не спросит.

Хеш контента заодно даёт бесплатную детекцию изменений и позволяет удалять сырой HTML по расписанию, сохраняя возможность доказать, что именно вы распарсили.

Правило 6: минимизировать на этапе извлечения, а не на экспорте

Большинство пайплайнов всасывают полный HTML, складывают его в S3 навсегда и фильтруют уже на уровне отчётов. Это значит, что в вашем бакете лежат все e-mail, телефоны и комментарии пользователей со всех страниц, к которым вы когда-либо прикасались.

Вытаскивайте нужные поля на этапе парсинга. Ставьте retention на сырой HTML (14 до 30 дней покрывают почти любую отладку). Если на странице есть персональные данные, за которыми вы не приходили, дропайте их прямо в парсере регуляркой и логируйте факт дропа. Чем меньше объём хранимых данных, тем короче любой неприятный разговор.

Лестница эскалации

Прежде чем вообще писать парсер, пройдите этот список сверху вниз и остановитесь на первом, что сработало:

  1. Официальный API или платный дата-фид. Дольше настраивать, зато не ломается от вёрстки и юридически прозрачно.
  2. Sitemap, RSS, JSON-эндпоинты, которые дёргает сам фронтенд сайта. Структурировано и дёшево.
  3. Обычный HTTP-фетч HTML через policy-гейт.
  4. Headless-браузер с реальным рендерингом.
  5. Полноценный антидетект с фингерпринтами и прокси.

Каждая ступень вниз дороже в разработке, дороже в поддержке и рискованнее. Я видел команды, которые начинали с пятого пункта, потому что это самая интересная часть, тогда как второй пункт отдал бы тот же датасет за один вечер. Сначала ищите JSON-эндпоинт. Всегда.

Ретрофит существующего парсера за 90 минут

Переписывать всё не надо. Порядок такой:

  1. Прогрепайте код на все прямые HTTP-вызовы и навигации браузера. Выпишите домены. Этот список и есть ваш первый domains.yml.
  2. Напишите fetch-гейт. Заверните вызовы в него. Неизвестный домен падает с ошибкой.
  3. Перенесите rate limits в Redis с ключом по домену.
  4. Добавьте поля провенанса в схему вывода. Что можете, забэкфильте, остальное помечайте unknown.
  5. Заведите job на удаление сырого HTML по retention.
  6. Поставьте реальный контактный адрес в user agent для тех домены, где это уместно.

Шесть шагов, и пайплайн перестаёт быть чёрным ящиком. В следующий раз, когда клиент спросит, можно ли перепродать датасет, или владелец сайта спросит, почему вы стучитесь к нему 40 раз в минуту, вы ответите запросом и git log, а не пожатием плечами.

Сбор данных это нормальная инженерная дисциплина с нормальными обязательствами. Постройте слой разрешений, пока проект маленький. Ретрофит после жалобы это та же работа, только втрое дороже и под стрессом.

Вопросы и ответы

Не убьёт ли соблюдение robots.txt половину моих проектов?

На практике нет, но важно понимать, что robots это инструмент, а не приговор. У меня policy-файл задаёт respect_robots на уровне домена, а не глобально. Для целей, где robots явно запрещает нужные разделы, решение принимается осознанно и фиксируется в файле с именем ревьюера и пометкой, почему так. Разница между «мы не читали robots» и «мы прочитали, обсудили и записали решение» огромна, когда кто-то задаёт вопросы. И почти всегда сначала стоит проверить пункты 1 и 2 из лестницы эскалации: официальный фид или JSON-эндпоинт снимают вопрос целиком.

Что делать, если проект уже собрал терабайт данных без провенанса?

Не пытайтесь восстановить всё. Сделайте три вещи. Первое: добавьте поля провенанса в схему прямо сейчас, все новые строки идут с ними, старые помечаются use_case=unknown и policy_version=legacy. Второе: если у вас сохранились логи фетчей или структура путей в S3, забэкфильте домен и приблизительную дату, этого часто хватает. Третье: примите решение по legacy-данным явно. Иногда правильный ответ это выделить их в отдельную таблицу и не отдавать клиенту в поставку, пока не перепарсите. Помеченный технический долг лучше, чем невидимый.

Как этот слой уживается с антидетект-браузерами и прокси?

Нормально, потому что это разные уровни. Policy-гейт отвечает на вопрос «можно ли идти на этот домен, под какой use case и с какой частотой». Антидетект отвечает на вопрос «как отрендерить страницу так, чтобы сайт отдал реальный контент браузеру». В BAS я держу функцию PolicyCheck, которую вызывает каждый блок навигации, а rate-бюджет всё равно живёт в общем Redis, чтобы двадцать потоков с разными прокси не превращались в двадцать независимых краулеров на один домен. Общий бюджет по домену тут даже важнее, чем в однопоточном Python-скрипте.

Похожие статьи