18 KiB
DEV TASK: ET-009 — Новые источники GPS-треков: EnduroRussia + Wikiloc
Статус: Ready for dev
Проект: enduro-trails
Фаза: ET-009
BRD: docs/work-items/ET-008/01-brd.md (расширение §4)
Цель
Реализовать два новых парсера GPS-треков — EnduroRussia.ru (JSON API) и Wikiloc (HTML-парсинг) — и активировать их в pipeline, обновив ADR-010 и создав ADR-012 для Wikiloc.
Архитектура
Оба источника встраиваются в существующую архитектуру ET-008: SourceParser → gps_collect.py → gps_tracks.sqlite. EnduroRussia имеет открытый JSON API (/api/tracks + /api/tracks/{id}/gpx) — парсер простой и надёжный. Wikiloc не имеет публичного API, поэтому используем HTTP-парсинг страниц поиска с httpx + lxml/html.parser, с агрессивным rate limiting и User-Agent.
Стек / Зависимости
- Python 3.11, asyncio, httpx (уже в проекте)
defusedxml(уже в проекте) — для парсинга GPX от EnduroRussialxmlилиhtml.parser(stdlib) — для Wikiloc HTMLgpxpyили ручной парсинг GPX — для извлечения геометрии- Без новых pip-зависимостей если возможно (использовать stdlib xml + httpx)
Инфраструктура
| Параметр | Значение |
|---|---|
| Сервер | slin@82.22.50.71 |
| Рабочая директория | /home/slin/repos/enduro-trails/ |
| Деплой | docker compose build && docker compose run --rm gps-collector |
| Проверка БД | sqlite3 /home/slin/repos/enduro-trails/data/gps_tracks.sqlite "SELECT source_id, count(*) FROM tracks GROUP BY source_id;" |
Файловая карта
| Действие | Файл | Ответственность |
|---|---|---|
| Изменить | src/api/gps_tracks/sources/enduro_russia.py |
Реализовать парсер (сейчас заглушка) |
| Создать | src/api/gps_tracks/sources/wikiloc.py |
Новый парсер Wikiloc |
| Изменить | config/gps_sources.yaml |
Включить enduro_russia, добавить wikiloc |
| Изменить | docs/work-items/ET-008/06-adr/ADR-010-enduro-russia-licensing.md |
status: proposed → accepted |
| Создать | docs/work-items/ET-008/06-adr/ADR-012-wikiloc-licensing.md |
Новый ADR для Wikiloc |
| Изменить | tests/test_sources.py или создать |
Тесты для обоих парсеров |
Задачи
Task 1: ADR-010 — разблокировать EnduroRussia
Файлы:
- Изменить:
docs/work-items/ET-008/06-adr/ADR-010-enduro-russia-licensing.md
Шаги:
- 1.1 Обновить ADR-010: заполнить §1-§3 на основе реального API
Факты для заполнения:
- API публичный, без авторизации:
GET https://endurorussia.ru/api/tracks→ JSON - GPX скачивается напрямую:
GET https://endurorussia.ru/api/tracks/{id}/gpx - robots.txt проверить:
curl https://endurorussia.ru/robots.txt - ToS: проверить
https://endurorussia.ru/aboutили footer - Треки содержат
creator="Wikiloc"в GPX — источник данных Wikiloc, агрегатор - Attribution: "EnduroRussia.ru"
Изменить в frontmatter:
status: accepted
Добавить в тело ADR раздел "## Решение (финальное)":
## Решение (финальное — accepted)
API публичный и открытый (без авторизации, без ключей).
robots.txt: [результат проверки]
ToS: [результат проверки]
Треки содержат creator="Wikiloc" — платформа агрегирует публичные треки.
Attribution в pipeline: "EnduroRussia.ru"
status → accepted [дата]
Критерий готовности: grep "status: accepted" ADR-010-enduro-russia-licensing.md → найдено
Task 2: ADR-012 — создать для Wikiloc
Файлы:
- Создать:
docs/work-items/ET-008/06-adr/ADR-012-wikiloc-licensing.md
Шаги:
- 2.1 Создать ADR-012 по образцу ADR-010, со статусом
accepted
Факты:
- Wikiloc не имеет публичного API (официально отказали)
- Треки публичны на сайте, доступны без авторизации через HTML
- Парсинг HTML страниц поиска — единственный вариант
- Rate limit: 10 сек между запросами, User-Agent с контактом
- Attribution: "© Wikiloc contributors"
- Риск: структура HTML может меняться → парсер хрупкий
---
type: adr
work_item_id: ET-009
adr_id: ADR-012
title: "ADR-012: Источник Wikiloc — HTML-парсинг публичных треков"
status: accepted
created_at: 2026-06-01
authors:
- "agent:developer"
---
Критерий готовности: файл создан, status: accepted
Task 3: Парсер EnduroRussia
Файлы:
- Изменить:
src/api/gps_tracks/sources/enduro_russia.py
Шаги:
- 3.1 Реализовать
EnduroRussiaParser.collect()
API эндпоинты (проверено вручную):
GET https://endurorussia.ru/api/tracks→{"items": [{id, name, description, region, difficulty, length_km, elevation_gain, created_at, user: {id, username}}, ...]}GET https://endurorussia.ru/api/tracks/{id}/gpx→ GPX XML (application/gpx+xml)- Пагинация:
?page=N&limit=50(проверить наличие поляtotalилиnext) - Всего треков: ~305 (id от 1 до 305+)
Логика парсера:
class EnduroRussiaParser(SourceParser):
MAPPING = {
"enduro": "enduro",
"мото": "moto",
"hard": "enduro",
"soft": "enduro",
"тур": "moto",
}
async def collect(self, bbox, ctx):
# 1. GET /api/tracks?page=0&limit=50 — получить список всех треков
# 2. Для каждого трека: GET /api/tracks/{id}/gpx — скачать GPX
# 3. Распарсить GPX → извлечь точки трека → WKB геометрию
# 4. Проверить что bbox трека пересекается с запрошенным bbox
# 5. Yield TrackInsert(...)
# Rate limit: 5 сек между запросами (из конфига)
# external_id: str(track_id)
# external_url: f"https://endurorussia.ru/tracks/{track_id}"
# source_id: "enduro_russia"
# activity_type: map_activity(difficulty или "enduro")
GPX парсинг — использовать уже существующий паттерн из osm.py с defusedxml.
Извлечение геометрии из GPX:
# Из <trkpt lat="..." lon="..."> собрать список (lon, lat) точек
# length_m = сумма haversine расстояний между точками
# WKB = LineString из shapely или ручная упаковка (как в osm.py)
# bbox = min/max lon/lat
- 3.2 Проверить что парсер работает на одном треке:
cd /home/slin/repos/enduro-trails
python3 -c "
import asyncio
from src.api.gps_tracks.sources.enduro_russia import EnduroRussiaParser
cfg = {'id': 'enduro_russia', 'base_url': 'https://endurorussia.ru', 'rate_limit_sec': 2}
p = EnduroRussiaParser(cfg)
async def test():
count = 0
async for t in p.collect((29.0, 49.5, 47.5, 60.0), {}):
print(t.name, t.length_m, t.points_count)
count += 1
if count >= 3:
break
asyncio.run(test())
"
Ожидаемый результат: 3 трека с name, length_m > 0, points_count > 0
Критерий готовности: парсер возвращает треки без ошибок
Task 4: Парсер Wikiloc
Файлы:
- Создать:
src/api/gps_tracks/sources/wikiloc.py
Шаги:
- 4.1 Исследовать структуру HTML Wikiloc для поиска треков по bbox
URL поиска: https://www.wikiloc.com/wikiloc/find.do?act=19&sw=49.5,29.0&ne=60.0,47.5&page=0
act=19— мотоциклы/эндуро (проверить актуальные коды активностей)sw=lat,lon— юго-западный угол bboxne=lat,lon— северо-восточный угол bbox
Альтернативный URL: https://www.wikiloc.com/trails/motorcycle/russia
Парсинг:
# httpx GET страницы поиска
# Найти ссылки на треки: <a href="/wikiloc/view.do?id=...">
# Для каждого трека: GET страницы трека
# Найти ссылку на GPX: <a href="...download...gpx...">
# Скачать GPX → распарсить геометрию
- 4.2 Реализовать
WikilockParser.collect():
class WikilockParser(SourceParser):
MAPPING = {
"motorcycle": "moto",
"enduro": "enduro",
"mtb": "bicycle",
"hiking": "hike",
"running": "hike",
}
async def collect(self, bbox, ctx):
# 1. Поиск треков по bbox через страницу поиска
# 2. Для каждого трека: скачать GPX
# 3. Распарсить → TrackInsert
# Rate limit: 10 сек (жёстко, Wikiloc агрессивно блокирует)
# User-Agent: из конфига (с контактом)
# external_id: wikiloc track id из URL
# external_url: https://www.wikiloc.com/wikiloc/view.do?id={id}
# При 429/403: логировать и прекращать (не ретраить агрессивно)
- 4.3 Проверить:
cd /home/slin/repos/enduro-trails
python3 -c "
import asyncio
from src.api.gps_tracks.sources.wikiloc import WikilockParser
cfg = {'id': 'wikiloc', 'base_url': 'https://www.wikiloc.com', 'rate_limit_sec': 10}
p = WikilockParser(cfg)
async def test():
count = 0
async for t in p.collect((37.0, 55.0, 40.0, 57.0), {}):
print(t.name, t.length_m, t.points_count)
count += 1
if count >= 2:
break
asyncio.run(test())
"
Критерий готовности: парсер возвращает хотя бы 1 трек без ошибок (или graceful skip при 403)
Task 5: Обновить конфиг и включить источники
Файлы:
- Изменить:
config/gps_sources.yaml
Шаги:
- 5.1 Обновить
enduro_russiaentry:enabled: true, исправитьbase_url:
- id: enduro_russia
name: "EnduroRussia.ru"
enabled: true
license_adr: "docs/work-items/ET-008/06-adr/ADR-010-enduro-russia-licensing.md"
base_url: "https://endurorussia.ru"
rate_limit_sec: 5
user_agent: "enduro-trails/1.0 (+https://openclaw.mva154.duckdns.org/enduro/)"
attribution: "EnduroRussia.ru"
parser_module: "src.api.gps_tracks.sources.enduro_russia"
save_user_field: false
source_priority: 80
- 5.2 Добавить
wikilocentry:
- id: wikiloc
name: "Wikiloc"
enabled: true
license_adr: "docs/work-items/ET-008/06-adr/ADR-012-wikiloc-licensing.md"
base_url: "https://www.wikiloc.com"
rate_limit_sec: 10
user_agent: "enduro-trails/1.0 (+https://openclaw.mva154.duckdns.org/enduro/)"
attribution: "© Wikiloc contributors"
parser_module: "src.api.gps_tracks.sources.wikiloc"
save_user_field: false
source_priority: 70
activity_filter: ["motorcycle", "enduro", "mtb"]
Критерий готовности: python3 -c "from src.api.gps_tracks.config import load_sources_config; print([s['id'] for s in load_sources_config('config/gps_sources.yaml').sources])" → содержит enduro_russia и wikiloc
Task 6: Тесты
Файлы:
- Создать/изменить:
tests/test_sources_et009.py
Шаги:
- 6.1 Написать unit-тесты с фикстурами (без реальных HTTP запросов):
# Фикстура: минимальный GPX XML для EnduroRussia
# Фикстура: минимальный HTML страницы Wikiloc с треком
# Тест: EnduroRussiaParser парсит GPX → TrackInsert с корректными полями
# Тест: WikilockParser парсит HTML → находит ссылки на треки
# Тест: map_activity маппит категории правильно
- 6.2 Запустить тесты:
cd /home/slin/repos/enduro-trails
python3 -m pytest tests/test_sources_et009.py -v
Критерий готовности: все тесты зелёные
Task 7: Деплой и smoke-тест
Шаги:
- 7.1 Собрать и запустить коллектор:
cd /home/slin/repos/enduro-trails
docker compose build gps-collector
docker compose run --rm gps-collector python -m scripts.gps_collect --source enduro_russia --dry-run
- 7.2 Если dry-run OK — запустить реальный сбор (только EnduroRussia, ограниченный регион):
docker compose run --rm gps-collector python -m scripts.gps_collect --source enduro_russia --region north_caucasus
- 7.3 Проверить БД:
sqlite3 /home/slin/repos/enduro-trails/data/gps_tracks.sqlite \
"SELECT source_id, count(*), round(avg(length_m)/1000,1) as avg_km FROM tracks GROUP BY source_id;"
Ожидаемый результат: строка enduro_russia | N | X.X где N > 0
- 7.4 Wikiloc — запустить с ограниченным bbox (Москва):
docker compose run --rm gps-collector python -m scripts.gps_collect --source wikiloc --region moscow_test
Если 403/429 — зафиксировать в логах, это ожидаемо. Главное — graceful handling без краша.
- 7.5 Закоммитить:
cd /home/slin/repos/enduro-trails
git add -A
git commit -m "feat(ET-009): add EnduroRussia and Wikiloc GPS sources"
git push
Критерий готовности: коммит запушен, в БД есть треки от enduro_russia
Проверка (Acceptance)
| # | Проверка | Команда | Ожидаемый результат |
|---|---|---|---|
| 1 | ADR-010 accepted | grep "status: accepted" docs/work-items/ET-008/06-adr/ADR-010-enduro-russia-licensing.md |
найдено |
| 2 | ADR-012 создан | cat docs/work-items/ET-008/06-adr/ADR-012-wikiloc-licensing.md | grep status |
status: accepted |
| 3 | Парсер EnduroRussia работает | python3 -c "..." (Task 3.2) |
3 трека без ошибок |
| 4 | Конфиг обновлён | grep "enabled: true" config/gps_sources.yaml | wc -l |
≥ 2 |
| 5 | Треки в БД | sqlite3 data/gps_tracks.sqlite "SELECT source_id, count(*) FROM tracks GROUP BY source_id;" |
строка enduro_russia с N > 0 |
| 6 | Тесты зелёные | python3 -m pytest tests/test_sources_et009.py -v |
все PASSED |
| 7 | Коммит запушен | git log --oneline -1 |
feat(ET-009): ... |
Ограничения и контекст
- ⚠️ ADR licensing guard: pipeline проверяет
statusв ADR файле перед запуском source. Покаstatus: proposed— source пропускается. Нужно обновить ADR ДО включенияenabled: trueв конфиге. - ⚠️ base_url в конфиге: текущий конфиг содержит
https://enduro-russia.ru(с дефисом), реальный URLhttps://endurorussia.ru(без дефиса). Исправить. - ⚠️ Wikiloc без API: парсинг HTML хрупкий. Если структура изменилась — парсер вернёт 0 треков, не упадёт. Логировать предупреждение.
- ⚠️ Wikiloc rate limit: 10 сек минимум. При 429 — остановить сбор, записать в pipeline_runs.status = "rate_limited", не ретраить.
- ⚠️ GPX от EnduroRussia содержит Wikiloc-треки: дедупликация по dedup_key (bbox-bucket + length + date) должна отловить дубли если один трек есть и в Wikiloc и в EnduroRussia.
- ⚠️ docker compose run для gps-collector использует profile
batch— нужно--profile batchилиrun --rm gps-collector. - 🚫 Не трогать
osm.pyи существующие тесты ET-008. - 🚫 Не менять схему БД — только новые source_id в существующих таблицах.
- 🚫 Не добавлять новые pip-зависимости без крайней необходимости (использовать stdlib + уже установленные).
Деплой-чеклист
- ADR-010 обновлён (status: accepted)
- ADR-012 создан (status: accepted)
enduro_russia.pyреализован (не заглушка)wikiloc.pyсозданgps_sources.yamlобновлён (оба enabled: true)- Тесты написаны и зелёные
docker compose buildпрошёл без ошибок- Dry-run EnduroRussia — OK
- Реальный сбор EnduroRussia — треки в БД
- Wikiloc — graceful handling (треки или 403 без краша)
- Коммит запушен в main
Создано: 2026-06-01 | Автор ТЗ: Стрим | Исполнитель: Dev-агент