plibvi · ingestion · версия 1 · 07.09.2026

Сборщики событий «По душе»: спецификация для инженера

Версия 1 · 07.09.2026 · ветка feature/figma-parity (ingestion-часть совпадает с integration/dev-stand-2026-08-31, HEAD d99a32a; в рабочем дереве лежат незакоммиченные правки порядка CRON_SOURCES и default_city_workers от 04.09.2026 — считать их частью базы).

Документ отвечает на три вопроса: что собирать (реестр площадок и что из него уже сделано), как устроены сборщики (договор, правила, сторожа) и как работать (разведка, карточка, тесты, коммит, выкат, отчёт). Всё, что здесь сказано про код, — измерено или прочитано в коде на дату версии; всё, что помечено «проверить», — не сверено и требует живой пробы.

Исходный список площадок взят из чата владельца от 24.07.2026 (https://claude.ai/share/b81679f0-be18-4e79-b72f-0bc05b30ad50), решения по уже сделанным — из беклога events и docs/internal/03.

0. Как читать и где источники правды

Вопрос Где ответ
Как устроен репозиторий, с чего начать docs/internal/00-index.md → 01-repo-map.md
Домен, схема БД, оркестратор, CLI docs/internal/02-ingestion-core.md
Каждая из 27 площадок: транспорт, ключи, особенности docs/internal/03-ingestion-collectors.md
Путь события от площадки до экрана (четыре шлюза) docs/internal/09-data-flow.md ★
Что уже ломалось и почему сделано именно так docs/internal/11-gotchas.md ★
Сводка требований к сбору (27.08.2026) docs/34-collectors-requirements-brief-2026-08-27.md
Словарь терминов (Событие, Сеанс, Площадка, Источник-семейство, Пригород…) CONTEXT.md
Принятые архитектурные решения docs/adr/0001…, 0002-suburbs-fold-into-big-city.md
Карточки площадок с разведкой и решениями беклог events (доска через MCP tazzz-backlog; коды SRC-*)
Команды docs/internal/12-cheatsheet.md

Правило чтения: перед правкой любого файла из ingestion/ открыть его докстринг. В этом проекте докстринг объясняет причину, а комментарий рядом с кодом называет инцидент («раньше здесь было X, и происходило Y»). Это стиль, который надо воспроизводить, а не ломать.

Правило источника правды: если этот документ расходится с кодом или с docs/internal/*, прав код, затем docs/internal, затем этот документ — и расхождение надо починить в документе.


1. Продукт: что считается ценным событием

«По душе» — афиша живых офлайн-встреч, второй продукт платформы (первый — знакомства «По любви»). Каталог не может стартовать пустым, поэтому его наполняют сборщики. Из чата владельца от 24.07.2026 и решений проекта следует шкала ценности:

  1. Событие с точными координатами и малого формата — главная цель. Лекции, мастер-классы, разговорные и книжные клубы, беседы, йога в парке, прогулки, квизы, настолки, дегустации, волонтёрские выезды. Именно ради них строится продукт (в чате: «события на 8–15 человек»).
  2. Событие с адресом без координат — годится: каталог догеокодирует по своему справочнику OSM (ingestion/geo/*), статус до тех пор needs_geo.
  3. Массовое зрелище (стадион, репертуарный театр, фестиваль) — в каталог берём, но помечаем format=mass: в ленту оно идёт через премодерацию и квоту (FEED_MASS_QUOTA=1), в районную ленту — нет. «Массовая афиша на карте районов — шум, который убивает гиперлокальность» (чат 24.07).
  4. Онлайн — по умолчанию не берём (у онлайна нет места на карте). Берём только по явному флагу прогона там, где площадка смешивает форматы.
  5. Прошедшее — не берём никогда; отсев на стороне сборщика.

Решения владельца, которые действуют на всю работу:

География: Москва — приоритет проверки качества, но справочник знает 91 город (ingestion/collectors/cities.py), и каждый сборщик объявляет, какие из них умеет. Пригороды складываются в большой город (ADR 0002).


2. Что уже сделано

2.1. 27 боевых источников (состояние на 07.09.2026)

Все зарегистрированы в orchestrator/registry.py, описаны в domain.SOURCE_CATALOG, входят в ночную и дневную волну (CRON_SOURCES). auto — политика стенда DEFAULT_POLICIES в events/catalog_import.py (True — карточка публикуется сразу, False — публикует оператор).

Код Площадка Транспорт Городов auto Волна
kudago KudaGo REST kudago.com/public-api/v1.4 13 да 1
leader_id Leader-ID + Точки кипения REST leader-id.ru/api/v4, серверный dateFrom 90 да 1
dobro Добро.рф Hydra API; JWT только для картинок 15 да 1
timepad Timepad Playwright-разбор afisha.timepad.ru (токен отозван площадкой) 8 да 1
data_mos data.mos.ru дампы датасетов: площадки (POI) + спортивные события 1 нет (в ленту нельзя: нет обложек) 1
opendata_mkrf Открытые данные Минкультуры opendata.mkrf.ru, ключ, курсорная пагинация 2 да 1
vk VK API service + user token; сообщества-события 69 нет 1
qtickets Qtickets HTML список + деталь, поддомены городов 3 да 2
kassir Кассир.ру api.kassir.ru + HTML 5 да 2
ticketland Ticketland HTML + /api/show/ 2 да 2
afisha Афиша.ру HTML, лестница httpx → curl → Playwright 5 да 2
yandex_afisha Яндекс Афиша внутренний GraphQL 5 да 2
nethouse Nethouse.События JSON-список + JSON-LD детали 6 нет 3
radario Radario JSON web-api/affiche 6 нет 3
intickets Intickets api.store.intickets.ru 2 нет 3
gorodzovet Городзовёт календарь по дням + CSRF-API 6 нет 3
mtslive МТС Live (бывш. Ponominalu) API + гостевой JWT-cookie 3 нет 3
mosafisha Единая афиша мэрии (Мосбилет, bilet.mos.ru) newsfeed v4 + occurrences 1 да 3
bileter Bileter HTML + гео здания 2 да 3
russpass Russpass api.russpass.ru (rqid) 6 да 3
all_events All-Events Bitrix SSR + load_more 19 да 4
habr_events Хабр Календарь неофициальный /kek/v2/events (page_num!) 9 да 4
hse НИУ ВШЭ (4 кампуса) SSR-анонсы + помесячные окна 4 да 4
ithrhub IT HR HUB (бывш. it-events.com) один GET = весь каталог 3 да 4
venue_cals Календари площадок: ЦМТ, Школа Сколково, events.sk.ru HTML, 3 адаптера 1 да 4
moibiz «Мой бизнес»: 3 именных адаптера + 57 региональных порталов RPC/HTML, стойкий кэш деталей, парковка при блоке по IP 60 да 5
bizassoc Деловые объединения: адаптер opora_msk HTML, год даты достраивается по ленте 1 нет 5

Итого в каталоге на стенде после ночной волны 04.09.2026 (для масштаба): moibiz 1309 событий / 482 площадки, leader_id 1061 / 322. Тестов в репозитории — 1131 (pytest --collect-only, 07.09.2026), все зелёные на 03.09.2026 (982 на тот момент).

2.2. Карточки беклога в «Заблокировано» (5) — с пробой 07.09.2026

Карточка Площадка Причина в карточке Проба с сервера 07.09.2026 Что делать
SRC-TICKETSCLOUD ticketscloud.com разведка не проведена; «партнёрский фид» сайт жив (200, за DDoS-Guard) разведать: есть ли публичная афиша/виджеты организаторов без партнёрства; партнёрский API — решение владельца (§18)
SRC-PONOMINALU ponominalu.ru причина не записана ponominalu.ru → 301 на live.mts.ru/moscow закрыть как дубль mtslive, записать причину в карточку
SRC-2DO2GO 2do2go.ru домен не резолвится (10.08.2026) по-прежнему мёртв оставить; перепроверять раз в квартал
SRC-RUNETID runet-id.com → id-runet.ru новый сайт не отвечает (18.08.2026) по-прежнему мёртв оставить; не писать скрейпер «вслепую»
SRC-EDTECH Нетология / Skillbox / Практикум почти всё онлайн; Нетология отдаёт JSON без ключа netology.ru/backend/api/webinars → 200, JSON низкий приоритет: только очные ДОД с адресом; иначе не делать

2.3. Решения «не делать» (чтобы не завести карточку заново)

Площадка Дата Причина
Random Coffee 02.09.2026 сервис подбора пар, публичной ленты встреч нет
VC.ru «события» 18.08.2026 раздела нет (404), timeline?subsite_id=events — обычная лента постов
ТПП (calendar.tpprf.ru, events.tpprf.ru) 02.09.2026 403 / Bitrix с личным кабинетом; вернуться адаптером в bizassoc, если появится открытая лента
Деловая Россия (deloros.ru) 02.09.2026 JSON-LD только Breadcrumb/Organization, событий в разметке нет
Яндекс Практикум 18.08.2026 единого календаря нет; ДОД — разовые лендинги
Шанинка (msses.ru) 18.08.2026 сайт лежит (timeweb noactive); анонсы только в VK/TG
Тир 4 (Timeleft, Sea Soul, Globi, Invitor, Кавёр, Luma, Partiful) 24.07.2026 решение владельца: не источник
Официальный API Хабра, VK newsfeed.search — закрыт / 238k id → 20 событий

2.4. Что сделано частично (важно не принять за «готово»)


3. Реестр площадок к сбору

Полный список из чата 24.07.2026, разложенный по разделам чата. Колонки: Статус — Готово (код) / Частично / Заблокировано / Отказ / Не начато; Проба 07.09 — что ответил сайт с сервера salam0nn.ru (код HTTP, движок, признаки машинной разметки: jsonld — есть JSON-LD, EVENT-LD — в нём есть @type: Event, ics/rss — есть ссылки на календарь или ленту, timepad-widget — на странице встроен виджет Timepad, ANTIBOT — капча/челлендж, 000 — соединения нет). ⚠️ Проба с сервера ≠ проба с ноутбука: mbm.mos.ru с сервера закрыт по IP, с ноутбука открыт. Всё с 000, 401, 403, ANTIBOT перепроверить с ноутбука.

Приоритет — предложение автора документа по шкале «плотность малых офлайн-событий × гео-полнота × простота»; утверждает владелец.

3.1. Универсальные афиши и агрегаторы (чат, раздел 1)

Площадка Статус Проба 07.09 Комментарий
KudaGo Готово (kudago) — эталон; лицензия требует ссылку и авторство фото
Timepad Готово (timepad) — только разбор афиши браузером; запасных путей не заводить (решение владельца)
Афиша.ру Готово (afisha) — анти-бот; лестница транспортов
Яндекс Афиша Готово (yandex_afisha) — GraphQL
Кассир.ру Готово (kassir) —
Ticketland Готово (ticketland) —
МТС Live Готово (mtslive) — гостевой JWT
Intickets Готово (intickets) —
Ticketscloud Заблокировано (SRC-TICKETSCLOUD) 200, DDoS-Guard партнёрский фид — вопрос владельца; разведать публичные виджеты
Qtickets Готово (qtickets) —
Radario Готово (radario) —
Nethouse.События Готово (nethouse) —
2do2go Заблокировано 000 домен мёртв
Городзовёт Готово (gorodzovet) —
Bileter Готово (bileter) —
Ponominalu Заблокировано → закрыть как дубль 301 → live.mts.ru бренд поглощён МТС Live
Culture.ru / PRO.Культура.РФ Готово через opendata_mkrf pro.culture.ru 200 (Next); culture.ru/afisha 000 партнёрский ключ PRO.Культуры не брали (обязательство выгружать все события региона за 2 суток); взяты открытые данные МКРФ с бесплатным ключом
Leader-ID Готово (leader_id) —
Единая афиша мэрии (mos.ru/afisha) Готово (mosafisha) bilet.mos.ru → mos.ru/afisha
Russpass Готово (russpass) —

3.2. Деловые, образовательные, IT (раздел 2)

Площадка Статус Проба 07.09 Комментарий
IT-Events (it-events.com) Готово (ithrhub) 200, Next домен переехал на ithrhub.com
Runet-ID Заблокировано 000 сайт в перестройке, публичной афиши нет
All Events Готово (all_events) — 19 городов
Конгресс-центры (ЦМТ, Сколково, Технопарк) Частично (venue_cals) — «Технопарк» не выбран
Habr / VC.ru Частично (habr_events) — VC — отказ
Нетология, Skillbox, Практикум Заблокировано (SRC-EDTECH) Нетология JSON 200 почти всё онлайн
ВШЭ, МГУ, РАНХиГС, Шанинка Частично (hse) msu.ru/science/allevents.html 200 (2,9 КБ — заглушка); ranepa.ru/dni-otkrytykh-dverey/ 200 МГУ: whitelist факультетов + ICS где есть; РАНХиГС: ДОД сезонны
Точки кипения Готово (внутри leader_id) — 87 городов, пометка boiling_point
Мой бизнес Готово (moibiz) — 60 городов; описания с деталей — есть
ТПП, Опора, Деловая Россия Частично (bizassoc) — только Опора МСК
Random Coffee Отказ —

3.3. Лектории и просвещение (раздел 3) — не начато

Здесь много площадок продают билеты через Timepad или публикуются в KudaGo — перед адаптером проверить, не течёт ли площадка через уже собранные источники (§13, шаг 2). Проба показала виджет Timepad у РГБ и Мосспорта.

Площадка Проба 07.09 Ожидаемый метод Приоритет
Мослекторий (mos.ru) не пробовано (раздел mos.ru) вероятно часть mosafisha/mos.ru — проверить пересечение средний
Лекторий «Прямая речь» (pryamaya.ru) 200, jsonld HTML + JSON-LD; много платных лекций, часть онлайн высокий
Синхронизация (synchronize.ru) 200 → online.synchronize.ru, Tilda, timepad-widget, DDoS-Guard офлайн-лекции искать отдельно; онлайн не брать средний
Level One (levelvan.ru) 200 HTML; смешаны онлайн/офлайн средний
Арзамас-события, Страдариум (stradarium.ru) 200, Nuxt искать JSON в payload Nuxt средний
Лекторий Достоевский (dostoverno.ru) 200 HTML средний
Политех (polytech.one) 000 перепроверить с ноутбука средний
Планетарий Москвы (planetarium-moscow.ru) 200, Apache HTML; расписание сеансов — mass? (зал) низкий
Дарвиновский, Биологический музеи не пробовано HTML лекториев низкий
ГЭС-2 (ges-2.org) 200, Next искать __NEXT_DATA__/API высокий
Гараж (garagemca.org) 200, Next то же высокий
Еврейский музей и центр толерантности 200, Bitrix, jsonld HTML + JSON-LD средний
Библиотеки Москвы (bibliogorod.ru) ⚠️ 200, но домен отдаёт чужую статью — домен, похоже, ушёл; искать актуальный сайт библиотек (mos.ru/бибгород) — высокий (сотни бесплатных событий/мес)
Иностранка (libfl.ru) 200, Tilda HTML средний
РГБ / Ленинка (rsl.ru) 200, Nuxt, timepad-widget вероятно уже в timepad — проверить низкий

3.4. Культурные площадки и парки (раздел 4) — не начато, кроме справочника

Площадка Статус / проба Метод Приоритет
Мосгорпарк (mosgorpark.ru) + парки: Горького, ВДНХ, Зарядье, Сокольники, Коломенское, Царицыно, Сад Эрмитаж, Аптекарский огород Не начато; mosgorpark.ru 000 у каждого парка свой сайт — семейство parks с адаптерами; координаты парка фиксированные высокий (йога в парке, бесплатные тренировки — ядро ЦА)
ЗИЛ (zilcc.ru) 200, Tilda, DDoS-Guard HTML высокий
ДК Рассвет (dkrassvet.space) 200, Bitrix HTML средний
ГЭС-2 см. 3.3
ЦТИ Фабрика, Хлебозавод, Флакон, Артплей не пробовано обычно Tilda/Bitrix; часть через Timepad средний
Винзавод (winzavod.ru) 200, Bitrix HTML средний
Севкабель, Новая Голландия (СПб) не пробовано HTML; город spb средний
Мои документы / Мой семейный центр не пробовано районные бесплатные события; искать ленту на mos.ru средний
Московское долголетие не пробовано возрастной сегмент 55+ — решение владельца низкий
ДК и клубы data.mos.ru Готово как справочник площадок (data_mos) — —

3.5. Музыка, вечеринки, клубы (раздел 5)

Концерты и стендап уже в большом объёме приходят из kassir, mtslive, radario, qtickets, afisha, yandex_afisha, ticketland. Прямые календари площадок нужны только там, где они дают то, чего нет у операторов (бесплатные, камерные).

Площадка Проба 07.09 Метод Приоритет
Resident Advisor (ra.co) 403 Cloudflare с ноутбука + GraphQL RA; анти-бот низкий
Bandlink / афиши лейблов не пробовано — низкий
Клубы: Mutabor, Arma, Powerhouse, 16 Тонн, ГлавClub, VK Stadium, Урбан, Community не пробовано почти всё через операторов; проверить пересечение низкий
Стендап: Stand Up Store, StandUp Club #1, Edinburgh Comedy не пробовано часть через Timepad/Qtickets средний
Джаз: Клуб Козлова (kozlovclub.ru), Эссе (jazzesse.ru), Butman Club 200 (274 байта — SPA), 200 JS-приложение; искать API средний
Квартирники, Дом на Патриарших не пробовано TG/VK — слой §15 низкий

3.6. Спорт и активности на воздухе (раздел 6)

Площадка Проба 07.09 Метод Приоритет
5 вёрст (5verst.ru) 200, WordPress, rss WP REST wp-json + список стартов по паркам; еженедельная серия → один Event, много Session высокий
Nike Run Club, adidas Runners не пробовано приложения/TG — слой §15 низкий
Мосспорт (mosgorsport.ru) 200, timepad-widget проверить пересечение с timepad; бесплатные тренировки в парках высокий
Йога в парках (программы Мосгорпарка, студии) см. парки высокий
Вело: Let's bike it, Велоночь, ВелоМосква не пробовано TG/VK низкий
Спорт-Марафон (sport-marafon.ru) ANTIBOT (showcaptcha) с ноутбука; лекторий + походы средний
Сап-станции, скалодромы, катки, корты не пробовано расписания открытых сессий — обычно без событий как таковых низкий
Танцы: Danceafisha, dancetime.ru, свинг dancetime.ru 000 перепроверить; сообщества в VK (whitelist) средний

3.7. Экскурсии, прогулки, краеведение (раздел 7)

Площадка Проба 07.09 Метод Приоритет
Tripster 200 → experience.tripster.ru это витрина частных гидов: у «экскурсии» нет даты — это услуга, не событие; брать только групповые с расписанием низкий
Спутник8 (sputnik8.com) 401 закрыт для сервера низкий
Москва глазами инженера (engineer-history.ru) 200 HTML расписание; групповые экскурсии с датой высокий
Москваход, Городские экскурсии, Гуляем по Москве (moscowwalking.ru) 200, Bitrix, DDoS-Guard HTML средний
Музей Москвы не пробовано; часто в KudaGo проверить пересечение низкий
Клубы прогулок (TG/VK) — слой §15 средний

3.8. Книжные клубы, разговорные форматы, настолки (раздел 8)

Площадка Проба 07.09 Метод Приоритет
Книжные с событиями: Фаланстер, Пиотровский, Читай-город, Библио-Глобус, «Москва», Подписные издания, Primus Versus, Достоевский не пробовано у каждого свой сайт/TG; сначала проверить, кто есть в KudaGo/Timepad средний
Книжные клубы («Прочитано», при библиотеках, TG) — слой §15 средний
Разговорные клубы языков (Language Exchange, Mundo Latino, English Speaking Club) — десятки TG/VK — слой §15 + VK whitelist высокий для ЦА
Дебаты («Федерация дебатов», «Диалоги») не пробовано — низкий
Мосигра (mosigra.ru) 200, Variti (анти-бот) с ноутбука; события магазинов средний
Hobby Games (hobbygames.ru) 200, QRATOR, jsonld, rss HTML + JSON-LD игротек; много городов высокий

3.9. Религиозные и духовные сообщества (раздел 9)

Площадка Проба 07.09 Метод Приоритет
Патриархия / Московская епархия (moseparh.ru) 200, WordPress, rss WP REST/RSS: анонсы; беседы и молодёжные клубы — чаще на приходских сайтах и в TG средний
Молодёжные отделы, «Даниловцы» (danilovcy.ru) 200, WordPress WP REST; волонтёрские выезды — пересечение с dobro средний
Лектории Сретенского, ПСТГУ, «Артос», Данилов монастырь, Марфо-Мариинская обитель не пробовано HTML/TG низкий
МЕОЦ (mjcc.ru) 200, WordPress, ics, rss есть календарь .ics — самый дешёвый адаптер раздела высокий (по цене)
Гилель, мусульманские центры не пробовано низкий
Философские кафе, стоицизм, медитация (Тергар, дзен, випассана) — TG — слой §15 низкий

3.10. Гастрономия и напитки (раздел 10) — не начато

Дегустации (SimpleWine, Invisible, винные школы), каппинги (Скуратов, Camera Obscura, ЛЕС), гастромаркеты (Депо, Вокруг света, StrEAT), кулинарные школы (CulinaryOn, Novikov School), social dining. Ни одна площадка не пробована. Ожидаемо: анонсы в TG и через Timepad/Qtickets. Приоритет средний; разведку начать с проверки пересечений (§13 шаг 2), потом Tilda/HTML-адаптеры.

3.11. Творчество и хобби (раздел 11) — не начато

Гончарные и художественные студии, Paint&Wine (ArtVino, «Холст и бокал»), швейные/столярные коворкинги (Rubankov), фотопрогулки (Photoplay), флористика, керамика, театральные студии, импров (BIT). Ни одна не пробована. Это «студийные расписания»: у большинства расписание — форма записи без дат-событий. Брать только площадки с календарём конкретных мастер-классов. Приоритет средний.

3.12. Маркеты, фестивали, сезонные (раздел 12)

Площадка Проба 07.09 Метод Приоритет
Ламбада-маркет (lambadamarket.ru) 200 → lmbd.ru, DDoS-Guard HTML; события редкие, но крупные (mass) низкий
Seasons (seasons-project.ru) 200, WordPress, jsonld WP REST средний
4 сезона, Local Weekend не пробовано низкий
Московские сезоны (moscowseasons.com) 200 → russpass.ru/moscowseasons проверить, отдаёт ли russpass уже эти события низкий
Гаражные распродажи, свопы — TG — §15 низкий
Фестивали.рф домен не угадан (пробован festivali.rf) найти настоящий адрес низкий

3.13. Знакомства и социальные форматы (раздел 13)

Площадка Проба 07.09 Метод Приоритет
Быстрые свидания (bistrosvidania.ru), Дуэт, FastLife bistrosvidania.ru 000 перепроверить средний
Мафия-клубы (Mafclub, турниры) не пробовано низкий
Квиз Плиз (quizplease.ru) 200, Nuxt JSON в payload Nuxt/XHR; расписание по барам и районам; много городов высокий
Мозгва (mozgva.com) 200 HTML высокий
Эйнштейн пати, 60 секунд не пробовано средний
Мир квестов (mir-kvestov.ru) 200, jsonld квест — услуга без даты (как Tripster); брать только события с датой низкий
Questme не пробовано низкий

3.14. Волонтёрство и социальные события (раздел 14)

Площадка Статус / проба Метод Приоритет
Добро.рф Готово (dobro) — —
Даниловцы, Старость в радость danilovcy.ru 200, WordPress проверить пересечение с dobro низкий
Чистые игры (cleangames.org) 200, Bitrix+Tilda, EVENT-LD (@type: Event в разметке) JSON-LD Event — дешёвый адаптер, много городов высокий
РазДельный сбор (rsbor-msk.ru) 000 перепроверить низкий
Ночлежка, благотворительные забеги не пробовано низкий

3.15. Соцсети и мессенджеры (раздел 15) — главный слой микро-событий

Канал Статус Метод Приоритет
VK: события сообществ Готово (vk), 69 городов whitelist + seeds + groups.search; newsfeed.search выключен расширять whitelist районных пабликов — дёшево
Telegram: каналы-афиши, районные и тематические Не начато Telethon + извлечение LLM — отдельный эпик, требования в §15 высокий, но после решений владельца
TenChat Не начато; 200, Bitrix+Nuxt, jsonld HTML/API ленты событий низкий
Instagram Не начато без отдельного решения владельца не делать —
Яндекс Район закрыт замена — районные TG-чаты (§15) —

3.16. Региональное расширение (раздел 16)

Площадка Проба 07.09 Метод Приоритет
Geometria (geometria.ru) 200, Bitrix, /msk/ сеть городских порталов, единый движок — один адаптер на все города высокий для регионов
Выбирай (vibirai.ru) 200, rss городские порталы, RSS средний
«Афиша города N» — ручной реестр доменов низкий
2ГИС события с сервера редирект на /museum закрыт для сервера; с ноутбука; официального API событий нет низкий
Региональные минкульты — уже покрыто opendata_mkrf (данные всех регионов; включены 2 города) — расширить supported_cities дешевле, чем новые парсеры высокий
Городские медиа: Собака.ру (QRATOR, rss), Инде (WordPress, jsonld), It's My City (itsmycity.net) 200 RSS/WP REST анонсов средний

Дешёвое региональное расширение без новых парсеров: у kudago, opendata_mkrf, qtickets, kassir, radario, nethouse, gorodzovet, russpass, hse списки городов короче, чем умеет площадка. Расширение supported_cities — это правка справочника + живая сверка идентификаторов (§13 шаг 6), а не новый код.


4. Очередность работ (волны 6–9)

Предложение автора документа. Принцип из чата 24.07: сначала то, что даёт плотность малых офлайн-событий на карте Москвы, потом регионы, потом самый дорогой слой — соцсети. Внутри волны — от площадок с машинной разметкой (JSON-LD/ICS/RSS/WP REST) к чистому HTML, от бесплатных к платным. Каждый пункт — отдельная карточка SRC-* в беклоге и отдельный коммит.

Волна 6 — «Лектории, парки, культурные центры Москвы» (гео-плотность)

# Карточка Состав Почему первым
6.1 SRC-PARKS семейство parks: Мосгорпарк (если оживёт), Горького, ВДНХ, Зарядье, Сокольники, Коломенское, Царицыно, Эрмитаж, Аптекарский огород йога, тренировки, прогулки — ядро ЦА; координаты парка известны заранее
6.2 SRC-MOSSPORT mosgorsport.ru (виджет Timepad → сперва проверить пересечение) бесплатные тренировки в парках
6.3 SRC-LIBRARIES библиотеки Москвы — найти живой домен (bibliogorod.ru ушёл), Иностранка, Некрасовка сотни бесплатных событий в месяц по районам
6.4 SRC-LECTORIA-2 Прямая речь (JSON-LD), ГЭС-2 и Гараж (Next), Еврейский музей (JSON-LD), Level One, Страдариум, Достоверно, Политех лекции — второй по ценности формат
6.5 SRC-CULTCENTERS ЗИЛ, ДК Рассвет, Винзавод, Хлебозавод, Флакон, Артплей, Фабрика районные ДК с расписаниями
6.6 SRC-LECTORIA (дочистить) МГУ whitelist + ICS, РАНХиГС ДОД разведка уже есть в карточке

Волна 7 — «Клубные форматы малого размера»

# Карточка Состав
7.1 SRC-QUIZ Квиз Плиз (Nuxt, много городов), Мозгва, Эйнштейн пати, 60 секунд
7.2 SRC-BOARDGAMES Hobby Games (JSON-LD, много городов), Мосигра (анти-бот, с ноутбука)
7.3 SRC-WALKS Москва глазами инженера, Гуляем по Москве, Москваход; Tripster/Sputnik8 только групповые с датой
7.4 SRC-RUNNING 5 вёрст (WP REST/RSS; серия → сеансы), Спорт-Марафон (анти-бот)
7.5 SRC-CLEANGAMES Чистые игры (@type: Event в JSON-LD, много городов)
7.6 SRC-COMMUNITY-CAL МЕОЦ (ICS), Московская епархия (RSS), Даниловцы (WP) — семейство community_cals
7.7 SRC-BOOKSHOPS книжные с событиями — после проверки пересечений с KudaGo/Timepad
7.8 SRC-GASTRO, SRC-CRAFT дегустации, каппинги, студии — только с календарём конкретных дат
7.9 SRC-MARKETS Seasons (WP+JSON-LD), Ламбада; Московские сезоны — проверить в russpass
7.10 SRC-SPEEDDATING Быстрые свидания, Дуэт, FastLife — после пробы с ноутбука

Волна 8 — «Регионы»

# Карточка Состав
8.1 SRC-CITIES-EXPAND расширить supported_cities у opendata_mkrf, kudago, qtickets, kassir, radario, nethouse, gorodzovet, russpass по живой сверке
8.2 SRC-GEOMETRIA geometria.ru — один Bitrix-движок на сеть городов
8.3 SRC-VIBIRAI vibirai.ru (RSS)
8.4 SRC-CITYMEDIA Собака.ру, Инде, It's My City — RSS/WP REST анонсов
8.5 SRC-2GIS только если найдётся доступ с ноутбука и стабильная поверхность

Волна 9 — «Соцсети и мессенджеры»

# Карточка Состав
9.1 SRC-VK-WHITELIST реестр районных и тематических пабликов (разговорные клубы, беговые, православные молодёжные) как данные, не код
9.2 SRC-TELEGRAM Telethon + извлечение через LLM; ADR и решения владельца до первой строки кода (§15)
9.3 SRC-TENCHAT лента деловых событий

Перепроверки заблокированного — по одной пробе в квартал: 2do2go, Runet-ID, Политех, Мосгорпарк, dancetime, bistrosvidania, РазДельный сбор (все 000).


5. Архитектура в одну страницу

collectors/<code>/        адаптер одной площадки: HTTP → CatalogUpsert   (ШЛЮЗ 1: сбор)
        ↓ CollectResult(items, venues, stats, errors)
orchestrator/runner.py    прогон: advisory-лок на источник, города в потоках,
                          heartbeat, промежуточная запись, статусы, парковка
        ↓ по одной строке
orchestrator/persist.py   слияние в каталог: дедуп, полевая политика, статус (ШЛЮЗ 2)
        ↓
db/models.py              Postgres #1: events / event_sessions / venues / organizers /
                          event_media / event_source_links / raw_payloads / collection_runs
        ↓ GET /api/export/events (X-Auth-Token)
ui/export.py::_build_card проекция в плоскую карточку, fail-closed по атрибуции,
                          интересы, feed_ready                                (ШЛЮЗ 3)
        ↓ HTTP
стенд events/catalog_import.py  политика источника, замки оператора, уборка   (ШЛЮЗ 4)
        ↓
витрина Nuxt · свайп-лента Android/iOS · консоль оператора

Три вещи, которые важнее всего помнить:

  1. Ingestion — отдельный сервис со своей базой. Стенд ходит к нему только по HTTP. Сборщик ничего не знает про базу; оркестратор ничего не знает про сайт. Договор между ними — датаклассы collectors/protocol.py.
  2. Событие проходит четыре независимых шлюза и может провалиться на любом. «Собрал» ≠ «в ленте». Проверка нового источника заканчивается на карточке экспорта и на строке в стенде, а не на stats.normalized.
  3. feed_eligible — не «видно в ленте». Настоящий предикат на стенде: is_active И is_published И окно дат И (feed_eligible ИЛИ пустой external_id).

Что где отсеивается (чтобы понимать, почему «событие есть, а в приложении нет»):

Шлюз Отсев
1. Сбор прошедшие; онлайн без флага; карточки без даты/ссылки/короткие (NormalizeError → filter_reasons); чужой город
2. Каталог склейка дублей; статус из слитой строки (draft без сеансов, needs_geo без координат, premod для mass)
3. Экспорт нет рабочей http(s)-ссылки на источник; заголовок < 8 символов; неизвестный город; feed_ready = обложка ∧ интересы ∧ место ∧ время в горизонте 45 дней
4. Стенд политика источника (in_catalog/in_feed/auto_publish); замки оператора; уборка после полного прохода

6. Договор сборщика

6.1. Каталог файлов

ingestion/collectors/<code>/
  __init__.py       экспорт класса XxxCollector
  collector.py      класс: параметры прогона, обход, статистика, отсев, flush/heartbeat
  http_client.py    транспорт: базовый URL, темп, пагинация, обход анти-бота
  normalize.py      сырой объект → CatalogUpsert; NormalizeError; is_past
  parse_list.py / parse_detail.py   только у HTML-скраперов
  <адаптеры>.py     у семейств: один файл на сайт (wtc.py, skolkovo.py, opora_msk.py)

Эталон для чтения — kudago (198 строк, всё по правилам). Эталон семейства — venue_cals (три сайта, общий нормализатор) и bizassoc (адаптеры + общие помощники из collect_util: parse_subset, moscow_address, mentions_other_city, https_url).

6.2. Класс сборщика

class XxxCollector:
    source_code = "xxx"          # постоянный ключ каталога; переименовать нельзя
    source_name = "Название"
    auth_type = "none"           # none | bearer | api_key | token
    supported_cities = tuple(city_ids_for("xxx"))   # нет атрибута → только msk
    default_max_pages = DEFAULT_MAX_PAGES           # None = дамп, обходить целиком
    # default_city_workers = 12  # ТОЛЬКО если города — разные хосты
    def collect(self, ctx: CollectContext) -> CollectResult: ...
    def configuration_error(self) -> str | None: ...  # опционально: почему не может работать

CollectContext даёт сборщику: city_code (код продукта), run_key, params (словарь параметров прогона), should_stop() (кооперативная отмена), heartbeat(dict) (признак жизни), flush(items) -> int (сдать собранное в каталог сейчас), known_digests(keys) -> dict (что каталог помнит про отпечатки анонсов). Любой из колбэков может быть None — сборщик обязан работать и без них (прогон из теста или скрипта).

CollectResult: items: list[CatalogUpsert], venues: list[VenueUpsert] (только для справочников площадок, событий из них оркестратор не делает), stats: dict, errors: list[str].

6.3. CatalogUpsert — одна карточка

Поле Обязательность Правило
source_object_id обязательно стабильный id объекта у площадки; у семейств — с префиксом адаптера (opora_msk:123); у Bileter — город:слаг
source_url обязательно рабочая http(s)-ссылка на страницу события у площадки; без неё карточка не отдаётся стенду
title обязательно ≥ 8 символов после чистки; до 1024
description_plain / _html желательно сливается по правилу «длиннее выигрывает»
city_code обязательно код продукта, под которым шёл обход (ctx.city_code), а не то, что написано в сырых данных
is_online обязательно по явному признаку площадки; «очно и ВКС» — очное
is_free, price_text, price_min/max, currency как есть is_free не выводится из нуля или пустой цены; None = «не указано»
capacity как есть число мест; 0/мусор → None (format_policy.clean_capacity); подписчики/аудитория — в extras, не сюда
age_restriction как есть строкой («18+»)
categories, tags желательно категории площадки как есть (свои слова), таксономии не переводятся; сопоставление интересам — в словаре стенда (§11)
format желательно только через format_policy.classify_format
status совещательно каталог пересчитает сам; ставить через status_policy.resolve_ingest_status
lat, lon если площадка даёт 0,0 и вне диапазона отбрасывать; не геокодировать в сборщике
venue: VenueSpec желательно name, address (город первым словом для московских: moscow_address), source_place_id (id площадки у источника; если id нет — ключ из адреса, а не из имени)
organizer: OrganizerSpec желательно source_org_id до 128 символов (имя-фолбэк — обрезать)
sessions: list[SessionSpec] обязательно для события с датой все будущие сеансы (до 60 в экспорте; в каталог можно до 500), timezone площадки, source_session_key уникальный на объект; открытый конец — +2 часа
media: list[MediaSpec] желательно только абсолютные https (https_url); credit если площадка даёт автора; favicon и заглушки не класть
extras свободно факты о событии; first-write-wins при слиянии
volatile_extras по необходимости ключи extras, которые пересчитываются каждый обход («сеансов всего», «обновлено», is_ongoing)
raw_payload обязательно сырой объект как пришёл (аудит, повтор, erase)
preview_digest при пропуске деталей отпечаток только полей списка (collect_util.preview_digest)

6.4. Точки регистрации — что править, добавляя источник

Порядок значим только там, где написано.

# Файл Что
1 ingestion/collectors/<code>/ сам сборщик
2 ingestion/orchestrator/registry.py импорт + запись в _BUILTIN_FACTORIES
3 ingestion/domain.py SourceDef(code, name, auth_type, enable_when_registered=True) в SOURCE_CATALOG
4 ingestion/collectors/cities.py идентификаторы городов площадки в CITIES (новые города — ADR 0002; длинные слои — отдельным словарём, как MOIBIZ_REGIONAL_IDS)
5 scripts/sync_stand_cities.py → backend/.../events/cities.py если добавлен город: пересобрать копию справочника стенда (test_city_directory_parity)
6 ingestion/schedule_status.py CRON_SOURCES — по длительности прогона, не в конец (test_slow_sources_are_launched_first)
7 docker/collect-all.sh, docker/verify.sh значение SOURCES по умолчанию = CRON_SOURCES слово в слово (test_cron_sources_parity)
8 ingestion/publish/interest_map.py все категории источника в CATEGORY_MAP, включая метку по умолчанию (иначе feed_ready никогда)
9 ingestion/ui/app.py _IMG_HOST_SUFFIXES CDN обложек площадки (иначе прокси картинок отвечает 403 и консоль пуста)
10 backend/.../events/catalog_import.py DEFAULT_POLICIES строка политики: in_feed, auto_publish (новый источник — False, пока качество не подтверждено глазами), note
11 tests/test_<code>_collector.py + tests/fixtures/<code>_*.json|html тесты (§12)
12 tests/test_skip_rollout.py MIGRATED или PENDING с причиной
13 tests/test_cron_sources_parity.py SLOW_SOURCES если источник дольше nethouse
14 docs/internal/03-ingestion-collectors.md, 02 (число источников), ingestion/README.md (строка команды), docs/CHANGELOG.md документация
15 CONTEXT.md, docs/adr/ новые термины и решения
16 .env.example, docker/entrypoint-app.sh (белый список для collect.env), docker/verify.sh (список ключей), ingestion/config.py если нужен ключ
17 tests/conftest.py если завёл стойкий кэш на диске — выключить его в тестах

6.5. Параметры прогона (ctx.params)

Общие имена, которые понимает CLI/UI/волна, и их надо уважать:

Параметр Смысл
max_pages бюджет страниц; None при --full → ABSOLUTE_MAX_PAGES сборщика
page_size, horizon_days если площадка умеет
include_online брать онлайн (по умолчанию нет)
include_past только для отладки
refresh_all качать детали даже у неизменившихся анонсов (ночная волна)
city_workers ширина обхода городов (оператор)
venues / adapters у семейств: подмножество адаптеров (parse_subset)
city_alias, city_id явный идентификатор города площадки

Неизвестный/неприменимый параметр не роняет прогон: он попадает в stats.ignored_params (так сделано у timepad).

Читать параметры только через params.param_bool / param_int: "false" и "0" — это False, а 0 — валидное число (int_or_default).

6.6. Статистика прогона (stats)

Обязательные ключи: source, pages, raw_events, normalized, skipped, filtered, filter_reasons (словарь причина → число), needs_geo, mass. Флаги: config_error, cancelled, source_unavailable, transport_error, parse_error, blocked. Диагностика: field_coverage/fields_missing (дрейф контракта), http_requests/http_wait_sec (цена прогона), details_skipped, digest_keys_asked/matched, skipped_samples (образцы отсева, до 5), ignored_params, api_total_by_city.

Правило слияния по городам (runner._merge): bool — ИЛИ, число — сумма, словарь — покомпонентная сумма. Значит, счётчики — числа, признаки — bool, разбивки — словари; строки в stats не сливаются.

errors — только сбои сборщика (сеть, разбор, баги). Брак карточки площадки — это отсев: filter_reasons + skipped_samples, не errors. Иначе волна вечно partial, а тревога дежурному орёт каждые 12 часов (инцидент moibiz 04.09.2026).

6.7. Коды выхода python -m ingestion run

Код Смысл
0 success
1 failed / partial
3 источник запаркован: нет ключа или площадка закрыта для этого адреса; волна и smoke считают это «пропущен», не провалом

7. Правила поведения сборщика

Нумерация постоянная — на неё можно ссылаться в ревью и карточках. «Обязан» — без этого код не принимается; «должен» — отступление требует причины в докстринге.

Обход и бюджеты

Живость и промежуточные результаты

Сбои и отсев

Что отсеивается на стороне сборщика

Город

Даты и время

Место

Цена, места, формат

Медиа и тексты

Экономия: не ходить за тем, что не менялось

Транспорт и анти-бот

Семейства

Стиль


8. Требования к карточке события

Что стенд и лента ждут от карточки, и почему. Всё это проверяется на выходе ui/export.py::_build_card и на стенде; сборщик отвечает за вход.

Поле Требование Почему
Ссылка на источник обязательна, http(s); без неё карточка не отдаётся (fail-closed) лицензия KudaGo; копирайт = трафик (решение владельца)
Заголовок ≥ 8 символов; стенд режет до 200 по границе слова + «…» «Выставка совреме» выглядит как ошибка вёрстки
Обложка обязательна для ленты; только https; длиннее 500 символов → пустая требование заказчика; mixed content; обрезанный URL = битая картинка навсегда
Интересы без единого интереса события в ленте нет; словарь CATEGORY_MAP + interest_overrides; фолбэк по тексту только для источников без категорий лента ранжируется по шести «вёдрам» анкеты
Город по коду; имя рядом; справочник один и копия стенда генерируется расхождение в символе = пустая лента города
Место адрес и/или координаты, либо is_online; точность геокода в extras.geo_source событие без места не показывается на карте
Сеансы все будущие (до 60 в карточке); число сеансов и ближайшая дата из одной популяции фильтр «суббота» у еженедельного квиза
Горизонт ближайший старт ≤ 45 дней или is_ongoing иначе карточка не экспортируется
Цена is_free не выводится из нуля; None = «не указано» молчаливое «бесплатно» — обещание от нашего имени
capacity 0 = неизвестно; аудитория — в extras подписчики VK делали 37 из 45 событий массовыми
format=mass не публикуется автоматически (premod); квота в ленте; min_group_size=2 спека 24 §6.4
Организатор имя + ссылка + id; два разных известных организатора никогда не сливаются защита дедупа
Атрибуция текста extras.description_source пишет каталог описание берётся «длиннее выигрывает», и его автор может быть не первичной площадкой
Пометки площадки venue.kind (boiling_point), extras.year_guessed и т. п. — только когда есть ключ входит в content_hash стенда

9. Вежливость и сеть

Лимит принадлежит хосту, а не сборщику и не городу.

Правило Как реализовано
Одна очередь запросов на хост на процесс http_util.HostGate; ключ — две последние части домена (поддомены городов делят лимит)
Самый осторожный клиент задаёт темп; дно отката — по хосту ThrottledJsonClient._floor; сторож test_a_fast_neighbour_cannot_speed_up_a_cautious_host
Слот занимается до запроса; неудачный запрос тоже потрачен gated_wait
Медленный ответ (≥ 10 с) удваивает интервал, быстрый — уполовинивает; потолок 8 с адаптивный бэкофф; soft-block часто приходит без 429
Retry-After — не больше 60 с иначе площадка усыпляет прогон
429/5xx — до 4 повторов; 3 таймаута подряд → SourceUnavailableError MAX_RETRIES, MAX_CONSECUTIVE_TIMEOUTS
Умолчание темпа min_interval_sec = 0.55; у площадок с документированным лимитом — по документу (Timepad 60/мин → 1.2 с)
Волна: ≤ 4 источника разом, пауза 8 с между стартами всплеск из 20 стартов роняет DNS даже с публичными резолверами
HTTP/2 не включать ALPN-отпечаток; сознательное решение
Цена прогона измеряется http_requests, http_wait_sec в stats; счётчик наследуется потоками (meter_handle/meter_adopt)

Сетевые особенности стенда, о которых надо знать до первого «у меня не открывается»: DNS в контейнерах прибит к 8.8.8.8/1.1.1.1; IPv6 выключен (иначе сайты с AAAA висят 20 с); MTU 1360 на машине разработчика (VPN); подсеть 172.28.0.0/16. Подробности — docs/internal/07 и 11.

Разница адресов: сервер и ноутбук владельца видят разные площадки (mbm.mos.ru закрыт для сервера по IP). Сборщики крутятся на ноутбуке (стек posobytiyam_ingestion_*), salam0nn.ru только пересылает. Любую пробу доступа повторять с обеих машин.


10. Дедупликация: что обязан знать сборщик

Дедуп делает каталог (orchestrator/persist.py::_find_merge_event), но его качество зависит от того, что пришло из сборщика.

Идентичность события: title_norm + city_code + место, сеансы выбирают близнеца. «То же место» = та же строка площадки или та же гео-ячейка (~110×55 м) и то же нормализованное имя площадки. Обе карточки с координатами в разных ячейках — два события, как бы ни совпадали часы. Два разных известных организатора — никогда одно событие.

Что из этого следует для сборщика:

  1. source_object_id стабилен между прогонами — иначе каждый прогон заводит новый объект, а старый уходит в архив.
  2. venue.source_place_id стабилен и не пустой: без id — ключ из нормализованного адреса. Площадка «Место будет объявлено позднее» с координатами центра города — не площадка (не давать ей координат).
  3. organizer.source_org_id — только если это действительно организатор, а не «сайт-источник»: ложный организатор блокирует склейку с другими площадками.
  4. Заголовок — как у площадки: «улучшение» заголовка разводит дубли.
  5. Серию отдавать одним объектом с сеансами, а не десятком объектов с одинаковым названием (каталог их всё равно склеит, но это лишняя работа и лишние raw_payloads).
  6. is_ongoing (постоянная экспозиция, «идёт сейчас») — в extras и в volatile_extras: заявка живёт по объекту источника и отзывается, когда объект перестаёт её присылать.
  7. Полевая политика слияния: title — первый непустой; описание — длиннее выигрывает; категории/теги — объединение; цены/capacity — первый ненулевой; координаты/площадка/организатор — правит только первичный источник. Значит, сборщик не должен присылать «пустое, чтобы затереть».

После каждой волны крутится maintenance dedup тем же предикатом; след каждой склейки — event_merge_log. Если новый источник породил волну ложных склеек — смотреть туда, а не в events.


11. Стенд: что нужно дописать, чтобы событие дошло до ленты

Что Где Без этого
Строка политики источника events/catalog_import.py::DEFAULT_POLICIES (in_catalog, in_feed, auto_publish, note) карточки заводятся с мягким умолчанием auto_publish=False и в ленту не попадают, пока оператор не включит; заводить явно и осознанно
Категории → интересы ingestion/publish/interest_map.py::CATEGORY_MAP feed_ready=False навсегда (инцидент moibiz 03.09.2026: 19 тем не знал словарь)
CDN обложек ingestion/ui/app.py::_IMG_HOST_SUFFIXES прокси картинок отвечает 403; в консоли карточки без фото (это не влияет на ленту, но ослепляет оператора)
Копия справочника городов scripts/sync_stand_cities.py новый город даёт пустую ленту на стенде
venue.kind и прочие пометки _build_card стенд читает то, что есть в карточке; пустые ключи не слать

Перенос в стенд идёт курсорно до 100 страниц × 200 карточек; события новых площадок имеют самые большие events.id и лежат в хвосте — при «переносе всех» хвост может не доехать (stats.truncated=True). Известная общая проблема; обход — точечный перенос import_catalog(sources=['<code>']). При приёмке нового источника проверять именно это.

Проверка «доехало»: сквозной тест tests/test_wave5_catalog_e2e.py — образец для каждого нового источника (сборщик → run_collector → каталог → _build_card → feed_ready, интересы, площадка с адресом, сеанс, обложка).


12. Тесты

12.1. Что обязательно для нового источника

Тест Что проверяет Образец
нормализация сырой объект → CatalogUpsert: даты с поясом, цена, is_free, площадка, source_place_id, обложка https, отсев прошедшего/онлайн/без даты с именованными причинами tests/test_wave2_normalize.py, test_bizassoc_collector.py
сборщик с фейковым клиентом пагинация, max_pages, flush после страницы, heartbeat, мягкий сбой не теряет страницы, cancelled, stats tests/test_kudago_collector.py::FakeKudagoClient
неизвестный город ValueError со списком известных → config_error, а не тихая Москва test_unknown_city_is_a_config_error
сквозной до карточки через run_collector в тестовую базу (@pytest.mark.pg) до _build_card: feed_ready, интересы, площадка, сеанс tests/test_wave5_catalog_e2e.py
пропуск деталей если переведён: второй прогон с теми же отпечатками не ходит за деталями, счётчик растёт; refresh_all перекачивает tests/test_skip_unchanged_details.py
парковка/блок если площадка может закрыть доступ: ответ блокировки → stats.blocked, сетевой сбой → нет tests/test_moibiz_collector.py

12.2. Правила

12.3. Живой smoke

python -m ingestion run --source <code> --city msk --max-pages 1, затем --city all --max-pages 1 (если городов много), затем docker/verify.sh <code> на стенде. Считать: сколько собрано, сколько отсеяно и почему, сколько needs_geo, есть ли обложки, сколько mass, сколько запросов и секунд ожидания. Эти числа идут в карточку и в docs/internal/03.


13. Разведка площадки: протокол

Разведка — половина работы и отдельный результат: карточка беклога с «паспортом площадки» (приложение Б), даже если вывод — «не делать». Разведка предшествует любому коду.

  1. Проверить, что этого ещё нет. Беклог events (все колонки), docs/internal/03, git status рабочей копии на ноутбуке владельца (/home/joda/Projects/ПоСобытиям/plibvi): 03.09.2026 две карточки из трёх оказались уже сделаны в незакоммиченной копии.
  2. Проверить пересечение с уже собранным. Название площадки/организатора поискать в каталоге (organizers, venues, консоль → каталог) и на Timepad/KudaGo/Leader-ID/Qtickets. Если ≥ 80 % событий площадки уже приходят оттуда — адаптер не нужен, нужен whitelist (VK) или ничего.
  3. Снять поверхность. Для домена: robots.txt, sitemap.xml, RSS/Atom, .ics, JSON-LD (@type: Event — лучший случай), WP REST (/wp-json/wp/v2/, плагины календарей), Bitrix (PAGEN_N, ajax), Tilda (данные в tildacdn/встроенный Timepad), Nuxt/Next (__NUXT__, __NEXT_DATA__, внутренний API из network tab), GraphQL. Вкладка Network браузера при листании и фильтрации даёт больше, чем HTML.
  4. Проба доступа с двух адресов: сервер и ноутбук. Коды 403/401/000, DDoS-Guard, QRATOR, Variti, Cloudflare — записать. JS-челлендж и блок по IP — разные вещи: первый обходится браузером, второй — нет.
  5. Померить данные на 1–3 страницах: всего записей, доля предстоящих, доля очных, доля с адресом, доля с координатами, доля с картинкой, доля с ценой, есть ли id объекта, стабилен ли URL, формат дат (год есть?), пояс, пагинация (offset/курсор/страница; сортировка по дате?), фильтр по дате на сервере, лимиты (заголовки Retry-After, X-RateLimit-*).
  6. Города. Как площадка называет города (слаг, id, поддомен), совпадают ли с нашим справочником, какие есть у неё и нет у нас. Идентификаторы сверять живьём (у KudaGo официальный справочник даёт 5 городов, работают 13; у Leader-ID номера двух городов до сих пор под вопросом).
  7. Категории. Полный список рубрик площадки (обходом разделов, если нет иначе) — он целиком пойдёт в CATEGORY_MAP.
  8. Оценить объём и цену: событий/неделю предстоящих очных; запросов на полный обход; секунд при вежливом темпе; нужен ли браузер; нужен ли пропуск деталей (деталь = отдельный запрос?).
  9. Вывод: делать / не делать / отложить, чем (API/HTML/ICS/RSS/браузер), в какое семейство, какой приоритет, какие решения нужны от владельца. Всё — в карточку, с датой пробы и адресами, с которых пробовали.

Инструменты: curl -sIL, curl -A "Mozilla/5.0 …", Playwright (.venv/bin/playwright install chromium; на сервере бинарь лежит в ~/.cache/ms-playwright/chromium-*/chrome-linux64/chrome), jq, Wayback для мёртвых сайтов (только чтобы понять, что было; не как источник данных).


