Сверено 2026-09-30. Актуальный source-specific transport — sources/rabota-md.md.
Crawler subsystem получает только публично доступные данные с разрешённой глубиной, нормализует их и передаёт в общий pipeline. Он не оценивает кандидата, не выбирает резюме и не отправляет отклики.
JobSource + configuration
↓
adapter registry
↓
access-policy check / discovery
↓
full | incremental | recheck
↓
raw reference → raw details → normalized job
↓
SourceJob upsert + snapshot + scan statistics
↓
canonical deduplication → matching pipeline
RabotaMdAdapter, GenericHtmlSourceAdapter и FixtureSourceAdapter реализуют
один контракт. Site-specific разметка не выходит за пределы adapter/parser.
Перед первым scan и периодически далее adapter проверяет:
- допустимость
base_urlи allowlist доменов; - собственные умеренные rate limits;
- зафиксированные публичные условия;
- отсутствие login/CAPTCHA обхода; site-specific WAF transport требует зафиксированной операторской access-policy проверки и не является общей гарантией юридической разрешённости;
- разрешённые API/feed/sitemap/HTML entrypoints;
- rate/concurrency и redirect policy.
Изменение access policy является health-событием. При запрете или неоднозначности источник безопасно приостанавливается; это не повод искать альтернативный скрытый endpoint.
Discovery выполняется динамически и сохраняет SourceCategory с parent, locale,
URL, active и last-seen. Источник может дополнительно вернуть locales и regions.
URL проходят canonicalization и SSRF-validation до запроса.
Не следует считать заранее известный path постоянным контрактом. Start URL может быть сконфигурирован, но ссылки категорий, pagination и вакансий извлекаются из актуальной разрешённой страницы/structured data.
Исчезнувшая категория не удаляется немедленно: она помечается неактивной только после завершённого надёжного discovery, чтобы сохранить историю и избежать реакции на временную поломку parser-а.
Full scan пытается охватить все активные объявления, которые источник в данный момент публично отдаёт. Он не обещает удалённый или непубличный архив.
Entry points объединяются:
- общая выдача;
- категории и подкатегории;
- региональные/городские выдачи;
- локализованные выдачи;
- разрешённые sitemap/feed/API;
- дополнительные публичные точки, подтверждённые исследованием.
Очередь entrypoints и pagination обрабатывается до доступного конца/явного ограничения. References объединяются по source identity/canonical/localized URL до дорогого details fetch. Все категории/регионы, где встретилась публикация, сохраняются.
ScanRun отражает реальную глубину: discovered categories, entrypoints, pages,
found/new/updated/unchanged, parsing/network errors и checkpoint. Ограничение
глубины сайта или конфигурации явно попадает в summary.
Checkpoint хранится в PostgreSQL в ScanRun, является JSON-сериализуемым и
с adapter-specific состоянием. Текущий DTO ScanCheckpoint не имеет отдельного
поля version: adapter хранит свою версию в adapter_state. Содержимое:
- версия adapter/config;
- scan mode;
- pending/finished entrypoints;
- текущий page/cursor;
- компактный набор уже обработанных reference IDs;
- counters и timestamp;
- cursor-specific stop state.
Обновление checkpoint и сохранение обработанной порции выполняется согласованно.
Upsert по (source_id, external_job_id)/устойчивой identity делает повтор порции
безопасным. Нельзя хранить единственную копию checkpoint только в Redis.
После несовместимого изменения adapter checkpoint не интерпретируется молча: run помечается требующим controlled restart, старый checkpoint остаётся в диагностике.
Инкрементальный режим оптимизирован для запуска каждые несколько часов:
- заново выполняет лёгкий discovery категорий;
- читает свежие страницы общей выдачи;
- читает первые configured pages каждой категории/региона;
- сравнивает external ID, content hash,
published_atиupdated_at; - обнаруживает поднятые вакансии, даже если publication date старая;
- прекращает entrypoint после configured последовательности известных неизменённых записей;
- продолжает другие entrypoints независимо.
Порог неизменённых записей не применяется глобально: иначе большой известный раздел может скрыть новую вакансию в другом регионе/категории.
Fetch layer обеспечивает:
- ограниченный user agent/fingerprint выбранного transport; generic HTTP и site-specific WAF/browser transports имеют разные настройки;
- per-source token bucket/rate limiter;
- ограниченную concurrency;
- timeouts и ограниченные retries только для безопасных GET;
- backoff/jitter для 429/5xx;
- максимальный размер ответа;
- allowlist content types;
- проверку каждого redirect;
- метрики без сохранения лишнего body.
HTML parser предпочитается browser automation. Playwright разрешён только для конкретного adapter/config, с теми же URL/egress ограничениями, без login и CAPTCHA обхода. Для Rabota standard PROD fallback выключен; emergency browser профиль требует отдельного разрешения.
Нормализация сохраняет None, если поле отсутствует. Даты приводятся к UTC с
сохранением исходного значения/locale metadata, salary — к min/max/currency без
догадки, HTML — к безопасному тексту. Ответ vacancy page не может изменять
конфигурацию или дать команду системе.
SourceJob — конкретная публикация конкретного источника. Upsert обновляет
last_seen_at всегда, но JobSnapshot создаётся только при существенном изменении
нормализованных полей/content hash. first_seen_at не перезаписывается.
Отдельно сохраняются:
published_atисточника;updated_atисточника;first_seen_at/last_seen_atсервиса;last_checked_atrecheck;- localized URLs и категории появления;
- raw metadata в минимальном безопасном объёме.
Внутрисайтовая identity сначала использует (source, external ID), canonical и
localized URL. Затем общий dedup связывает SourceJob с CanonicalJob по набору
сигналов: normalized company/title/location, employer domain/contact, salary,
dates, content hash и semantic similarity.
Слабое сходство не приводит к необратимому merge. Исходные SourceJob сохраняются, relation можно разъединить. Уникальность отклика контролируется на CanonicalJob в области профиля, а не только на URL публикации.
Периодическая выборка active jobs проверяет существование и изменения. Переход:
active → possibly_closed → closed
closed устанавливается при явном корректно распарсенном сообщении о закрытии
или после configured числа независимых подтверждённых отсутствий. Timeout,
DNS failure, 429, 5xx, CAPTCHA, login redirect и parser anomaly не увеличивают
счётчик подтверждённого отсутствия.
Если одновременно исчезает большая доля страниц или обязательных элементов, срабатывает circuit breaker источника: recheck transitions и downstream applications приостанавливаются.
Health evaluator сравнивает scan с baseline и абсолютными guards:
- нулевой или аномально низкий результат;
- parsing error ratio;
- обязательные поля/ID отсутствуют;
- неожиданные host/path/redirect;
- 403/429/CAPTCHA/login;
- access-policy change;
- content/URL structure drift.
Решение degraded должно сопровождаться alert и безопасной диагностикой. При этом:
- источник не создаёт applications из неполных записей;
- массовое закрытие отключено;
- остальные источники продолжают работу;
- исторические записи не удаляются.
URL валидируется при конфигурации, discovery и непосредственно перед каждым сетевым соединением. Запрещаются:
- localhost и
127.0.0.0/8; - RFC1918 private IPv4;
- link-local, unspecified, multicast и reserved ranges;
- IPv6 loopback, ULA и link-local;
- cloud metadata endpoints;
- URL с embedded credentials;
- host вне source allowlist;
- redirect на запрещённый host/IP.
DNS-resolve проверяется для всех адресов; соединение должно быть привязано к проверенному результату или повторно проверять IP, чтобы снизить DNS-rebinding. Сетевой egress firewall дублирует запреты приложения.
URL из описания вакансии не становится автоматически crawl target. Employer URL, contact evidence и application URL проходят отдельную allowlist/official-domain проверку.
У каждого источника собственные full/incremental/recheck schedules. Celery Beat
публикует задания, worker получает distributed lock по (operation, source_id).
Lock имеет ограниченный TTL и owner token; освобождать чужой lock нельзя.
После scan processing запускается по сохранённым IDs/range, а не по недоверенным командам source. Ошибка processing не должна заставлять повторно скачать весь сайт.
Fixture adapter и локальный fixture site являются полноценным независимым источником для CI/E2E, а не условием в production pipeline. Он моделирует другую HTML-структуру, pagination, локали, старые/новые/updated/raised/closed jobs, межсайтовый дубль, scam и prompt injection. Fake provider разрешён в тестовом окружении; production конфигурация не должна случайно считать fixture контакты реальными.
- access-policy status записан;
- source/config/adapter version известны;
- rate limit и concurrency соблюдены;
- все entrypoints и stop reasons посчитаны;
- checkpoint обновляется;
first_seen_atне изменился у старых jobs;- snapshots создаются только для значимых изменений;
- localized pages объединены;
- network error не стал closed;
- source degradation блокирует downstream;
- логи не содержат PII/полный HTML;
- report агрегаты совпадают с ScanRun/BatchScanRun.