Skip to content

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

39 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

vk-glamping-parser

CI

HTTP-микросервис на Go: собирает карточку глэмпинга для фронтенда aframedomik.ru. По домену VK достаёт фотографии и данные домиков, структурирует описание (удобства, цены, доп.услуги, правила) и отдаёт единым JSON-ом — как страница Сабадури: объект + список А-фрейм домиков.

Источники данных по объекту:

  • фото — из VK (photos.get, оставляем 15 лучших по разрешению);
  • домики — товары VK (market.getById по прямым id) и/или «ручные» домики из конфига (например, описание с Avito, который ботами не парсится);
  • координаты — параметр запроса → конфиг → геокодер по адресу (Nominatim).

Стек

  • Go 1.24, стандартный net/http (сервер и клиент к VK), log/slog (логи);
  • github.com/joho/godotenv — загрузка .env;
  • github.com/anthropics/anthropic-sdk-go — опциональное LLM-извлечение.

Структура

app/               точка входа: main / types / handler / helpers / deps
internal/config/   конфигурация из окружения (.env)
internal/vk/       клиент VK API (resolveScreenName, photos.get, market.getById)
internal/extract/  структурирование описания (Extractor: Heuristic | LLM)
internal/objects/  пер-объектные конфиги data/<domain>.json
internal/geocode/  адрес → координаты через Nominatim (OSM)
internal/cache/    in-memory TTL-кэш ответов
internal/images/   конвейер фото (альбом → дедуп → отбор → webp)
data/              конфиги объектов (<domain>.json)

Запуск

cp .env.example .env             # подставь свой VK_TOKEN
go mod tidy
go run ./app                     # сервис слушает :8080
go build -o bin/parser ./app     # сборка бинаря (bare `go build ./app` нельзя:
                                 #   имя бинаря совпало бы с папкой app/)
go test ./...                    # тесты (без сети)

Экспорт фото объекта

Готовит галерею объекта для фронта: photo-1.webp … photo-15.webp (макс. сторона 1280, q80). Требует установленного imagemagick (magick).

go run ./app -export elkidom37 -out ../iv-iframes/public/elkidom
# без -out кладёт в export/<domain>/

Конвейер (internal/images):

  1. Источник — альбом дома, а не стена. Из описаний товаров (config items) достаётся ссылка «ВСЕ ФОТО ДОМА: vk.com/album-<owner>_<album>» → тянем photos.get этого альбома. Фоллбэк на стену, если альбома нет (напр. страница-пользователь). Стена даёт повторы и обложки товаров с текстом.
  2. Дедуп по перцептивному хэшу (goimagehash) — убирает повторяющиеся кадры.
  3. Порядок и обложка. Кадры без людей — вперёд (детекция лиц pigo, чистый Go), с людьми — в конец (первые ~10 без людей). Внутри — экстерьеры (высокая «уличность»: доля зелени/неба) раньше интерьеров, поэтому обложка обзорная.
  4. Ресайз + webp через magick (шелл-аут, без cgo).

Ограничения (честно): детекция людей — фронтальные лица (кадр со спины может пройти), «уличность» — эвристика по цвету. Главный рычаг чистоты — источник-альбом.

API

GET /api/glamping?domain=<screen_name>[&items=...][&coords=lat,lon][&map=URL]
Параметр Обяз. Описание
domain да VK screen name (буквы/цифры/._). Прочее → 400
items нет товары-домики: полные ссылки или id через запятую
coords нет координаты "lat,lon", если VK не отдал
map нет ссылка на карту

Параметры приоритетнее значений из конфига data/<domain>.json. Минимально достаточно ?domain=..., если объект описан конфигом.

curl "http://localhost:8080/api/glamping?domain=elkidom37"

Контракт ответа

Ответ — три уровня: объект → домики (cabins) → структура домика (property).

Объект-уровень:

Поле Тип Описание
title string название глэмпинга (имя VK-группы)
about string описание сообщества
location string строка для показа; исторический формат, разбирать обратно нельзя — для этого поля ниже
region string регион/направление источника; не гарантированно субъект РФ; отсутствует, если неизвестен
locality string населённый пункт объекта; отсутствует, если реально неизвестен — пустое честнее чужого города
nearCity string опорный город направления (откуда ехать), не адрес объекта
houseTypes string[] формы жилья объекта: A-frame, Купольный дом, Барнхаус… Список, потому что у объекта бывают корпуса разной формы
surroundings string[] что вокруг: лес, река, озеро, склон. Из тегов источника, не из разбора описания
petsAllowed bool можно ли с питомцем. Отсутствует = источник не сказал; false = сказал «нельзя»
rating / reviewsCount number оценка и число отзывов числами — для сортировки каталога
distanceKm / distanceFrom number / string сколько километров и от чего их считать: источник меряет то от МКАД, то от города, то от аэропорта. Без точки отсчёта цифра ничего не значит, и это НЕ nearCity
highway string шоссе, канонизированное: хвост «(Направление: Казань)» отброшен, иначе одна дорога даёт две строки фильтра
priceValue number цена числом (cabins[0].price остаётся строкой «7 360 ₽» для показа)
guestsMax number вместимость самого большого домика; отсутствует, если источник промолчал
coords {lat,lon} координаты (опционально)
mapUrl string ссылка на карту (опционально)
contact string телефон (опционально)
photos string[] галерея, до 15 лучших фото
cabins Cabin[] домики

Поля для фильтров — числами и списками, а не строками. rating, priceValue, guestsMax, distanceKm дублируют то, что и раньше лежало в cabins[].property.facts строками вида «до 6 гостей» и «4.8 · 129 отзывов». Строка годится показать, но не отфильтровать: разбирать её регуляркой на стороне сайта — значит завести четвёртое место, где формат источника трактуется, и получить четвёртый источник расхождений.

