fix(tradein): реальные координаты Cian/N1 через геокодинг #379
4 changed files with 39 additions and 24 deletions
|
|
@ -111,23 +111,36 @@ async def scrape_around(
|
|||
)
|
||||
|
||||
|
||||
def _clean_address_for_geocode(addr: str) -> str:
|
||||
"""Чистим address для геокодера.
|
||||
|
||||
Cian отдаёт «улица Латвийская, 56/3 · р-н Чкаловский» — суффикс ' · ...'
|
||||
мешает Nominatim. Берём часть до ' · '. N1 отдаёт «Репина, 75/2 стр.» — ок.
|
||||
"""
|
||||
main = addr.split(" · ")[0].strip()
|
||||
return main or addr
|
||||
|
||||
|
||||
@router.post("/geocode-missing")
|
||||
async def geocode_missing(
|
||||
db: Annotated[Session, Depends(get_db)],
|
||||
limit: int = 50,
|
||||
limit: int = 500,
|
||||
) -> dict:
|
||||
"""Геокодинг listings у которых нет lat/lon (используя address).
|
||||
|
||||
Запускать после scrape если парсер не достал координаты.
|
||||
Nominatim rate-limited 1 req/sec — поэтому небольшой batch.
|
||||
Запускать после scrape — Cian/N1 не отдают координаты, только адрес.
|
||||
Nominatim rate-limited 1 req/sec → большой batch идёт ~limit секунд.
|
||||
geom обновляется автоматически триггером listings_set_geom_trg.
|
||||
"""
|
||||
rows = db.execute(
|
||||
text(
|
||||
"""
|
||||
SELECT id, address, raw_payload
|
||||
SELECT id, address
|
||||
FROM listings
|
||||
WHERE (lat IS NULL OR lon IS NULL)
|
||||
AND COALESCE(address, '') != ''
|
||||
AND address NOT LIKE '%(Avito)%'
|
||||
AND address NOT LIKE '%(N1)%'
|
||||
ORDER BY scraped_at DESC
|
||||
LIMIT :limit
|
||||
"""
|
||||
|
|
@ -138,18 +151,14 @@ async def geocode_missing(
|
|||
geocoded = 0
|
||||
skipped = 0
|
||||
for row in rows:
|
||||
result = await geocode(row["address"], db)
|
||||
clean = _clean_address_for_geocode(row["address"])
|
||||
result = await geocode(clean, db)
|
||||
if result is None:
|
||||
skipped += 1
|
||||
continue
|
||||
# geom пересчитается триггером из lat/lon.
|
||||
db.execute(
|
||||
text(
|
||||
"""
|
||||
UPDATE listings
|
||||
SET lat = :lat, lon = :lon
|
||||
WHERE id = :id
|
||||
"""
|
||||
),
|
||||
text("UPDATE listings SET lat = :lat, lon = :lon WHERE id = :id"),
|
||||
{"lat": result.lat, "lon": result.lon, "id": row["id"]},
|
||||
)
|
||||
db.commit()
|
||||
|
|
|
|||
|
|
@ -346,12 +346,12 @@ class CianScraper(BaseScraper):
|
|||
if ppm2 > 0:
|
||||
area = round(price / ppm2, 2)
|
||||
|
||||
# Coords: anchor + jitter (Cian не отдаёт координаты в card HTML, только в API)
|
||||
# Coords: НЕ ставим (Cian card HTML не отдаёт). Оставляем None —
|
||||
# geocode-missing проставит реальные по address. Anchor+jitter тут
|
||||
# был БАГ: Cian отдаёт ВСЕ объявления ЕКБ независимо от anchor, и
|
||||
# jitter вокруг одного якоря давал фейковые координаты → ST_DWithin
|
||||
# не находил аналоги для адресов вдали от центра.
|
||||
lat = lon = None
|
||||
if hasattr(self, "_anchor_lat"):
|
||||
jitter_deg = (self._anchor_radius_m / 1000) / 111
|
||||
lat = self._anchor_lat + (random.random() - 0.5) * jitter_deg
|
||||
lon = self._anchor_lon + (random.random() - 0.5) * jitter_deg / 2
|
||||
|
||||
# Photos из img.src в card (первые 5)
|
||||
photo_urls: list[str] = []
|
||||
|
|
|
|||
|
|
@ -12,13 +12,13 @@ SSR HTML с DOM-карточками `data-test="offers-list-item"`.
|
|||
- [data-test="offers-list-item-param-price"] — "15 700 000"
|
||||
- img.src — фото
|
||||
|
||||
Координаты карточки не отдаёт → anchor+jitter (как Avito).
|
||||
Координаты карточки не отдаёт → оставляем lat/lon = None,
|
||||
geocode-missing проставит реальные по адресу.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import random
|
||||
import re
|
||||
from typing import Any
|
||||
|
||||
|
|
@ -167,12 +167,10 @@ class N1Scraper(BaseScraper):
|
|||
if not price:
|
||||
return None
|
||||
|
||||
# Координаты: anchor + jitter (N1 card не отдаёт coords)
|
||||
# Координаты НЕ ставим — N1 card не отдаёт. Оставляем None,
|
||||
# geocode-missing проставит реальные по address (N1 отдаёт всё ЕКБ,
|
||||
# jitter вокруг якоря был бы фейком).
|
||||
lat = lon = None
|
||||
if hasattr(self, "_anchor_lat"):
|
||||
jitter_deg = (self._anchor_radius_m / 1000) / 111
|
||||
lat = self._anchor_lat + (random.random() - 0.5) * jitter_deg
|
||||
lon = self._anchor_lon + (random.random() - 0.5) * jitter_deg / 2
|
||||
|
||||
# Фото
|
||||
photos: list[str] = []
|
||||
|
|
|
|||
|
|
@ -63,4 +63,12 @@ for anchor in "${ANCHORS[@]}"; do
|
|||
sleep 5
|
||||
done
|
||||
|
||||
# Геокодинг свежих лотов без координат (Cian/N1 отдают только адрес).
|
||||
# Nominatim 1 req/sec → batch ~500 идёт до ~8 мин.
|
||||
log "→ geocode-missing"
|
||||
geo=$(docker exec "$CONTAINER" curl -s -X POST \
|
||||
"http://localhost:8000/api/v1/admin/geocode-missing?limit=500" \
|
||||
-m 700 2>/dev/null || echo '{"error":"geocode_timeout"}')
|
||||
log "← geocode-missing $geo"
|
||||
|
||||
log "=== cron-scrape done ==="
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue