Skip to content

scrape-helper

scrape-helper отвечает за автоматический сбор данных из внешних источников.

Что делает

  • запускает задания по cron;
  • собирает данные с подключённых площадок;
  • переводит внешний ответ в единый CollectedRawRecord;
  • формирует raw-события source.raw.v1;
  • репортит SourceRun статусы в backend;
  • валидирует события по JSON Schema;
  • публикует их в RabbitMQ;
  • загружает артефакты в S3/MinIO;
  • отправляет проблемные элементы в quarantine-очередь.

Поддерживаемые источники

КодЧто собираетОсновной артефактОсобенности
easuzзакупки ЕАСУЗ Московской областиRAW_HTMLHTML-поиск и детальные карточки
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_JSONJSON API-источник

Как устроен жизненный цикл парсера

1. Планировщик и runtime

Сервис поднимается один раз, резолвит список адаптеров по ENABLED_SOURCES и дальше запускает их по cron-расписанию.

Ключевая точка входа находится в scrape-helper/src/main.ts:

ts
const resolvedSources = resolveEnabledSources(config);
const adapters: SourceAdapter[] = resolvedSources.adapters;

await Promise.allSettled(adapters.map((adapter) => runAdapter(adapter)));

2. Каждый адаптер возвращает единый контракт

Базовый интерфейс очень маленький, и это намеренно. Источник отвечает только за сбор и первичную упаковку:

ts
export interface SourceAdapter {
  code: string;
  name: string;
  collect(context: SourceRunContext): Promise<CollectedRawRecord[]>;
}

CollectedRawRecord содержит:

  • url исходной карточки;
  • raw сырой payload для последующей нормализации;
  • metadata с контекстом адаптера;
  • artifacts для HTML, JSON, PDF и других исходников.

3. Публикация raw-события

После сбора сервис:

  1. загружает артефакты в MinIO/S3;
  2. обогащает payload ссылкой на sourcePageUrl, rawArtifactUrl, checksum;
  3. валидирует событие по contracts;
  4. публикует его в RabbitMQ;
  5. при ошибке отправляет элемент в quarantine.

Форма события соответствует типу RawSourceEvent из scrape-helper/src/types.ts:

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.

ts
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 выдаются площадкой при подключении.

Новая ветка интеграции работает так:

ts
const jwt = await authenticate();
const apiResponse = await fetchJson<FedresursApiSearchResponse>(
  buildApiUrl("v1/messages", {
    DatePublishBegin,
    DatePublishEnd,
    IncludeContent: "true",
    IncludeBankruptInfo: "true",
    Limit: String(maxItems),
    Offset: "0"
  }),
  jwt
);

А затем каждое сообщение преобразуется в наши общие поля:

ts
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 и активных запусков

Пример ручного старта:

bash
curl -X POST http://localhost:3001/api/source-runs \
  -H 'content-type: application/json' \
  -d '{"sourceCodes":["eis","fedresurs"]}'

Основные env-переменные

Общие

  • RABBITMQ_URL
  • QUEUE_RAW_EVENT
  • QUEUE_QUARANTINE_EVENT
  • SCRAPE_SCHEDULE
  • REQUEST_TIMEOUT_MS
  • RETRY_ATTEMPTS
  • RETRY_BASE_DELAY_MS
  • CIRCUIT_BREAKER_FAILURE_THRESHOLD
  • CIRCUIT_BREAKER_OPEN_MS
  • SHARED_CONTRACTS_DIR
  • ENABLED_SOURCES
  • S3_ENDPOINT
  • S3_REGION
  • S3_ACCESS_KEY
  • S3_SECRET_KEY
  • S3_BUCKET
  • S3_FORCE_PATH_STYLE
  • HTTP_PROXY
  • HTTPS_PROXY
  • NO_PROXY

Source-specific, которые особенно важно помнить

  • EIS_SEARCH_TERMS
  • EIS_CONTRACTS_SEARCH_URL
  • EIS_CONTRACTS_223_SEARCH_URL
  • RNP_SEARCH_URLS
  • FNS_LOOKUP_QUERIES
  • FNS_DOWNLOAD_EXTRACT
  • FEDRESURS_API_URL
  • FEDRESURS_API_LOGIN
  • FEDRESURS_API_PASSWORD
  • FEDRESURS_API_LOOKBACK_DAYS

Локальный запуск

bash
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:dev

ENABLED_SOURCES

Значение читается как список через запятую:

bash
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.

Диагностика по слоям

Если источник запускается, но не даёт записей

Смотрите по порядку:

  1. SourceRun статус в backend;
  2. логи scraper-service;
  3. доступность внешнего сайта или API;
  4. корректность поискового окна и фильтров;
  5. не отфильтровал ли запись сам adapter.

Если есть raw, но нет данных в UI

Это уже не scrape-helper, а следующий слой:

  1. RabbitMQ публикация;
  2. processing-worker;
  3. ingest в backend;
  4. нормализация и GraphQL-агрегаты.

Если Fedresurs снова “упал”

Проверьте отдельно:

  1. заданы ли FEDRESURS_API_LOGIN и FEDRESURS_API_PASSWORD;
  2. доступен ли FEDRESURS_API_URL;
  3. не истёк ли JWT или доступ учётной записи;
  4. не пытается ли сервис снова жить только на публичном HTML.

Качество

bash
npm run check
npm run test
npm run build

Техническая и аналитическая документация платформы NPPWEB.