惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

AI
AI
博客园 - 叶小钗
Blog — PlanetScale
Blog — PlanetScale
Microsoft Azure Blog
Microsoft Azure Blog
Vercel News
Vercel News
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
MyScale Blog
MyScale Blog
大猫的无限游戏
大猫的无限游戏
A
About on SuperTechFans
量子位
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
博客园 - 【当耐特】
Martin Fowler
Martin Fowler
阮一峰的网络日志
阮一峰的网络日志
D
Docker
Jina AI
Jina AI
Cyber Security Advisories - MS-ISAC
Cyber Security Advisories - MS-ISAC
The Register - Security
The Register - Security
J
Java Code Geeks
S
SegmentFault 最新的问题
月光博客
月光博客
G
Google Developers Blog
美团技术团队
Last Week in AI
Last Week in AI
L
LangChain Blog
Apple Machine Learning Research
Apple Machine Learning Research
T
The Blog of Author Tim Ferriss
腾讯CDC
Recent Announcements
Recent Announcements
Recorded Future
Recorded Future
The Cloudflare Blog
有赞技术团队
有赞技术团队
博客园_首页
博客园 - 聂微东
人人都是产品经理
人人都是产品经理
B
Blog
I
InfoQ
CTFtime.org: upcoming CTF events
CTFtime.org: upcoming CTF events
F
Fortinet All Blogs
B
Blog RSS Feed
Engineering at Meta
Engineering at Meta
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
Microsoft Security Blog
Microsoft Security Blog
MongoDB | Blog
MongoDB | Blog
爱范儿
爱范儿
D
DataBreaches.Net
F
Full Disclosure
M
MIT News - Artificial intelligence
博客园 - 司徒正美
H
Help Net Security

Все публикации подряд на Хабре

Ловим музу за клавиатуру: как айтишнику стать автором Что умеет Midjourney в 2026? Мой немного грустный разбор этого шикарного инструмента Никто не любит писать тесты, но ИИ может исправить это IPv8 выглядит как мечта. Поэтому почти наверняка не взлетит Производители вернули в продажу материнки с DDR3. Что происходит? Управление агентом с телефона через Telegram теперь в KodaCode От координации к лидерству: как меняется роль руководителя разработки Я сделала родителям бизнес вместо пенсии: зарабатываем 70 тысяч, мама не даёт продать В три раза быстрее приемка товара и оптимизация трудозатрат на 73%: как «РСТ-Инвент» помог Gulliver Group ИИ-шечный мир победил? О влиянии искусственного интеллекта на игропром Кремль снижает давление на Телеграмм пока Европа строит интернет по паспорту Как CEO, CTO и CIO за 8 часов собрали ИИ-директора, который умеет держать позицию под давлением Как (не) потерять домен за выходные Вместо 8 разных VPS: как я организовал практику студентам на одном сервере Почему твой Open Source проект не замечают? R&D: искусство управления неопределенностью в разработке AI-дефляция: вакансий для разработчиков больше, а рост зарплат — худший за 15 лет Мы отдали управление роботами OpenClaw. Что из этого вышло Галактический ID: система идентификации для всех форм разумной жизни Шесть основ бизнес-анализа: начинаем с вопроса «Кто в игре?» Код-ревью, в котором дело не в коде Данные переехали. Команда — нет Системной подход к сдаче OSWE в 2025 Почему комната управления реактором покрашена в цвет морской пены 4 YAML-файла вместо PySpark: как аналитикам строить пайплайны без разработчиков LLM-агент для поиска свободных доменов: автоматизируем подбор Когда, зачем и как правильно начинать новую сессию в Claude Code? Как я заставил нейросеть писать макросы для FreeCAD Анатомия ИИ‑агента для подбора персонала. От тысячи резюме к топ‑10 за минуты Опыт разработчика как экономика внимания Автономность как точка невозврата: кто будет субъектом в цифровом будущем Обучение ИИ в «диких» условиях: как рутинные действия превращаются в датасеты Как измерить LLM для задач кибербеза: обзор открытых бенчмарков Где хранить код? Сравнение GitHub, GitLab и Bitbucket Математика объясняет, почему нормальное распределение встречается повсюду Почему ваш FinOps не работает: 12 тезисов от практиков Как подписать проектную документацию УКЭП с использованием бесплатных лицензий Pilot Адаптивное администрирование Sigla Vision Я грузил уран в бочки, а потом 20 лет строил ИТ в атомной отрасли Чем позвонить с Эвереста? История и обзор спутниковой связи. Часть 2 Как языковая модель помогает контролировать качество инструктажей по охране труда в металлургии Как не передать на desktop свой IP в РКН Анатомия SAP Privileges: как устроено управление правами в macOS MoneyDev: Сказка про три главных слова Обновлённый токенизатор видео K-VAE 2.0 от Сбера Как сделать диспетчеризацию дома на 1284 квартиры почти бесплатно Как мы разогнали железную дорогу Мы дали агентам рутину. Теперь надо решить — что делать с освободившимся временем Токсичный контент, промпт-хакинг и защита ИИ — всё о Guardrails для LLM Умный город начинается с точного взгляда: как «Фалькон Тех» меняет пространство к лучшему Навайбкодил приложение для анализа графов Почему Дюну так интересно читать? Упрощаем работу с рутиной или как стать Гендальфом Белым Деконструкция Go: CPU, RAM и что там происходит. Go Assembler база. Часть 1.1 Какие профессии исчезнут из-за ИИ, а какие появятся? И что с этим делать Как мы построили IT-отдел, где хочется расти: архитектурные встречи, прозрачные метрики и книжные подарки Rufler: Делаем из Claude Code автономный рой через один YAML-конфиг Sing-box и белый список приложений Как построить надёжный обмен сообщениями в микросервисах: лучшие практики для enterprise OpenAI строит MLM-пирамиду, а McKinsey и Accenture помогают ей в этом Дом, который не построил Фишер (Часть 2) «Сверхзвуковой математик» против «Вдумчивого логиста»: битва алгоритмов 3D-упаковки Мультимодальные модели – грубый и дорогой инструмент Разговоры ничего не стоят. Код тоже Проверки физических лиц: с кого начнет ФНС Топ-10 бесплатных нейросетей для создания видео в 2026 году Первые слои кода: как наши решения сегодня определяют архитектуру ИИ на десятилетия Разработка нового статического анализатора: PVS-Studio JavaScript Поиск уязвимостей ПО: базовый минимум или роскошный максимум Почему оценка персонала не работает как инструмент управления Как мы разработали ИИ-ассистента и сократили рутину продуктовой команды на 50% Как я ушел из найма, нажарил косточек и продал на маркетплейсах на 168 млн в год Когда 1С:ERP уже внедрена, а нормального производственного плана всё ещё нет Как я сделал Claude мультимодальным, подключив к нему Qwen Omni Как приглашение на вакансию мечты превращается в атаку Infrastructure as Code: философия и лучшие практики IaC Тестируем Yandex Code Assistant на задаче, в которой нужно хранить секреты nxs-universal-chart v3.0: новое поколение универсального Helm-чарта Callback Injection: Техника, которая отправила Microsoft Defender в глухой нокаут «Все идеи на стол»: митап как способ вывести проект из тупика Сегодня я узнал нечто новое о GPU благодаря багу в своей игре Как заставить LLM ̶ ̶г̶а̶л̶л̶ю̶ ̶ эволюционировать Карта событий как фундамент аналитики: практический кейс для E-commerce Что выбрать для AI: x86, ARM или RISC-V? Дайджест железа за март Роль соматических мутаций в развитии аутоиммунных заболеваний: путь к избирательной терапии Mythos от Anthropic — тревожный сигнал для всех, а не только для банков Guardrails для LLM на Java: как приручить промпт‑инъекции и токсичные ответы Green-VLA: как мы собрали VLA-модель для реального антропоморфного робота и не потеряли обобщение Финансовая гонка вооружений: почему умные люди добровольно в ней участвуют Эра ИИ-агентов наступила: выбираем лучшего цифрового сотрудника # Практический опыт внедрения WinCC Redundancy на производственном предприятии Сделал MVP за 3 дня, а потом неделю прикручивал оплату. Оно того стоило? Физика против Маска: почему Starship V3 может оказаться ещё одной катастрофой Нефть Венесуэлы: крупнейшие запасы в мире, но не крупнейшая нефтяная держава JPA 4. Переосмысление Hibernate Почему зеркальная фотокамера Nikon D5 десятилетней давности идеально подошла для миссии «Артемида-2» Проект «Уровень-Спутник» или как мы сделали платформу для гидрологов «Замедлиться, чтобы ускориться»: почему ИИ повышает цену ошибок в требованиях и архитектуре Как с нуля поднять трафик IT-компании на 1657% при бюджете 55 тыс. и выжить Pixel-perfect Downsampling — идеальная отрисовка 50 миллионов точек без потерь
Парсил zakupki.gov.ru без API — расскажу что узнал
Александр · 2026-05-30 · via Все публикации подряд на Хабре

Средний

7 мин

12K

Делаю pet-проект — приложение, чтобы свайпать тендеры в телефоне и видеть AI-скоринг заказчика. Идея простая: свайпнул, посмотрел «ваш шанс — высокий/средний/низкий», дальше принимаешь решение, лезть в этот тендер или нет.

Чтобы скоринг был не из воздуха, нужно собрать всю историю заказчика — какие контракты у него были, как он платил, какой типичный дисконт от стартовой цены. Источник один — ЕИС zakupki.gov.ru. И вот тут я наступил на грабли которыми и хочу поделиться.

Если кто тоже думает парсить госзакупки — пост сэкономит вам пару недель.

Что у Минфина есть из официального

Перед тем как идти парсить HTML я честно попробовал три «официальных» способа. Все три отвалились.

Первый — SOAP-API на int44.zakupki.gov.ru. Документация есть, схемы XSD есть, метод getCustomerDocs есть. Чтобы подключиться нужна квалифицированная ЭЦП юр.лица. Я физлицо. не подходит.

Второй — FTP-сервер с XML-выгрузками. Был. Закрыли с 1 января 2025, в объяснении что-то про оптимизацию инфраструктуры. Видел старые статьи на Хабре где люди этим пользовались — теперь не пользуются.

Третий — портал opendata.gov.ru. Звучит как обещание счастья, но это просто красивый каталог. Заходишь, видишь «Реестр контрактов 44-ФЗ», кликаешь — попадаешь на страницу с описанием датасета. Ссылок на скачать ничего нет. Кнопка «API» ведёт обратно на сам же ЕИС, замкнутый круг.

Я много времени убил. Делал curl на каждый passport, грепал по zip/xml/csv. Ничего не работало. Если кто-то реально через них что-то скачал — буду благодарен в комментариях, может я не догадался до чего то.

Остаётся то с чем у любого нормального человека всё работает — публичные HTML-страницы в браузере.

Какие страницы вообще нужны

Пять штук:

  1. /epz/order/extendedsearch/results.html?fz44=on&regions=... — лента тендеров по 44-ФЗ

  2. То же самое с fz223=on — лента 223-ФЗ

  3. /epz/order/notice/{type}/view/common-info.html?regNumber=X — открытие конкретного тендера 44-ФЗ

  4. /epz/contract/contractCard/common-info.html?reestrNumber=Y — общая инфа по контракту

  5. /epz/contract/contractCard/document-info.html?reestrNumber=Y — там лежат акты приёмки

Все рендерятся серверно, SPA нет, JS не требуется. Обычный парсинг через SwiftSoup (Swift-порт jsoup) или любой аналог в вашем стеке.

Чем парсю

Я пишу на Swift, бэкенд на Vapor 4 — потому что знаю Swift и Vapor нормальный фреймворк без бойлерплейта Spring и без какашек Express. PostgreSQL для тендеров и контрактов, Redis для кэша HTML-страниц и mapping orgId→ИНН.

Флоу грубо такой: юзер открывает ленту → бэк дёргает feed-страницу ЕИС → парсит HTML в список карточек → достаёт ИНН заказчиков → джойнит с табличкой customer_scores → возвращает клиенту. Cold-cache около 100мс, hot-cache — 5-15мс. С учётом сетки до ЕИС.

Дальше — где были проблемы.

44-ФЗ и 223-ФЗ совершенно разные

Сначала я думал что страницы для 44-ФЗ и 223-ФЗ — просто разные параметры в одном URL.

У них:

  • Разная вёрстка, классы и айдишники не совпадают

  • Идентификатор заказчика по-разному: 44-ФЗ даёт ?organizationId=12345, 223-ФЗ — ?agencyId=618991

  • Поиск контрактов работает по разному URL вообще: 44-ФЗ через /contract/search/results.html?customer={inn}, 223-ФЗ — /organization/view223/info.html?agencyId={X} и там JSON совершенно другой формы

В итоге два полностью раздельных парсера за общим интерфейсом. И если что-то сломается в вёрстке — два места править. Но другого пути нет.

Идентификатор заказчика — три формата

Это была самая болезненная часть.

ЕИС использует три разных идентификатора для одного и того же юр.лица:

  • Настоящий ИНН — 10 цифр у юр.лиц, 12 у ИП. Например 7708410783

  • organizationId — внутренний 5-8-значный ID, например 2225253

  • organizationCode — 11-значный код, например 01795000003

Я этого не знал когда начинал. Делал парсер, всё нормально работало, потом в какой-то момент стал замечать что у части тендеров на ленте AI-скоринг «нет данных». Лезу в БД — там скоринг есть! Под другим ключом.

Оказалось что feed-парсер вытаскивает organizationCode (то что в URL карточки), а в табличку customer_scores я писал по реальному ИНН (то что приходит с детальной страницы). При сопоставлении промах.

Когда конкретно ЕИС добавил organizationCode — я не отследил, может быть постепенный rollout по регионам. Просто заметил что в фид-листинге сейчас могут прилетать оба формата и оба нужно обрабатывать.

В итоге сделал bridge через Redis. Идея простая: как только хоть один раз резолвим связку “orgCode = real_inn” (например после открытия деталки тендера), записываем mapping под всеми возможными ключами:

private func writeAllAliases(_ ids: CustomerIds) async throws {
    let json = try JSONEncoder().encode(ids).asString()
    // forward
    try await redis.setex("customer_ids:" + ids.inn, 30*24*3600, json)
    // reverse под orgId
    if let orgId = ids.organizationId {
        try await redis.setex("customer_ids_byorg:v2:\(orgId)", 30*24*3600, json)
    }
    // reverse под orgCode
    if let orgCode = ids.organizationCode {
        try await redis.setex("customer_ids_byorg:v2:\(orgCode)", 30*24*3600, json)
    }
}

И потом при поиске scoring канонизируем любой входной идентификатор через эту таблицу:

for inn in inns {
    if inn.count == 10 || inn.count == 12 {
        // уже реальный ИНН, оставляем как есть
        canonicalByOriginal[inn] = inn
    } else {
        // orgId или orgCode — резолвим из Redis
        let cached: CustomerIds? = try? await readJson("customer_ids_byorg:v2:\(inn)")
        canonicalByOriginal[inn] = cached?.inn ?? inn
    }
}

До этого фикса в БД находилось примерно 5% запрошенных скорингов. После — около 85%. Остальные 15% — холодные заказчики, которых никто ещё не открывал, для них mapping не создавался, обогащаются ленивым резолвом.

Кстати с префикса v2: сразу советую начинать. Я сначала писал без префикса, потом понадобилось менять формат и старые ключи болтаются с устаревшими данными. Версионирование решает.

НМЦК не там где я думал

Долго разбирался с парсером для страницы /contract/contractCard/common-info.html?reestrNumber=X. URL называется “info.html”, в голове логично — там должна быть инфа о контракте. Достаю поля по их подписям, парсю — НМЦК не нахожу. Дата электронного акта приёмки — тоже не нахожу.

Психанул, открыл DevTools, начал тыкать другие страницы. Выяснил:

НМЦК (начальная максимальная цена) живёт не на странице контракта, а на странице извещения: /epz/order/notice/ea44/view/common-info.html?regNumber={pn}. Там есть label «Начальная (максимальная) цена контракта». Покрытие правда грустное — около 4-5% от тендеров 44-ФЗ, остальные публикуют НМЦК диапазоном типа “не более 1М ₽” что для скоринга бесполезно.

Дата эл.акта приёмки — на отдельной странице document-info.html?reestrNumber={Y}. Там в HTML строки вида «акт № (N) от ДД.ММ.ГГГГ». Берём максимальную дату среди актов с этапным номером — это и есть дата фактического закрытия контракта. Покрытие хорошее, около 100%.

А вот тот common-info.html который я долго разбирал — там просто общая инфа, цена факта, дата заключения, срок исполнения. НМЦК и elact_at туда не положили. Моя ошибка в том что я полез писать парсер не проверив сначала что данные вообще на странице есть.

Strict mode vs толерантный парсер

Первая версия парсера НМЦК у меня была «умная»:

func parseNoticeNmck(html: String) -> Decimal? {
    // canonical label
    if let v = extractAfterLabel(html, "Начальная (максимальная) цена контракта") {
        return parseDecimal(v)
    }
    // fallback на синоним
    if let v = extractAfterLabel(html, "Максимальное значение цены договора") {
        return parseDecimal(v)
    }
    return nil
}

Звучит логично — если основной label не нашли, попробуем синоним, авось то же самое. На одном тестовом контракте этот fallback мне дал discount 99.6%. Заказчик в БД вдруг стал «гениально дисконтить», скоринг покрасился зелёным.

Открываю руками, смотрю — а там фактическая цена 1.2М, а «Максимальное значение цены договора» вернуло 35М. Потому что это был рамочный контракт на много лет, и максимум — это лимит всей суммы за все годы вперёд, а не НМЦК конкретной закупки.

Урок банальный — для финансовых полей strict mode лучше чем толерантность. Если canonical label не нашли — return nil. nil лучше чем neправильное число которое поедет в формулу и сломает результат.

Заодно поставил anti-outlier guard в формулу margin:

let valid = contracts.filter { c in
    guard let nmck = c.maxPrice, nmck > 0,
          let price = c.price, price > 0 else { return false }
    let discount = 1.0 - Double(price) / Double(nmck)
    return discount < 0.80  // больше 80% дисконта — почти наверняка outlier
}

Грубо но эффективно. Реальные дисконты редко больше 30-40%, всё что выше 80% — почти всегда либо ошибка парсинга, либо рамочник который проскочил strict-mode.

Rate limit и пустые ответы Varnish

ЕИС не публикует rate limits, мерил эмпирически. У меня вышло:

  • 8 req/s — комфортно, 429 не ловлю

  • 15 req/s — иногда 429 на всплесках

  • 30 req/s — стабильно ловлю 429 в 30% случаев

Остановился на 3 параллельных запроса для тяжёлой задачи (обогащение контрактов). Пробовал 5 — на customer’е с 316 контрактами получил 30% rate-limit. На 3 уже почти 0%.

Ещё прикол — у ЕИС перед бэком стоит Varnish и иногда отдаёт 0-байтные ответы. Видел такое раз из 4-5 на document-info при cold cache. Лечится retry с exponential backoff:

for attempt in 0..<3 {
    do {
        let response = try await client.get(url, timeout: 15)
        if response.body.isEmpty { throw EisError.emptyResponse }
        return response.body.string
    } catch {
        try await Task.sleep(seconds: pow(2.0, Double(attempt)))
    }
}
throw EisError.gaveUp

302 редиректы тоже фолловлю руками а не через стандартный механизм клиента — нужен был контроль над количеством хопов для логирования. По-моему overkill, но работает.

Кэш — по разному для разного

С TTL я сначала пытался обойтись одним глобальным значением. Быстро понял что глупость. Лента тендеров — обновляется каждые 5 минут, новые тендеры публикуются постоянно. А mapping ИНН → orgId — да он по сути никогда не меняется, юр.лицо своё name свой ИНН не теряет. Зачем им один TTL.

Сейчас у меня примерно так:

  • лента — 5 минут

  • карточка тендера — сутки

  • акты приёмки — неделя

  • НМЦК извещения — 90 дней

  • mapping ИНН — 30 дней

И отдельно для негативных кэшей (404, пустые ответы) — короткий TTL, час-сутки, чтобы не дёргать ЕИС в пустую. Без negative cache юзер скроллит ленту → каждый swipe долбит сетку → сразу же ловишь rate limit.

Что в итоге

После долгой разработки:

В БД 128 тысяч контрактов, около 340 уникальных заказчиков с заполненным скорингом по 4 метрикам (надёжность, активность, маржа, стабильность). Покрытие около 85% от запрашиваемых скорингов, остальное — холодные заказчики которые обогащаются по первому запросу за 3-15 секунд.

Latency feed-запроса на hot cache 15мс, на cold 100мс. Глубокое обогащение customer’а с большим количеством контрактов — 30-90 секунд в фоне, это нормально, юзер этого не видит, scoring появится через SSE когда recalculate отработает.

Где сейчас слабо

Покрытие НМЦК для 44-ФЗ — всего 4-5%. Большая часть аукционов публикует НМЦК как диапазон что для расчёта margin score бесполезно. Думаю где брать точные значения, пока не нашёл.

Mapping для холодных заказчиков делает один HTTP к ЕИС, около 500мс cold. Если в ленте 50 неизвестных orgId — это 25 секунд latency на резолв всех. Решаю через async warmup — запускаем резолв в фоне после возврата ответа, к следующему feed-запросу всё в кэше.

Вёрстка ЕИС иногда меняется без предупреждения. Каждые несколько месяцев что-то ломается — добавляется класс, переименовывается ID. Нужен monitoring парсера в CI на нескольких известных тендерах. Пока делаю руками когда вижу баги от тестеров.

Парсер 223-ФЗ слабее чем 44-ФЗ. У 44 я месяц копал, у 223 пока половина методов работает в режиме best-effort. Этот разрыв закрываю.

P.S.

Парсер использую в pet-приложении «ГосЛоты» — мобильный клиент для свайпа госзакупок. Сейчас open beta в RuStore, бесплатно. Если кому интересно как продукт устроен с UX-стороны, могу отдельный пост написать.

И если кто-то знает легальный путь к данным ЕИС без ЭЦП юр.лица — пишите в комменты. Может я реально что-то пропустил из официальных каналов.