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 и решений проекта следует шкала ценности:
- Событие с точными координатами и малого формата — главная цель. Лекции, мастер-классы, разговорные и книжные клубы, беседы, йога в парке, прогулки, квизы, настолки, дегустации, волонтёрские выезды. Именно ради них строится продукт (в чате: «события на 8–15 человек»).
- Событие с адресом без координат — годится: каталог догеокодирует по
своему справочнику OSM (
ingestion/geo/*), статус до тех порneeds_geo. - Массовое зрелище (стадион, репертуарный театр, фестиваль) — в каталог
берём, но помечаем
format=mass: в ленту оно идёт через премодерацию и квоту (FEED_MASS_QUOTA=1), в районную ленту — нет. «Массовая афиша на карте районов — шум, который убивает гиперлокальность» (чат 24.07). - Онлайн — по умолчанию не берём (у онлайна нет места на карте). Берём только по явному флагу прогона там, где площадка смешивает форматы.
- Прошедшее — не берём никогда; отсев на стороне сборщика.
Решения владельца, которые действуют на всю работу:
- Юридическую чистоту источников не оцениваем — владелец договаривается
с площадками сам. Но технические правила проекта, которые из этого выросли,
обязательны: ссылка на источник у каждой карточки (иначе карточка не
отдаётся стенду), авторство фото там, где площадка его даёт, ретенция сырья
90 дней,
eraseпо запросу. - Указание источника в карточке = копирайт и обмен трафиком, поэтому берём широко — «от афиш концертов до простых локальных прогулок, читательских клубов, православных бесед, йоги в парках».
- Тир 4 (Timeleft, Sea Soul, Globi, Invitor, Luma, Partiful) не собираем — это конкурентная разведка, не источник контента.
- Билетные операторы (тир 3 в чате) уже собраны парсерами — решение о партнёрских фидах остаётся за владельцем (см. §18).
География: Москва — приоритет проверки качества, но справочник знает
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. Что сделано частично (важно не принять за «готово»)
SRC-LECTORIAзакрыта, но реализован только ВШЭ (hse). МГУ (whitelist факультетских календарей), РАНХиГС (ДОД + научный календарь) — не сделаны; в карточке есть разведка 18.08.2026.SRC-CONGRESS— ЦМТ, Школа Сколково, events.sk.ru. «Технопарк» так и не выбран (technopark.ru — магазин техники; кандидаты: Технополис Москва, Техноград ВДНХ, кластер «Ломоносов»).SRC-BIZASSOC— только Опора МСК; на 03.09.2026 у неё ноль предстоящих очных событий. Самый бедный источник проекта.SRC-HABRVC— только Хабр; VC не делать.timepad— механизм пропуска неизменившихся деталей не заведён (страница события ~2,5 с; окупится сильнее всех) — см.tests/test_skip_rollout.py.leader_id— ⚠️ номера городов Тулы и Ярославля расходятся между двумя слитыми реализациями (1227359/1265352 против 1283537/1299162); параметрplaceIds[]уevents/searchне проверен.
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 ленты событий | низкий |
| Не начато | без отдельного решения владельца не делать | — | |
| Яндекс Район | закрыт | замена — районные 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 · консоль оператора
Три вещи, которые важнее всего помнить:
- Ingestion — отдельный сервис со своей базой. Стенд ходит к нему только по
HTTP. Сборщик ничего не знает про базу; оркестратор ничего не знает про
сайт. Договор между ними — датаклассы
collectors/protocol.py. - Событие проходит четыре независимых шлюза и может провалиться на
любом. «Собрал» ≠ «в ленте». Проверка нового источника заканчивается на
карточке экспорта и на строке в стенде, а не на
stats.normalized. 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. Правила поведения сборщика
Нумерация постоянная — на неё можно ссылаться в ревью и карточках. «Обязан» — без этого код не принимается; «должен» — отступление требует причины в докстринге.
Обход и бюджеты
- R-1. Сборщик обязан уважать
max_pagesи объявлятьABSOLUTE_MAX_PAGES— предохранитель на--full: у площадокnextне кончается, а обход в?archive=yили в карточки, похожие на списки, длился часами (moibiz: 129 334 страницы ради 6436 событий). - R-2.
default_max_pages = Noneтолько у дампов, где страницы — не «ещё того же», а части одного набора (data.mos резался с 5529 до 5000). - R-3. Страница списка — это сам путь списка максимум с
/page/N; ссылка «Подробнее »» — не пагинация. - R-4. Если у площадки есть серверный фильтр по дате — обязан
использоваться (
leader_id: безdateFromбюджет уходил в многомесячные программы, начатые давно). - R-5. Обход останавливается на первой странице без будущих анонсов, если лента упорядочена по дате.
Живость и промежуточные результаты
- R-6.
ctx.flush(items)после каждой страницы. Без этого оператор видит работающий сбор и ноль в каталоге, а обрыв теряет всё. - R-7.
ctx.heartbeat({...})на каждой странице и на каждой долгой фазе (индекс площадок уkassir). Иначе часовой reaper закроет живой прогон. - R-8.
should_stop()проверяется между страницами и перед каждым ожиданием (interruptible_sleep,gated_wait). Отмена прекращает сбор, а не запись. - R-9. Обход страниц оборачивается в
collect_util.drain_event_pages: мягкий сбой (SourceUnavailableError,httpx.HTTPError,ValueError) не теряет уже собранные страницы, ставит флаг вstatsи заканчивает честно.
Сбои и отсев
- R-10. Брак одной карточки —
NormalizeError→filter_reasons[<причина>]и образец вskipped_samples; страница продолжается. - R-11. Неожиданное исключение при разборе одной карточки —
stats.bugs += 1, строка вerrors, страница продолжается (kudago,except Exception). - R-12. Три таймаута подряд →
SourceUnavailableError, не долбить дальше. Уopendata_mkrfэто норма, а не поломка. - R-13. Блокировка по IP/гео (
403 geoblocked, «restricted access from your IP») — не JS-челлендж: браузер не поможет. Сборщик поднимает свою ошибку →stats.blocked = True, оркестратор паркует источник на 20 часов (config.network_blockedвsources),runвыходит с кодом 3. Парковка живёт в базе, не в памяти процесса: cron запускает каждый источник отдельным процессом. - R-14. Сетевой сбой (DNS, таймаут) парковкой не считается.
- R-15. Нет ключа →
configuration_error()возвращает текст; сид паркует источник (needs_credentials) и сам оживляет, когда ключ появится. Сетевых проб изconfiguration_errorне делать (сид держит открытую сессию базы; проба из сида снята 03.09.2026). - R-16. 404 у детали —
PageGoneError: страницу сняли, это не ошибка разбора и не сбой площадки.
Что отсеивается на стороне сборщика
- R-17. Прошедшее — всегда (
is_past,future_only=True). Карточка без времени живёт до конца дня. - R-18. Онлайн — по умолчанию отсев (
filter_reasons.online), берётся только поinclude_online. «Очное участие и ВКС» — очное. - R-19. Событие с явным маркером другого города/региона у московского
источника — отсев
out_of_town(collect_util.moscow_address,mentions_other_city), а не «Москва, г. Сочи». - R-20. Закрытые/внутренние записи (защиты диссертаций у ВШЭ, «запишись в ЛК» без даты у «Моего бизнеса», курсы вместо событий у edtech) — отсев с именованной причиной.
- R-21. Услуга без даты (экскурсия «по запросу», квест, мастер-класс по записи) — не событие; не брать.
Город
- R-22. Код города продукта → идентификатор площадки только через
cities.city_ids_for(code); неизвестный город —ValueErrorсо списком известных, не молчаливая Москва. - R-23.
city_codeу карточки — код обхода. Пригороды складываются в большой город, адрес и координаты остаются настоящими (ADR 0002). Значение в справочнике может быть кортежем идентификаторов — сборщик обходит все. - R-24. Ширину
default_city_workersвыше 4 поднимать только там, где города — разные хосты. Один хост от этого быстрее не станет: темп задаёт общий шлюз.
Даты и время
- R-25. Сеанс хранится с часовым поясом площадки/города (
SessionSpec.timezone), сравнение «прошло/будущее» — по aware-датам. - R-26. Год у даты без года — не «текущий» по умолчанию. Диапазон —
от конца (
timepad); лента, убывающая по дате, — год от предыдущей карточки (bizassoc). Пометкаextras.year_guessed, чтобы оператор видел. - R-27. «15.10.2026» — не время 15:10; «взнос 12.90» — не время; голое «до 12:00» — срок, не начало. Значения вне суток отбрасываются, не роняют прогон.
- R-28. Серия (один объект на слот, еженедельный старт) — один Event с многими Session; ключ сеанса стабильный.
Место
- R-29. Координаты в сборщике не выдумываются и не геокодируются; это
делает каталог (
geo backfillпо справочнику OSM). Если площадка даёт координаты — отбросить0,0и вне диапазона. - R-30. У площадки с известным адресом (свой офис, парк, кампус) —
фиксированные координаты и
source_place_idв коде адаптера, не геокод каждой карточки. - R-31. Адрес московского источника получает «Москва, » первым словом
только если в нём нет маркера другого города (
moscow_address). - R-32. Уровень точности геокода не подделывается:
extras.geo_sourceпишет каталог.
Цена, места, формат
- R-33.
is_free— только по явному признаку площадки («бесплатно»,price is nullтам, где это документированная семантика). Пустая цена ≠ бесплатно. - R-34.
capacity— места; аудитория сообщества —extras.members_count.format— толькоclassify_format. - R-35. Форумы, выставки, стадионы, репертуар —
format=mass(через категорию-зрелище илиlargeисточника), но в каталог — да.
Медиа и тексты
- R-36. Обложка — абсолютный
https, с CDN площадки; favicon, логотип площадки, заглушки «изображение отсутствует» не пишутся (all_events.is_placeholder_image, favicon Timepad 196×196). - R-37. Обложку, которой нет в списке, можно добрать с детали/
og:image, но только с allowlist-хостов (защита от SSRF). - R-38. Описание — как даёт площадка (plain и/или html); заголовок не «улучшать»; чистка только от мусора вёрстки.
Экономия: не ходить за тем, что не менялось
- R-39. Если деталь — отдельный запрос на карточку, сборщик переводится
на пропуск неизменившихся деталей целиком:
preview_digestпо полям списка,known_digests_for(ctx, keys)разом на страницу, пропуск только у анонса с датой, карточка при пропуске всё равно эмитится (last_seenживёт), счётчикdetails_skippedзаведён и растёт,refresh_all = param_bool(...)привязан. Половина механизма опаснее его отсутствия — сторожитtest_skip_rollout. - R-40. Стойкий кэш на диске (
detail_cache.pyуleader_id/moibiz) — допустимая альтернатива, но с TTL, путём через переменную окружения и выключателем вconftest.
Транспорт и анти-бот
- R-41. Один темп на хост в процессе: любой транспорт (httpx, curl,
Playwright, GraphQL) ходит через
http_util.host_gate/gated_waitилиThrottledJsonClient/fetch_text. Свой цикл запросов не писать (третья копия уже удалялась в ревью). - R-42. Браузер — один на прогон, не на запрос; закрывать в
finally. Playwright — только когда httpx и системныйcurlне проходят (лестницаafisha), и только сPLAYWRIGHT_BROWSERS_PATHв cron-окружении. - R-43. Системный
curl— законная ступень (Cloudflare режет JA3 CPython), но allowlist хостов обязателен. - R-44.
verify=False— только для конкретного хоста с битым TLS (events.sk.ru), никогда глобально. - R-45. Браузерный
User-Agent+Refererтам, где без них soft-block (Хабр отдаёт HTML вместо JSON).
Семейства
- R-46. Несколько сайтов одного рода (ассоциации, центры, парки,
библиотеки) — один
source_codeс адаптерами внутри (Источник-семейство,CONTEXT.md): код нельзя переименовать без потери связей, а сайты приходят и уходят.source_object_idс префиксом адаптера. - R-47. Разные по природе площадки, которые лишь делят парсер, семейством не делаются.
Стиль
- R-48. Докстринг модуля объясняет «зачем так», комментарий у кода называет инцидент; русский в комментариях, английский в именах; числа — измерения, не оценки. Того, чего нет, не изображать («антифрода нет»).
- R-49. Python 3.12+ (PEP 695 в
persist.py). Секреты — только черезsecrets.resolve_secret; вparamsпрогона не класть (они вырезаются, но не надо и пытаться).
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 м) и то же нормализованное имя площадки. Обе карточки с
координатами в разных ячейках — два события, как бы ни совпадали часы.
Два разных известных организатора — никогда одно событие.
Что из этого следует для сборщика:
source_object_idстабилен между прогонами — иначе каждый прогон заводит новый объект, а старый уходит в архив.venue.source_place_idстабилен и не пустой: без id — ключ из нормализованного адреса. Площадка «Место будет объявлено позднее» с координатами центра города — не площадка (не давать ей координат).organizer.source_org_id— только если это действительно организатор, а не «сайт-источник»: ложный организатор блокирует склейку с другими площадками.- Заголовок — как у площадки: «улучшение» заголовка разводит дубли.
- Серию отдавать одним объектом с сеансами, а не десятком объектов с
одинаковым названием (каталог их всё равно склеит, но это лишняя работа
и лишние
raw_payloads). is_ongoing(постоянная экспозиция, «идёт сейчас») — вextrasи вvolatile_extras: заявка живёт по объекту источника и отзывается, когда объект перестаёт её присылать.- Полевая политика слияния:
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. Правила
- Никакой сети в тестах. Фикстуры — реальные ответы площадки, снятые
один раз в
tests/fixtures/<code>_*.json|html, обезличенные, не больше необходимого (одна страница списка + одна-две детали). - Даты в фикстурах сдвигаются в будущее в тесте (
_page()у KudaGo): прибитые даты красят тест через месяц без единой правки кода. - Тестовая база: имя обязано заканчиваться на
_test(TEST_DATABASE_URL=postgresql+psycopg://podushe:podushe@127.0.0.1:5433/podushe_ingestion_test); фикстуры делаютalembic downgrade baseи TRUNCATE. - Стойкие кэши и сетевые пробы выключаются в
tests/conftest.py(*_DETAIL_CACHE_PATH="",INGESTION_SKIP_PROBES=1). - Сторожа, которые обязаны остаться зелёными:
test_cron_sources_parity,test_city_directory_parity,test_skip_rollout,test_export_sources,tests_dating_tz/test_api_contract.py(стенд),test_live_window_matches_the_feed,test_a_fast_neighbour_cannot_speed_up_a_cautious_host. - Весь прогон:
.venv/bin/python -m pytest -q(1131 тест на 07.09.2026). Перед коммитом — весь, не «свой файл».
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. Разведка площадки: протокол
Разведка — половина работы и отдельный результат: карточка беклога с «паспортом площадки» (приложение Б), даже если вывод — «не делать». Разведка предшествует любому коду.
- Проверить, что этого ещё нет. Беклог
events(все колонки),docs/internal/03,git statusрабочей копии на ноутбуке владельца (/home/joda/Projects/ПоСобытиям/plibvi): 03.09.2026 две карточки из трёх оказались уже сделаны в незакоммиченной копии. - Проверить пересечение с уже собранным. Название площадки/организатора
поискать в каталоге (
organizers,venues, консоль → каталог) и на Timepad/KudaGo/Leader-ID/Qtickets. Если ≥ 80 % событий площадки уже приходят оттуда — адаптер не нужен, нужен whitelist (VK) или ничего. - Снять поверхность. Для домена:
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. - Проба доступа с двух адресов: сервер и ноутбук. Коды 403/401/000, DDoS-Guard, QRATOR, Variti, Cloudflare — записать. JS-челлендж и блок по IP — разные вещи: первый обходится браузером, второй — нет.
- Померить данные на 1–3 страницах: всего записей, доля предстоящих,
доля очных, доля с адресом, доля с координатами, доля с картинкой, доля с
ценой, есть ли id объекта, стабилен ли URL, формат дат (год есть?),
пояс, пагинация (offset/курсор/страница; сортировка по дате?), фильтр по
дате на сервере, лимиты (заголовки
Retry-After,X-RateLimit-*). - Города. Как площадка называет города (слаг, id, поддомен), совпадают ли с нашим справочником, какие есть у неё и нет у нас. Идентификаторы сверять живьём (у KudaGo официальный справочник даёт 5 городов, работают 13; у Leader-ID номера двух городов до сих пор под вопросом).
- Категории. Полный список рубрик площадки (обходом разделов, если нет
иначе) — он целиком пойдёт в
CATEGORY_MAP. - Оценить объём и цену: событий/неделю предстоящих очных; запросов на полный обход; секунд при вежливом темпе; нужен ли браузер; нужен ли пропуск деталей (деталь = отдельный запрос?).
- Вывод: делать / не делать / отложить, чем (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. Процесс: от карточки до отчёта
- Карточка. Одна площадка или одно семейство — одна карточка
SRC-*в беклогеevents(шаблон — приложение А). Карточка живёт в «В работе» с момента разведки. - Разведка (§13) → паспорт в карточке. Если нужны решения владельца
(новый город, пригород, семейство или отдельный код, публиковать сразу
или оператором, брать ли онлайн, лимит), — вопрос задаётся до кода,
списком, с вариантами и рекомендацией. Решение с последствиями на годы
(география, идентичность) — ADR в
docs/adr/(формат: Статус, Контекст, Решение, Последствия). - Реализация по §6–7, начиная с
normalize.pyи фикстур: сначала тест на нормализацию, потом клиент, потом сборщик. - Регистрация по чек-листу §6.4 (все 17 строк, отмечать в карточке).
- Тесты §12: весь прогон зелёный, живой smoke по одному городу и по всем.
- Самопроверка по правилам R-1…R-49 и по «Граблям» (§17). Ревью 03.09.2026 нашло 33 находки за четыре прохода на двух площадках — это норма, не позор; лучше найти самому.
- Документация: раздел площадки в
docs/internal/03(транспорт, ключи, особенности, измеренные числа, что решено не брать и почему), число источников в02, строка вingestion/README.md, запись вdocs/CHANGELOG.md, термины вCONTEXT.md. - Коммит: один источник — один коммит (правки по ревью — отдельными),
сообщение в стиле репозитория:
feat(<code>): «Название» — семейство, адаптер X (SRC-XXX),fix(<code>): …,perf(<code>): …,test(<code>): …,docs(…): …. Заголовок по-русски, называет что и почему. Пуш требует проброшенного ssh-агента (SSH_AUTH_SOCK). - Выкат на стенд: пересборка образа сборщиков на ноутбуке
(
./docker/build.sh, код запекается в образ),sources seed,docker/verify.sh <code>, точечный перенос в стенд (import_catalog(sources=['<code>'])), проверка политики источника и карточек в консоли оператора. - Наблюдение: дождаться ночной волны (00:17 МСК) и дневной (12:17),
прочитать
collect_cron.logиcollect_status.json: длительность источника (и его место вCRON_SOURCES),errorsvsfilter_reasons, число записанных,truncatedу переноса. - Отчёт в карточку и перевод в «Готово»: измеренные числа (собрано /
отсеяно по причинам /
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 код не пишется.
- Реестр каналов — данные, не код. Таблица
channel_registry(или JSON в репозитории на первом шаге): канал/паблик, город, район, тематика, кто добавил, дата, активен ли. 200–400 записей по оценке чата. Пополнение — операторская задача, не релиз. - Доступ к Telegram. Telethon требует пользовательской сессии (номер,
код, 2FA) — как у VK/Добро (
.secrets, keepalive). Кто владелец аккаунта, где живёт сессия, что при бане — решение владельца. - Извлечение. На каждый пост с признаками даты — запрос к модели со
строгой JSON-схемой:
title,starts_at(с поясом; год из контекста поста и даты публикации),ends_at,address,venue_name,is_online,price_text,is_free(только по явному слову),organizer,registration_url,confidence. Поля, которых нет в тексте, —null, не догадка. Промпт и схема — в репозитории, с версией; изменение промпта = перепрогон оценочного набора. - Оценочный набор: 100–200 размеченных вручную постов (события / не
события / поля) в
tests/fixtures/tg_eval/; порог приёмки — точность даты и адреса ≥ 90 %, ложных событий ≤ 5 %. Ниже порога — слой не выкатывается. - Экономия: извлекать только посты с датой-кандидатом (регулярка),
один раз на
post_id(кэш по хешу текста), не перечитывать правки без изменения текста; лимит стоимости на волну. - Что становится карточкой:
source_object_id = "<channel>:<post_id>",source_url— ссылка на пост (t.me/<channel>/<id>), обложка — фото поста (свой CDN-хост в allowlist),raw_payload— текст поста без служебных полей. ПДн: телефоны, личные контакты организаторов из текста в карточку не переносить; сырьё под ретенцией 90 дней;eraseпо запросу. - Публикация:
auto_publish=False— каждую карточку смотрит оператор, пока оценочный набор не докажет качество; в карточкеextras.llm_extractedс версией промпта иconfidence. - Дедуп: тот же механизм каталога; ожидается, что половина постов — репосты событий, уже собранных с Timepad/KudaGo; проверять долю склеек.
- Модель: у проекта уже есть шлюз к модели (раздел «SEO» ходит к Claude через панель на salam0nn.ru); переиспользовать его или завести отдельный ключ — решение владельца. Рестарт панели рвёт и генерацию статей.
- Наблюдение: в
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. Здесь то, на что новый источник
наступает чаще всего.
page=без_numу Хабра отдаёт архив 2023 года — параметры пагинации сверять с network tab, а не с документацией.- У opendata.mkrf данные в нумерованной версии,
$last→ 404; свежая версия не обязательно непустая. - Длинный заголовок переполнял
dedup_signature varchar(512)и терял событие целиком — лечится в каталоге, но длину полей помнить (source_org_id128). - Пустая цена ≠
is_free;capacity=0≠ «мест нет». - Ключ площадки без id — из адреса, не из имени.
- «Москва, г. Сочи» — маркер чужого города проверять до дописывания города.
- Год без года: текущий — неверно почти всегда (Опора: 15 призраков из 76).
- Карточка события, похожая на страницу списка, зацикливает обход
(
moibiz: 129 334 страницы). - Брак карточки в
errorsкрасит волну навсегда — толькоfilter_reasons. - Проба доступа из
configuration_errorпод открытой сессией базы — нельзя. - Парковка в памяти процесса не работает под cron — только в базе.
boolвstatsскладывается как число при слиянии городов — признаки толькоbool, счётчики только числа.- Стойкий кэш деталей ломает изоляцию тестов — выключать в
conftest. - Обложка площадки не в
_IMG_HOST_SUFFIXES— консоль слепа. - Категории источника не в
CATEGORY_MAP—feed_ready=Falseнавсегда. - Перенос в стенд обрезается на 20 000 карточках — новый источник в хвосте.
- Playwright в cron без
PLAYWRIGHT_BROWSERS_PATH— «Executable doesn't exist». curl— жёсткая зависимость образа (JA3), не удобство отладки.- Сессия SQLAlchemy не потокобезопасна — свои сессии на поток города делает оркестратор; в сборщике базы нет вообще.
verify=Falseглобально — никогда.
18. Открытые вопросы к владельцу
- Партнёрские фиды билетных операторов (Ticketscloud, Qtickets,
Radario): парсеры уже есть; нужен ли партнёрский API ради комиссии
(фаза 3 в чате 24.07)? Если да — карточка
SRC-TICKETSCLOUDоживает как интеграция, а не парсер. - PRO.Культура.РФ: партнёрский ключ с обязательством выгружать все события региона за 2 суток — брать или остаёмся на открытых данных МКРФ?
- Telegram-слой: аккаунт для Telethon, модель и бюджет на извлечение, кто ведёт реестр каналов (§15, пункты 1, 2, 7, 9).
- Московское долголетие (55+): расширять ли возрастной сегмент.
- Онлайн-события: сейчас отсев везде; нужен ли отдельный раздел
«онлайн» на витрине (тогда
include_onlineпо умолчанию у части источников). - Leader-ID: номера Тулы и Ярославля (1227359/1265352 против
1283537/1299162) — сверить живьём и оставить один;
placeIds[]. - Перенос в стенд: чинить обрезку курсора (общая проблема) до подключения новых источников или продолжать точечные переносы.
- Порядок волн 6–9 (§4) — утвердить или переставить.
- Регионы: какие города продукт открывает первыми после Москвы (это
определяет, где расширять
supported_cities). - Незакоммиченные правки от 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 для источника
- [ ] Карточка с разведкой и решениями; ADR, если решение на годы.
- [ ] Код по §6–7; докстринги с «зачем»; никаких вторых путей.
- [ ] Все 17 точек регистрации §6.4.
- [ ] Тесты §12.1, фикстуры без сети, даты сдвигаются; весь прогон зелёный.
- [ ] Smoke по одному городу и по всем; числа записаны.
- [ ] Стенд: политика источника, интересы для всех категорий, CDN в allowlist, точечный перенос, карточка видна в консоли с обложкой и адресом.
- [ ] Прошла хотя бы одна ночная и одна дневная волна:
errorsпусты или объяснены, длительность известна, место вCRON_SOURCESпо времени. - [ ]
docs/internal/03,02,README,CHANGELOG,CONTEXT.md. - [ ] Коммит(ы) с сообщением по стилю; карточка в «Готово» с отчётом.
Г. Команды
# окружение
.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 и результат пробы с сервера.