Пустое поле — это ответ. Ни одно из них не имеет дефолта. Вместимость раньше подставлялась как «до 4» и стояла почти у всего каталога при реальном разбросе от 2 до 27: гость видел «до 4», ехал вчетвером, а его ждала одна двуспальная кровать. Фильтр обязан пропускать «неизвестно» мимо, а не выдавать за подходящее.

Адрес. Поля заполняет тот провайдер, чей источник их различает. У глэмпинги.рф это регион и опорный город — населённого пункта источник в списке не отдаёт вовсе:

"location": "Московская область, Москва",
"region":   "Московская область",
"nearCity": "Москва"

«Москва» здесь означает точку отсчёта расстояния, а не адрес (подробности и цифры — в комментарии к apiCity, providers/glamping_rf/types.go). Поэтому locality отсутствует — это правильное состояние, а не пробел в данных. У VK-объектов источник даёт одну свободную строку, поэтому заполнен только location.

В Preview (облегчённая карточка для списков) из адреса есть только location и region: по региону идёт фильтрация, а location нужен для полнотекстового поиска — искать по нему подстрокой можно и нужно, запрет касается только разбора его на составляющие.

Поля фильтров в Preview. Кроме адреса, превью несёт surroundings, petsAllowed, guestsMax, priceValue, rating. Это единственное исключение из правила «превью облегчённое», и у него есть причина: каталог фильтруется на клиенте по уже загруженному списку, а список — это Preview. Чего в нём нет, по тому и не отфильтруешь; ровно так чипсы каталога и оказались мёртвыми — они читали tags, которых в превью нет и не было.

houseTypes в превью не берём: фильтр по форме дома отложен, а поле заполнено у 133 объектов из 309 — больше половины каталога промолчало бы, и раздел показывал бы не «домов такой формы нет», а «мы не знаем».

Новые поля появляются в выдаче после пересбора: go run ./app --provider glamping и обновление generated/. Мержа кода недостаточно — до пересбора API отдаёт прежний набор полей.

Домик (cabins[i]):

Поле Тип Описание
title / price / description string сырые данные товара
property Property структурированная карточка (ниже)
variants string[] названия схлопнутых дублей (напр. «AFRAME тёмный»)

Структура домика (property):

Поле Тип Описание
summary string краткое описание
priceFrom string цена
facts {label,value}[] факты (вместимость, площадь…)
amenityGroups {title,items[]}[] удобства по группам
extras {name,price}[] платные доп.услуги
rules string[] правила проживания

Пример (сокращён):

{
  "title": "ЁLKI.DOM ГЛЭМПИНГ / БАНЯ / Отдых Иваново",
  "location": "Иваново",
  "coords": { "lat": 57.070886, "lon": 41.01518 },
  "mapUrl": "https://yandex.ru/maps/org/elki_dom/205090190510/",
  "photos": ["https://sun9-...userapi.com/...jpg"],
  "cabins": [
    {
      "title": "AFRAME светлый (аренда )",
      "price": "7,000–9,500 ₽",
      "property": {
        "summary": "АРЕНДА светлого ДОМА…",
        "priceFrom": "7,000–9,500 ₽",
        "facts": [{ "label": "Вместимость", "value": "до 4 чел." }],
        "amenityGroups": [
          { "title": "В домике", "items": ["Кухня (оборудованная)", "Интернет"] }
        ],
        "extras": [{ "name": "Растопка Фурако / чана", "price": "" }],
        "rules": ["заезд в 15:00, выезд до 12:00"]
      },
      "variants": ["AFRAME тёмный (аренда)"]
    }
  ]
}

Заголовок X-Cache: HIT|MISS показывает, отдан ли ответ из кэша.

Код Когда
200 успех
400 не передан или невалиден domain
502 ошибка при обращении к VK API

Пер-объектный конфиг data/<domain>.json

Один файл на глэмпинг — ручные данные, которых нет в VK. Все поля необязательны.

{
  "address": "Ивановская обл., д. Крюково, Славянская ул., 6",
  "coords": "57.082342,40.876057",
  "map": "https://yandex.ru/maps/org/scandi/237456869090/",
  "items": ["https://vk.com/market/product/aframe-211011668-6377368"],
  "cabins": [
    { "title": "Scandi Villa", "price": "", "description": "<текст из Avito>" }
  ]
}
  • items — товары-домики VK (грузятся через market.getById);
  • cabins — «ручные» домики (источники, недоступные API, например Avito).

В репозитории лежит только шаблон data/example.json. Реальные конфиги объектов (data/<domain>.json) не коммитятся (см. .gitignore) — они содержат чужой контент (описания с VK/Avito) и хранятся локально.

Если каталог VK скрыт (market.get пуст) — id товаров берутся отсюда или из параметра items.

Конфигурация (окружение)

Переменная Обяз. Описание
VK_TOKEN да токен доступа к VK API
ANTHROPIC_API_KEY нет если задан — структурирование через Claude, иначе бесплатная эвристика
SERVER_ADDR нет адрес сервера (по умолчанию :8080)
DATA_DIR нет каталог конфигов объектов (по умолчанию data)

.env в репозиторий не коммитится (см. .gitignore).

Извлечение структуры

internal/extract прячет движок за интерфейсом Extractor:

  • Heuristic (по умолчанию) — бесплатно, по словарю ключевых слов (dictionary.go) и регэкспам. Работает офлайн, без ключей.
  • LLMClient — Claude (strict tool-use), включается при наличии ANTHROPIC_API_KEY. Качественнее, но платно.

Переключение — один if в main; остальной код не меняется.

About

HTTP-микросервис на Go: парсер глэмпингов из VK API

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages