diff --git a/backend/app/services/scrapers/okn_egrkn_client.py b/backend/app/services/scrapers/okn_egrkn_client.py index e2d2a0ea..c701229c 100644 --- a/backend/app/services/scrapers/okn_egrkn_client.py +++ b/backend/app/services/scrapers/okn_egrkn_client.py @@ -1,15 +1,22 @@ -"""Клиент АИС ЕГРКН Минкультуры — поиск ОКН-объектов на карте (#1066). +"""Клиент АИС ЕГРКН Минкультуры — поиск ОКН-объектов на карте + detail-card (#1066, #1159). + +Источник: +- list: ``POST https://okn-mk.mkrf.ru/Maps/searchMap`` (Content-Type: form-urlencoded). +- detail: ``GET https://okn-mk.mkrf.ru/Show/Show?id={source_id}`` (HTML-карточка). -Источник: https://okn-mk.mkrf.ru/Maps/searchMap -Метод: POST, Content-Type: application/x-www-form-urlencoded Аутентификация: не требуется (открытый endpoint). -Ответ: GeoJSON FeatureCollection. -Каждая фича: {id, geometry: {type: "Point", coordinates: [lat, lon]}, properties: {...}}. +Ответ searchMap: GeoJSON FeatureCollection. +Каждая фича: {id, geometry: {type: "Point", coordinates: [lat, lon]}, properties: {id}}. ВНИМАНИЕ: ЕГРКН использует Яндекс-порядок координат [lat, lon], НЕ [lon, lat]. Для PostGIS: ST_MakePoint(coordinates[1], coordinates[0]) → (lon, lat). +searchMap отдаёт в properties **только {id}** — название/адрес/датировка приходят +отдельным detail-endpoint'ом ``Show/Show?id=`` (HTML-карточка ОКН с метаданными). +Парсим название/адрес/категорию охраны через BeautifulSoup, ошибки detail-парсинга +не валят прогон (raw_props сохраняется без detail-полей, баг #1159 acceptance 2). + TLS: mkrf.ru использует сертификат, верификация по умолчанию включена. При SSLError автоматический fallback на verify=False (аналогично ekburg_permits #242). """ @@ -17,17 +24,23 @@ TLS: mkrf.ru использует сертификат, верификация from __future__ import annotations import logging +import re import urllib.parse from typing import Any import httpx +from bs4 import BeautifulSoup logger = logging.getLogger(__name__) _SEARCHMAP_URL = "https://okn-mk.mkrf.ru/Maps/searchMap" +_DETAIL_URL = "https://okn-mk.mkrf.ru/Show/Show" -# Таймаут на один HTTP-запрос к ЕГРКН (секунды) +# Таймаут на один HTTP-запрос к ЕГРКН (секунды). +# Detail-вызов короче (10 s) — карточки лёгкие, длинный timeout растягивал бы прогон +# на сотни секунд при ~700 объектах. _REQUEST_TIMEOUT = 30.0 +_DETAIL_TIMEOUT = 10.0 # Browser headers -- required: without User-Agent the server returns 403. # Content-Type включён явно: при content= (raw bytes/str) httpx не выставляет его @@ -43,6 +56,15 @@ _HEADERS: dict[str, str] = { "Content-Type": "application/x-www-form-urlencoded; charset=UTF-8", } +# Detail-карточка отдаёт HTML, не JSON — Content-Type form-urlencoded убираем, +# X-Requested-With оставляем для совместимости (на случай SSR-условного рендера). +_DETAIL_HEADERS: dict[str, str] = { + "User-Agent": _HEADERS["User-Agent"], + "Referer": "https://okn-mk.mkrf.ru/Maps", + "Accept": "text/html,application/xhtml+xml", + "Accept-Language": "ru-RU,ru;q=0.9", +} + def _build_form_data(address: str, category_type: int) -> list[tuple[str, str]]: """Построить form-data в формате jQuery serializeArray для поиска по карте. @@ -68,7 +90,8 @@ def fetch_okn_points(address: str, category_type: int) -> list[dict[str, Any]]: Returns: Список feature-dict'ов из GeoJSON FeatureCollection. Каждый dict содержит поля: id, geometry (с coordinates=[lat, lon]), - properties (сырые свойства из ЕГРКН, часть полей требует авторизации). + properties (обычно {id} — название/адрес/датировку нужно догружать + через ``fetch_okn_detail``). Пустой список при ошибке или пустом ответе. Note: @@ -144,4 +167,146 @@ def _do_request( return features -__all__ = ["fetch_okn_points"] +# ── Detail-карточка ────────────────────────────────────────────────────────── + + +# Поля карточки ЕГРКН: «Наименование», «Адрес», «Категория историко-культурного значения», +# «Вид объекта», «Датировка», «Документ о постановке на гос. охрану», «Регистрационный номер». +# HTML-структура — labelled rows, label слева, value справа в одном tr/td-блоке. +# Используем устойчивые keys-маркеры (lowercased substring match) для тегов
//