Как мы сделали легковесный парсер данных из официального API госзакупок на C++
В работе с открытыми данными часто сталкиваешься с тем, что готовые решения предлагают доступ к информации, которая и так лежит в публичном API, но «в красивой обёртке» и за деньги. Меня интересовал не готовый сервис, а именно инженерная задача: как стабильно забирать большие объёмы данных из внешнего API, хранить их в реляционной БД, отдавать через REST и при этом укладываться в минимальные ресурсы.
В этом кейсе расскажу про архитектуру и ключевые технические решения, которые позволили держать всю систему на VPS с 1 ядром и 1.5 ГБ RAM.
Идея и требования
Задача была такой:
- Регулярно получать тендеры по 44‑ФЗ и 223‑ФЗ из официального API ЕИС (zakupki.gov.ru).
- Хранить данные в БД с возможностью поиска и фильтрации по регионам.
- Отдавать данные через REST API с пагинацией.
- Поддерживать дополнительные функции (например, рассылку) как отдельные модули, которые можно включать/выключать по необходимости.
При этом система должна быть устойчивой к ошибкам API (429, 504), избегать дублей, не раздувать базу и эффективно использовать память и CPU.
Архитектура
Проект реализован как монолит с чётким разделением компонентов:
- C++ парсер — забирает тендеры из API каждые 15 минут и сохраняет в PostgreSQL.
- PostgreSQL — хранит тендеры, подписки и токены подтверждения.
- C++ API (libmicrohttpd) — отдаёт тендеры через REST API с фильтрацией и пагинацией.
- Flask API — обрабатывает подписки, вебхуки и подтверждение по email.
- Рассылка (Python) — отправляет письма с тендерами (модуль временно отключён).
- Nginx — проксирует запросы к API и отдаёт статику.
Почему C++ для парсера и API
На старте был выбор между Python и C++. У Python — более быстрая разработка, но выше потребление памяти (300+ МБ) и нагрузка на CPU. C++ сложнее в написании, но позволяет добиться минимального потребления ресурсов: парсер стабильно работает в ~20 МБ RAM и практически без нагрузки на CPU в простое. Для слабой конфигурации VPS это оказалось решающим фактором.
Структура данных тендера
Всё начинается с удобной структуры:
Защита от одновременного запуска и повторных попыток
Первые версии парсера падали на 504, создавали дубли при параллельном запуске и не выдерживали 429 Too Many Requests.
PID‑файл и flock
Чтобы не допустить одновременного запуска нескольких экземпляров, используется файловая блокировка:
Теперь можно запускать задачу через cron хоть каждую минуту: если предыдущий процесс ещё работает, новый просто завершится.
Retry‑механизм для HTTP‑запросов
Для устойчивости к временным сбоям и лимитам API реализован простой retry с задержкой:
Это заметно снизило количество сбоев: раньше парсер падал примерно раз в неделю, теперь работает стабильно.
Парсинг JSON и главный цикл
Для работы с JSON используется библиотека nlohmann/json:
Главный цикл последовательно загружает тендеры по двум законам:
Почему не грузим всё разом: чтобы не создавать пиковую нагрузку на API и не «положить» его. Забирая по 100 записей каждые 15 минут, удаётся держать базу актуальной без длительных полных выгрузок.
База данных и оптимизация поиска
PostgreSQL 15 развёрнут в Docker‑контейнере. Основная таблица:
Полнотекстовый поиск вместо ILIKE
Изначально использовался ILIKE '%строительство%', но на 500 000 тестовых записей запрос выполнялся 4 секунды. Решение — перейти на полнотекстовый поиск с GIN‑индексом:
Пример запроса:
Поиск ускорился примерно в 20 раз. Да, теперь нельзя искать по произвольной подстроке, но это разумный компромисс для слабой конфигурации.
UPSERT для предотвращения дублей
Чтобы не плодить дубли и обновлять существующие записи, используется ON CONFLICT:
Очистка старых записей
База не должна расти бесконечно. Cron‑скрипт ежедневно удаляет тендеры старше 30 дней:
Так удаётся удерживать размер базы на уровне 10–12 тысяч записей даже при активном парсинге.
C++ API и пул соединений
Первая версия API открывала новое соединение к БД на каждый HTTP‑запрос. При 10 RPS PostgreSQL быстро упирался в лимит соединений.
Решение — реализовать пул из 5 персистентных соединений:
Под нагрузкой в 50 одновременных пользователей система стабильно держит 5 активных соединений к БД.
Безопасность и защита
Защита от SQL‑инъекций
Вместо ручной конкатенации и экранирования лучше использовать параметризованные запросы. Это стандарт безопасности для работы с БД и гарантированно защищает от инъекций.
Ограничение частоты запросов (rate limiting) в Nginx
Для защиты от спама на уровне веб‑сервера настроен rate limit:
Защита на стороне Flask
Дополнительно реализована простая защита от брутфорса по IP:
Сборка и деплой
Компиляция C++ на слабом VPS (1 ядро) занимает до 15 минут и полностью загружает CPU. Чтобы ускорить деплой, сборка вынесена в GitHub Actions (образ ubuntu:22.04). Готовый бинарник кладётся в artifacts, а на VPS просто скачивается через wget и помещается в /usr/local/bin/. Так удалось сократить время деплоя до ~30 секунд и не держать компилятор на сервере.
Мониторинг и статистика
Для быстрой проверки состояния системы используется простой bash‑скрипт, который выводит:
- общее количество тендеров;
- динамику по дням;
- диапазон цен;
- число активных подписок.
Это позволяет оперативно понять, работает ли парсер и наполняется ли база.
Выводы
Этот кейс показывает, как можно построить легковесную систему для регулярного получения данных из внешнего API, их хранения и выдачи через REST, не раздувая инфраструктуру. Ключевые моменты:
- Устойчивость к ошибкам API: retry‑логика и экспоненциальные задержки.
- Защита от дублей и контроль размера БД: ON CONFLICT и регулярная очистка старых записей.
- Эффективный поиск: GIN‑индексы и полнотекстовый поиск.
- Оптимизация соединений: пул соединений вместо открытия нового на каждый запрос.
- Деплой без компиляции на сервере: сборка в CI/CD и доставка бинарника.
Описанные подходы применимы к любым проектам, где нужно стабильно забирать данные из API, хранить в реляционной БД и отдавать через REST, укладываясь в минимальные ресурсы.
Посмотреть работу сервиса можно здесь:
❓ Ответы на возможные вопросы
А что за баги в API?
Иногда API возвращает тендер, которого нет на сайте ЕИС. На сайте вывешена плашка:
ℹ Внимание: Если при переходе на сайт госзакупок вы видите сообщение «Запрашиваемая страница не существует» — значит тендер был удалён, отозван на редактирование или скрыт заказчиком. Мы показываем только то, что получаем от официального источника.
Были случаи, когда регион в API не совпадал с реальным. Мы не чиним чужие ошибки — мы показываем то, что получили.