# Пилот парсинга RF4DB и RF4-STAT Текущий статус разрешений, атрибуции и консервативных лимитов зафиксирован в `docs/data-permissions.md`. Дата контрольного запуска: **3 сентября 2026 года**. Владелец RF4 Spotter подтвердил наличие разрешений на получение данных из обоих сервисов. Пилот использует только публичный HTML, не обращается к закрытым API, не обходит авторизацию/Premium и не скачивает изображения. ## RF4DB Проверенная страница: `https://download.rf4db.com/ru/catches`. HTML формируется Next.js, но готовые карточки присутствуют в серверном ответе. Из карточки извлекаются: - UUID улова и каноническая detail-ссылка; - русское название и внешний slug рыбы; - русское название и внешний ID водоёма; - координаты; - игровое время; - приманка и её внешний ID; - оснастка; - погода и температура воды. На контрольной странице найдено 24 элемента `catch-card`: 23 являлись уловами с detail-ссылкой, один — неполным/служебным элементом и безопасно пропущен. У всех 23 уловов были координаты. Вес, игрок и абсолютное время публикации отсутствовали и не подменяются догадками. Detail-страница дополнительно содержит ветер, клипсу, выпуск лески, направление заброса и полный список снастей, но по-прежнему не содержит вес. Добавлен отдельный `RF4DBCatchDetail`: контрольная живая страница вернула ветер, три числовых параметра заброса и 11 элементов снасти. Detail-запросы не выполняются автоматически для всей выдачи, чтобы не умножать нагрузку на источник. `robots.txt` на download-домене запрещает автоматический обход для `User-Agent: *` и `/api/`. Наличие отдельного разрешения нужно сохранить в документации проекта. Публичный API не исследовался и не использовался; для пилота взят только серверный HTML одной страницы. ## RF4-STAT Проверенные страницы: - `https://rf4-stat.ru/fishing/`; - `https://rf4-stat.ru/posts/`; - `https://rf4-stat.ru/active-spots/`. Запросы выполнялись последовательно с `Crawl-delay: 5`, указанным в `robots.txt`. Из таблицы `/fishing/` извлекаются ID, ссылка, рыба и slug, вес, водоём, приманка, игрок, время публикации, клипса, стиль ловли и ссылка на изображение. Контрольный результат: **100 из 100 строк**. Вес присутствовал во всех 100 строках; координаты в публичном представлении были замаскированы и не извлекались. Из `/posts/` один пост разворачивается в одно или несколько наблюдений по числу рыб. Извлекаются ID `post:fish_index`, время публикации Unix, рыба, вес, водоём, приманки, игрок, клипса, стиль и URL миниатюр-доказательств. Контрольный результат: **16 наблюдений из 10 постов**, у всех 16 были вес и доказательства. Координаты и погода, помеченные `position-locked`/`locked weather`, намеренно не извлекались. `/active-spots/` содержит десять агрегированных карточек, но публично видимые координаты и интерактивная статистика заблокированы, а стабильного ID наблюдения и рыбы нет. Эти карточки не преобразуются в уловы. Значения из скрытых `data-position` не используются. ## Общий контракт Парсеры возвращают `ExternalCatch` и никогда не записывают данные непосредственно в БД. Поля `weight_g`, координаты, игрок и время nullable, потому что источники дополняют друг друга, но не должны склеиваться только из-за похожих значений. Текущие пространства идентификаторов разделены: - `rf4db` + UUID; - `rf4stat-fishing` + числовой ID; - `rf4stat-post` + `post_id:fish_index`. - `rf4map` + числовой ID отдельного наблюдения; - `rf4posts-spot` + `spot_uuid:fish_slug`. Следующий слой импорта должен хранить `source_system` отдельно от внешнего ID. Автоматическое объединение RF4DB и RF4-STAT пока запрещено: одна и та же публикация может присутствовать в обоих агрегаторах, но надёжного общего первичного ключа нет. ## Реализовано - `rf4_research/community_sources.py` — пять fail-closed HTML-парсеров; - `parse_rf4db_detail` — отдельное обогащение одного RF4DB-улова; - `python -m rf4_research.community_cli` — read-only CLI одного ограниченного снимка; - обезличенные минимальные фикстуры для каждого контракта; - тесты всех извлекаемых полей, locked-координат и отказа на постороннем HTML; - живой контрольный прогон без сохранения персональных данных и изображений в репозиторий. RF4MAP и RF4 Posts остаются исследовательскими: на живых HTML-снимках получены соответственно 30 индивидуальных наблюдений и 6 видов рыб одной точки. Оба результата укладываются в nullable-контракт, имеют отдельные пространства ID и не смешиваются с RF4DB/RF4-STAT. Разрешение и минимальный интервал 30 минут подтверждены; источники разрешены для staging, но остаются выключенными. RF4 Posts трактуется как агрегированная точка, а не индивидуальный улов. ## Перед продуктивным импортом 1. Сохранить подтверждение разрешения и согласованные лимиты запросов. 2. Расширять добавленные алиасы рыб и водоёмов по мере ручной проверки новых значений. 3. Согласовать начальные уровни доверия для RF4DB и двух каналов RF4-STAT (в staging записаны консервативные значения 70/65/60, но источники выключены). 4. Проверить правила публикации на небольшой вручную подтверждённой выборке. 5. Не хранить и не проксировать изображения без отдельного условия разрешения; на первом этапе достаточно исходной ссылки. ## Staging Миграция `0008` добавляет справочник `data_source` и таблицу `external_observation`. Уникальность `(source_system, source_external_id)` делает повторную загрузку идемпотентной; `first_seen_at` сохраняется, `last_seen_at` и payload обновляются. Вес, координаты и время могут отсутствовать. Загрузка принимает только известные источники, HTTPS-ссылки соответствующего домена и значения в допустимых диапазонах. Все записи получают статус `staged`; таблица не связана с расчётом активности. Тест доказывает путь `HTML → ExternalCatch → JSON → external_observation` без потери provenance. Контрольная загрузка в PostgreSQL создала 139 staging-записей: 23 `rf4db` с координатами, 100 `rf4stat-fishing` с весом и 16 `rf4stat-post` с весом. Немедленный повтор дал `created=0, updated=139`, подтвердив идемпотентность на реальной БД. Все три источника остались `enabled=false`. Миграция `0009` добавляет устойчивые алиасы внешних рыб и водоёмов, канонические ссылки и состояние ручной проверки. Страница `/admin/external-sources` позволяет сопоставить, отклонить или явно опубликовать наблюдение. Публикация создаёт одобренный `catch_report` с исходной ссылкой и полным provenance, но только если одновременно известны канонические сущности, координаты и вес. Поэтому текущие 139 неполных записей после миграции остались в staging.