14. Процесс: от карточки до отчёта

  1. Карточка. Одна площадка или одно семейство — одна карточка SRC-* в беклоге events (шаблон — приложение А). Карточка живёт в «В работе» с момента разведки.
  2. Разведка (§13) → паспорт в карточке. Если нужны решения владельца (новый город, пригород, семейство или отдельный код, публиковать сразу или оператором, брать ли онлайн, лимит), — вопрос задаётся до кода, списком, с вариантами и рекомендацией. Решение с последствиями на годы (география, идентичность) — ADR в docs/adr/ (формат: Статус, Контекст, Решение, Последствия).
  3. Реализация по §6–7, начиная с normalize.py и фикстур: сначала тест на нормализацию, потом клиент, потом сборщик.
  4. Регистрация по чек-листу §6.4 (все 17 строк, отмечать в карточке).
  5. Тесты §12: весь прогон зелёный, живой smoke по одному городу и по всем.
  6. Самопроверка по правилам R-1…R-49 и по «Граблям» (§17). Ревью 03.09.2026 нашло 33 находки за четыре прохода на двух площадках — это норма, не позор; лучше найти самому.
  7. Документация: раздел площадки в docs/internal/03 (транспорт, ключи, особенности, измеренные числа, что решено не брать и почему), число источников в 02, строка в ingestion/README.md, запись в docs/CHANGELOG.md, термины в CONTEXT.md.
  8. Коммит: один источник — один коммит (правки по ревью — отдельными), сообщение в стиле репозитория: feat(<code>): «Название» — семейство, адаптер X (SRC-XXX), fix(<code>): …, perf(<code>): …, test(<code>): …, docs(…): …. Заголовок по-русски, называет что и почему. Пуш требует проброшенного ssh-агента (SSH_AUTH_SOCK).
  9. Выкат на стенд: пересборка образа сборщиков на ноутбуке (./docker/build.sh, код запекается в образ), sources seed, docker/verify.sh <code>, точечный перенос в стенд (import_catalog(sources=['<code>'])), проверка политики источника и карточек в консоли оператора.
  10. Наблюдение: дождаться ночной волны (00:17 МСК) и дневной (12:17), прочитать collect_cron.log и collect_status.json: длительность источника (и его место в CRON_SOURCES), errors vs filter_reasons, число записанных, truncated у переноса.
  11. Отчёт в карточку и перевод в «Готово»: измеренные числа (собрано / отсеяно по причинам / needs_geo / обложки / mass / длительность / запросов), что не сделано и почему, что решено не брать.

