scrape-helper
scrape-helper отвечает за автоматический сбор данных из внешних источников.
Что делает
- запускает задания по cron;
- собирает данные с подключённых площадок;
- переводит внешний ответ в единый
CollectedRawRecord; - формирует raw-события
source.raw.v1; - репортит
SourceRunстатусы в backend; - валидирует события по JSON Schema;
- публикует их в RabbitMQ;
- загружает артефакты в S3/MinIO;
- отправляет проблемные элементы в quarantine-очередь.
Поддерживаемые источники
| Код | Что собирает | Основной артефакт | Особенности |
|---|---|---|---|
easuz | закупки ЕАСУЗ Московской области | RAW_HTML | HTML-поиск и детальные карточки |
eis | закупки ЕИС | RAW_HTML | фильтрация по атомному контуру и матчингу станций |
eis_contracts | контракты ЕИС 44-ФЗ | RAW_HTML | отдельный поток договоров |
eis_contracts_223 | договоры ЕИС 223-ФЗ | RAW_HTML | отдельный поток 223-ФЗ |
rnp | реестр недобросовестных поставщиков | RAW_HTML | поиск по нескольким URL / режимам |
fedresurs | банкротные сообщения и сигналы | RAW_JSON через API, fallback RAW_HTML | публичный HTML больше не является стабильным источником |
fns | карточки компаний ФНС | RAW_JSON + опциональный REPORT_FILE PDF | может скачивать выписку |
gistorgi | лоты ГИС Торги | RAW_JSON | JSON API-источник |
Как устроен жизненный цикл парсера
1. Планировщик и runtime
Сервис поднимается один раз, резолвит список адаптеров по ENABLED_SOURCES и дальше запускает их по cron-расписанию.
Ключевая точка входа находится в scrape-helper/src/main.ts:
const resolvedSources = resolveEnabledSources(config);
const adapters: SourceAdapter[] = resolvedSources.adapters;
await Promise.allSettled(adapters.map((adapter) => runAdapter(adapter)));2. Каждый адаптер возвращает единый контракт
Базовый интерфейс очень маленький, и это намеренно. Источник отвечает только за сбор и первичную упаковку:
export interface SourceAdapter {
code: string;
name: string;
collect(context: SourceRunContext): Promise<CollectedRawRecord[]>;
}CollectedRawRecord содержит:
urlисходной карточки;rawсырой payload для последующей нормализации;metadataс контекстом адаптера;artifactsдля HTML, JSON, PDF и других исходников.
3. Публикация raw-события
После сбора сервис:
- загружает артефакты в MinIO/S3;
- обогащает payload ссылкой на
sourcePageUrl,rawArtifactUrl,checksum; - валидирует событие по
contracts; - публикует его в RabbitMQ;
- при ошибке отправляет элемент в quarantine.
Форма события соответствует типу RawSourceEvent из scrape-helper/src/types.ts:
type RawSourceEvent = {
eventId: string;
runKey: string;
source: string;
collectedAt: string;
url: string;
payloadVersion: "v1";
artifacts: ArtifactRef[];
metadata?: Record<string, unknown>;
raw: Record<string, unknown>;
};4. SourceRun-статусы
Каждый запуск источника репортится в backend как RUNNING / SUCCESS / FAILED. Это и есть база для operational-экранов, parser reports и health-метрик.
Если API_INGEST_TOKEN не задан, сервис продолжит собирать данные, но backend не увидит статусы запусков.
Как выглядит реальный source adapter
Ниже упрощённый фрагмент из EIS-адаптера: сначала получаем список карточек, затем тянем детали, фильтруем нецелевые элементы и маппим в CollectedRawRecord.
const links = await client.listNoticeLinks(context.logger, context.requestTimeoutMs);
for (const link of links) {
const { html, notice } = await client.fetchNotice(link.detailUrl, childLogger, timeoutMs);
if (!isRelevantNppItem(notice, { matchedQuery: link.matchedQuery })) {
continue;
}
records.push(
mapEisNoticeToCollectedRecord({
notice,
html,
matchedQuery: link.matchedQuery,
sourceCode: config.code
})
);
}Практический смысл такой:
- клиент отвечает за HTTP и получение внешнего ответа;
- parser вытаскивает поля из HTML/JSON;
- mapper переводит их в
CollectedRawRecord; main.tsуже не знает деталей площадки.
Что важно про парсеры в NPPWEB
Парсер не пишет в базу
Это принципиальная граница: scrape-helper не создаёт Procurement, не считает бизнес-метрики и не принимает доменных решений. Его задача:
- достать внешний ответ;
- упаковать его в единый transport-формат;
- сохранить трассируемость до первоисточника.
Parser и normalizer разделены
Если у нас изменилась HTML-верстка EIS или формат JSON у ГИС Торги, правим scrape-helper.
Если поменялась доменная логика, например матчинги АЭС, дедлайны или derived-поля, это уже чаще processing-worker и/или npp-backend.
Артефакт важен не меньше raw-поля
Артефакт нужен для:
- ручной проверки спорных карточек;
- воспроизводимости парсинга;
- аудита и сравнения “что реально отдал источник”;
- повторной диагностики без нового похода на внешний сайт.
Fedresurs: актуальное состояние интеграции
Fedresurs теперь нужно воспринимать отдельно от HTML-источников.
- публичная страница
Messages.aspxстала SPA и больше не отдаёт стабильный список ссылок; - рабочий путь для устойчивого сбора — официальный REST API
bank-publications-prod.fedresurs.ru; - для него нужны
FEDRESURS_API_URL,FEDRESURS_API_LOGIN,FEDRESURS_API_PASSWORD; - логин и пароль для production Fedresurs выдаются площадкой при подключении.
Новая ветка интеграции работает так:
const jwt = await authenticate();
const apiResponse = await fetchJson<FedresursApiSearchResponse>(
buildApiUrl("v1/messages", {
DatePublishBegin,
DatePublishEnd,
IncludeContent: "true",
IncludeBankruptInfo: "true",
Limit: String(maxItems),
Offset: "0"
}),
jwt
);А затем каждое сообщение преобразуется в наши общие поля:
return {
externalId,
externalUrl,
sourceName: "fedresurs",
sourceType: "bankruptcy",
messageType,
subjectName,
subjectInn,
subjectOgrn,
publishedAt,
description,
checksum
};Control API сервиса
У scrape-helper есть собственный лёгкий HTTP control-plane:
| Метод | URL | Что делает |
|---|---|---|
GET | /health | простой health endpoint |
POST | /api/source-runs | вручную запускает указанные источники |
GET | /api/runtime-config | показывает текущее расписание |
PUT | /api/runtime-config | меняет cron и autoRunEnabled |
GET | /api/runtime-status | показывает состояние runtime и активных запусков |
Пример ручного старта:
curl -X POST http://localhost:3001/api/source-runs \
-H 'content-type: application/json' \
-d '{"sourceCodes":["eis","fedresurs"]}'Основные env-переменные
Общие
RABBITMQ_URLQUEUE_RAW_EVENTQUEUE_QUARANTINE_EVENTSCRAPE_SCHEDULEREQUEST_TIMEOUT_MSRETRY_ATTEMPTSRETRY_BASE_DELAY_MSCIRCUIT_BREAKER_FAILURE_THRESHOLDCIRCUIT_BREAKER_OPEN_MSSHARED_CONTRACTS_DIRENABLED_SOURCESS3_ENDPOINTS3_REGIONS3_ACCESS_KEYS3_SECRET_KEYS3_BUCKETS3_FORCE_PATH_STYLEHTTP_PROXYHTTPS_PROXYNO_PROXY
Source-specific, которые особенно важно помнить
EIS_SEARCH_TERMSEIS_CONTRACTS_SEARCH_URLEIS_CONTRACTS_223_SEARCH_URLRNP_SEARCH_URLSFNS_LOOKUP_QUERIESFNS_DOWNLOAD_EXTRACTFEDRESURS_API_URLFEDRESURS_API_LOGINFEDRESURS_API_PASSWORDFEDRESURS_API_LOOKBACK_DAYS
Локальный запуск
cd ../infra
cp .env.example .env
docker compose --env-file .env -f docker-compose.yml -f docker-compose.apps.yml up -d rabbitmq minio minio-init
cd ../scrape-helper
npm install
npm run start:devENABLED_SOURCES
Значение читается как список через запятую:
ENABLED_SOURCES=eis
ENABLED_SOURCES=easuz,eis,rnp
ENABLED_SOURCES=easuz,eis,eis_contracts,eis_contracts_223,rnp,fedresurs,fns,gistorgiЧто важно
- если указаны только неизвестные источники, сервис стартует без активных адаптеров;
- для части государственных площадок может потребоваться
HTTP_PROXYилиHTTPS_PROXY; - для внутренних сервисов нужно корректно поддерживать
NO_PROXY.
Диагностика по слоям
Если источник запускается, но не даёт записей
Смотрите по порядку:
SourceRunстатус в backend;- логи
scraper-service; - доступность внешнего сайта или API;
- корректность поискового окна и фильтров;
- не отфильтровал ли запись сам adapter.
Если есть raw, но нет данных в UI
Это уже не scrape-helper, а следующий слой:
- RabbitMQ публикация;
processing-worker;- ingest в backend;
- нормализация и GraphQL-агрегаты.
Если Fedresurs снова “упал”
Проверьте отдельно:
- заданы ли
FEDRESURS_API_LOGINиFEDRESURS_API_PASSWORD; - доступен ли
FEDRESURS_API_URL; - не истёк ли JWT или доступ учётной записи;
- не пытается ли сервис снова жить только на публичном HTML.
Качество
npm run check
npm run test
npm run build