Definition of Done — приложение В.

Что не делать без явного решения владельца: переименовывать source_code; менять общие модули (protocol, runner, persist, http_util, cities) ради одной площадки без ADR/обсуждения; заводить второй путь сбора «на всякий случай» (у Timepad прямо запрещено); подключать платные API и партнёрские фиды; менять политику публикации источника на стенде.


15. Слой Telegram/VK с извлечением через LLM

В чате 24.07 это «самый дорогой в обработке, но безальтернативный источник йоги в парке и православных бесед». Сегодня в проекте есть только vk (события сообществ, без извлечения из текста). Ниже — требования к слою, чтобы разведка и ADR шли по одному списку. До решений владельца по пунктам 1, 2, 7 код не пишется.

  1. Реестр каналов — данные, не код. Таблица channel_registry (или JSON в репозитории на первом шаге): канал/паблик, город, район, тематика, кто добавил, дата, активен ли. 200–400 записей по оценке чата. Пополнение — операторская задача, не релиз.
  2. Доступ к Telegram. Telethon требует пользовательской сессии (номер, код, 2FA) — как у VK/Добро (.secrets, keepalive). Кто владелец аккаунта, где живёт сессия, что при бане — решение владельца.
  3. Извлечение. На каждый пост с признаками даты — запрос к модели со строгой JSON-схемой: title, starts_at (с поясом; год из контекста поста и даты публикации), ends_at, address, venue_name, is_online, price_text, is_free (только по явному слову), organizer, registration_url, confidence. Поля, которых нет в тексте, — null, не догадка. Промпт и схема — в репозитории, с версией; изменение промпта = перепрогон оценочного набора.
  4. Оценочный набор: 100–200 размеченных вручную постов (события / не события / поля) в tests/fixtures/tg_eval/; порог приёмки — точность даты и адреса ≥ 90 %, ложных событий ≤ 5 %. Ниже порога — слой не выкатывается.
  5. Экономия: извлекать только посты с датой-кандидатом (регулярка), один раз на post_id (кэш по хешу текста), не перечитывать правки без изменения текста; лимит стоимости на волну.
  6. Что становится карточкой: source_object_id = "<channel>:<post_id>", source_url — ссылка на пост (t.me/<channel>/<id>), обложка — фото поста (свой CDN-хост в allowlist), raw_payload — текст поста без служебных полей. ПДн: телефоны, личные контакты организаторов из текста в карточку не переносить; сырьё под ретенцией 90 дней; erase по запросу.
  7. Публикация: auto_publish=False — каждую карточку смотрит оператор, пока оценочный набор не докажет качество; в карточке extras.llm_extracted с версией промпта и confidence.
  8. Дедуп: тот же механизм каталога; ожидается, что половина постов — репосты событий, уже собранных с Timepad/KudaGo; проверять долю склеек.
  9. Модель: у проекта уже есть шлюз к модели (раздел «SEO» ходит к Claude через панель на salam0nn.ru); переиспользовать его или завести отдельный ключ — решение владельца. Рестарт панели рвёт и генерацию статей.
  10. Наблюдение: в stats — постов прочитано / с датой / отправлено в модель / извлечено / отброшено по confidence / стоимость.

Для VK тот же путь дешевле: wall.get по whitelist пабликов + то же извлечение. Но сначала — расширение whitelist сообществ-событий (карточка 9.1), это без LLM.


16. Операционка и наблюдение

Что Когда/где
Полная волна: все источники --city all --full → maintenance archive → dedup --actor cron → purge-raw → статус → alert 17 */12 * * * МСК (00:17 ночная с --refresh-all, 12:17 дневная), docker/collect-all.sh, лог logs/collect_cron.log
Порядок запуска = CRON_SOURCES, по убыванию длительности волна кончается вместе с самым долгим; долгий в хвосте держит хвост (было 9,4 ч вместо 6,6 ч)
Параллельность 4 источника разом, пауза 8 с между стартами; внутри источника — city_workers
Парковка needs_credentials (нет ключа) и network_blocked (блок по IP, 20 ч) в строке sources; run → код 3
Тревога дежурному ingestion alert: красные источники и ключи; INGESTION_ALERT_*; ПДн в чат не уходят
Ключи VK user token */8 (живёт ~15 мин), Dobro JWT 9 */6, keepalive сессии VK 37 */4 — в контейнере token-refresher
Импорт в стенд ~каждые 6 ч; уборка только после полного прохода, не ограниченного городом
Smoke docker/verify.sh [code] — один город, одна страница; код 3 = пропущен
Состояние python -m ingestion status; GET /api/export/schedule, /token-health; вкладка «Прогоны» в UI :8765
Почему события нет в приложении консоль стенда → «По душе» → каталог → «В ленте»; adminpanel/event_filters.py::event_gaps

Бюджет длительности нового источника: он не должен становиться самым долгим без причины. Ориентиры медианы (02–04.09.2026): moibiz 6,5 ч (до починки), ticketland 2,5 ч, russpass ~1,7 ч; большинство — минуты. Если полный обход площадки дольше часа — сначала серверный фильтр по дате, пропуск деталей и ABSOLUTE_MAX_PAGES, потом ширина потоков.


17. Известные грабли (короткий список)

Полный свод — docs/internal/11-gotchas.md. Здесь то, на что новый источник наступает чаще всего.

  1. page= без _num у Хабра отдаёт архив 2023 года — параметры пагинации сверять с network tab, а не с документацией.
  2. У opendata.mkrf данные в нумерованной версии, $last → 404; свежая версия не обязательно непустая.
  3. Длинный заголовок переполнял dedup_signature varchar(512) и терял событие целиком — лечится в каталоге, но длину полей помнить (source_org_id 128).
  4. Пустая цена ≠ is_free; capacity=0 ≠ «мест нет».
  5. Ключ площадки без id — из адреса, не из имени.
  6. «Москва, г. Сочи» — маркер чужого города проверять до дописывания города.
  7. Год без года: текущий — неверно почти всегда (Опора: 15 призраков из 76).
  8. Карточка события, похожая на страницу списка, зацикливает обход (moibiz: 129 334 страницы).
  9. Брак карточки в errors красит волну навсегда — только filter_reasons.
  10. Проба доступа из configuration_error под открытой сессией базы — нельзя.
  11. Парковка в памяти процесса не работает под cron — только в базе.
  12. bool в stats складывается как число при слиянии городов — признаки только bool, счётчики только числа.
  13. Стойкий кэш деталей ломает изоляцию тестов — выключать в conftest.
  14. Обложка площадки не в _IMG_HOST_SUFFIXES — консоль слепа.
  15. Категории источника не в CATEGORY_MAP — feed_ready=False навсегда.
  16. Перенос в стенд обрезается на 20 000 карточках — новый источник в хвосте.
  17. Playwright в cron без PLAYWRIGHT_BROWSERS_PATH — «Executable doesn't exist».
  18. curl — жёсткая зависимость образа (JA3), не удобство отладки.
  19. Сессия SQLAlchemy не потокобезопасна — свои сессии на поток города делает оркестратор; в сборщике базы нет вообще.
  20. verify=False глобально — никогда.

18. Открытые вопросы к владельцу

  1. Партнёрские фиды билетных операторов (Ticketscloud, Qtickets, Radario): парсеры уже есть; нужен ли партнёрский API ради комиссии (фаза 3 в чате 24.07)? Если да — карточка SRC-TICKETSCLOUD оживает как интеграция, а не парсер.
  2. PRO.Культура.РФ: партнёрский ключ с обязательством выгружать все события региона за 2 суток — брать или остаёмся на открытых данных МКРФ?
  3. Telegram-слой: аккаунт для Telethon, модель и бюджет на извлечение, кто ведёт реестр каналов (§15, пункты 1, 2, 7, 9).
  4. Московское долголетие (55+): расширять ли возрастной сегмент.
  5. Онлайн-события: сейчас отсев везде; нужен ли отдельный раздел «онлайн» на витрине (тогда include_online по умолчанию у части источников).
  6. Leader-ID: номера Тулы и Ярославля (1227359/1265352 против 1283537/1299162) — сверить живьём и оставить один; placeIds[].
  7. Перенос в стенд: чинить обрезку курсора (общая проблема) до подключения новых источников или продолжать точечные переносы.
  8. Порядок волн 6–9 (§4) — утвердить или переставить.
  9. Регионы: какие города продукт открывает первыми после Москвы (это определяет, где расширять supported_cities).
  10. Незакоммиченные правки от 04.09.2026 (порядок CRON_SOURCES, default_city_workers) — закоммитить до начала работы сотрудника.

19. Приложения

А. Шаблон карточки беклога SRC-*

Заголовок: Collector: <Площадка> (<домен>)
Приоритет: high | normal | low
Why (одной фразой): что даёт продукту и почему сейчас.

## Цель
Что собираем (тип событий, города), чего не собираем.

## Разведка <дата>, с <сервер | ноутбук>
Таблица: поверхность | факт (код, движок, разметка, пагинация, лимит, id, даты, гео, картинки).
Пересечения с уже собранным (что и какая доля).
Категории площадки (полный список).

## Как парсить
Транспорт, адаптер/семейство, source_object_id, source_url, площадка и гео,
пропуск деталей, ABSOLUTE_MAX_PAGES, темп.

## Фильтры
Что отсеиваем и под каким именем в filter_reasons.

## Риски
Анти-бот, нестабильность, дубли, ПДн.

## Решения владельца
Список вопросов → ответ, дата.

## Приёмка
- [ ] чек-лист §6.4 (17 пунктов)
- [ ] тесты §12
- [ ] smoke msk / all
- [ ] docs/internal/03, README, CHANGELOG
- [ ] стенд: политика, интересы, CDN, перенос
- [ ] волна: длительность, errors=0, отчёт числами

Б. Паспорт площадки (заполняется при разведке)

Поле Значение
Домен, разделы событий
Движок / разметка (JSON-LD Event, ICS, RSS, WP REST, Bitrix, Tilda, Nuxt/Next, GraphQL)
Доступ с сервера / с ноутбука (код, защита)
Id объекта и стабильность URL
Пагинация, сортировка, серверный фильтр по дате
Формат даты (год? пояс?), сеансы/серии
Гео: адрес / координаты / имя площадки / id площадки — доли
Картинки: хост CDN, доля с обложкой
Цена: формат, «бесплатно» явно?
Категории (список)
Города и их идентификаторы
Объём: всего / предстоящих / очных / в неделю
Лимиты и темп
Пересечение с уже собранным
Нужен ли браузер / curl / ключ
Вывод и приоритет

В. Definition of Done для источника

Г. Команды

# окружение
.venv/bin/pip install -r requirements.txt
.venv/bin/playwright install chromium
.venv/bin/alembic upgrade head
.venv/bin/python -m ingestion sources seed

# сбор
python -m ingestion run --source <code> --city msk --max-pages 1
python -m ingestion run --source <code> --city all --max-pages 1
python -m ingestion run --source <code> --city msk --full --refresh-all
python -m ingestion status
python -m ingestion ui            # http://127.0.0.1:8765/

# тесты
TEST_DATABASE_URL=postgresql+psycopg://podushe:podushe@127.0.0.1:5433/podushe_ingestion_test \
  .venv/bin/python -m pytest -q
.venv/bin/python -m pytest -q tests/test_<code>_collector.py tests/test_cron_sources_parity.py tests/test_skip_rollout.py

# стенд (ноутбук владельца)
./docker/build.sh
docker compose exec app /app/docker/verify.sh <code>
docker compose exec app /app/docker/collect-all.sh --dry-run
docker compose exec app cat /app/state/collect_status.json
tail -f logs/collect_cron.log

Д. Откуда взят реестр площадок

Чат владельца от 24.07.2026 «Сервис мероприятий» — два ответа: тиры 1–4 с правовыми и архитектурными требованиями, затем полный реестр из 16 разделов по просьбе «максимально полный список, от афиш концертов до простых локальных прогулок». Разделы 3.1–3.16 этого документа повторяют структуру реестра один к одному; ни одна площадка из чата не пропущена, к каждой добавлен статус на 07.09.2026 и результат пробы с сервера.