Merge pull request 'Домклик: число собственников снова читается, жилая площадь и балконы не стираются переобходом, нули в площадях — не площадь' (#3569) from fix/domclick-card-fields into main
All checks were successful
Deploy Trade-In / build-browser (push) Successful in 1m8s
Deploy Trade-In / build-frontend (push) Successful in 4m55s
Deploy Trade-In / test (push) Successful in 6m25s
Deploy Trade-In / build-backend (push) Successful in 3m22s
Deploy Trade-In / deploy (push) Successful in 3m13s
Deploy Trade-In / deploy-status (push) Successful in 3s
Deploy Trade-In / perimeter-smoke (push) Successful in 1m53s
Deploy Trade-In / changes (push) Successful in 30s
All checks were successful
Deploy Trade-In / build-browser (push) Successful in 1m8s
Deploy Trade-In / build-frontend (push) Successful in 4m55s
Deploy Trade-In / test (push) Successful in 6m25s
Deploy Trade-In / build-backend (push) Successful in 3m22s
Deploy Trade-In / deploy (push) Successful in 3m13s
Deploy Trade-In / deploy-status (push) Successful in 3s
Deploy Trade-In / perimeter-smoke (push) Successful in 1m53s
Deploy Trade-In / changes (push) Successful in 30s
This commit is contained in:
commit
be1403289a
6 changed files with 286 additions and 19 deletions
|
|
@ -0,0 +1,29 @@
|
||||||
|
-- 320_listings_domclick_zero_area_to_null.sql
|
||||||
|
-- Issue #3252: Домклик отдаёт `livingArea: 0` / `kitchenArea: 0` как «не указано»
|
||||||
|
-- (живая карточка 2078257603, 17.09: обе площади 0, ремонт пустой). Парсер карточки
|
||||||
|
-- писал этот 0 в колонку как настоящую площадь; COALESCE(:new, old) в UPDATE при этом
|
||||||
|
-- затирал нулём ранее известное значение. Писатель починен в том же PR
|
||||||
|
-- (providers/domclick/detail.py::_pos_float), но починка разбора строк не чинит:
|
||||||
|
-- COALESCE(NULL, 0) оставит 0 навсегда.
|
||||||
|
--
|
||||||
|
-- ЗАМЕР ПРОДА 2026-09-17 (SELECT source, count(*) FILTER (WHERE kitchen_area_m2=0) ...):
|
||||||
|
-- domklik: kitchen_area_m2 = 0 — 303 строки, living_area_m2 = 0 — 84 строки;
|
||||||
|
-- все обогащены карточкой 26.08–14.09 (detail_enriched_at), ни одной до этого.
|
||||||
|
-- avito / cian / yandex / n1: нулей нет вовсе.
|
||||||
|
-- Ожидаемо тронуто: 303 + 84 строки. Отрицательных площадей нет ни у кого.
|
||||||
|
--
|
||||||
|
-- Idempotent: повторный прогон — 0 строк.
|
||||||
|
|
||||||
|
BEGIN;
|
||||||
|
|
||||||
|
UPDATE listings
|
||||||
|
SET kitchen_area_m2 = NULL
|
||||||
|
WHERE source = 'domklik'
|
||||||
|
AND kitchen_area_m2 = 0;
|
||||||
|
|
||||||
|
UPDATE listings
|
||||||
|
SET living_area_m2 = NULL
|
||||||
|
WHERE source = 'domklik'
|
||||||
|
AND living_area_m2 = 0;
|
||||||
|
|
||||||
|
COMMIT;
|
||||||
|
|
@ -83,9 +83,9 @@ _SSR_LITERAL = """{
|
||||||
},
|
},
|
||||||
"legalOptions": {"saleType": "Свободная продажа"},
|
"legalOptions": {"saleType": "Свободная продажа"},
|
||||||
"egrnData": {
|
"egrnData": {
|
||||||
"area": 38.2,
|
"area": {"status": "success", "value": 38.2},
|
||||||
"floor": 5,
|
"floor": {"status": "success", "value": 5},
|
||||||
"owners_count": 1,
|
"owners_count": {"status": "success", "value": 1},
|
||||||
"collateral": true,
|
"collateral": true,
|
||||||
"collateral_sber": false
|
"collateral_sber": false
|
||||||
},
|
},
|
||||||
|
|
@ -264,7 +264,7 @@ def test_parse_detail_html_raw_extra() -> None:
|
||||||
# wall/floor type живут ТОЛЬКО в raw_extra, НЕ в listings.house_type.
|
# wall/floor type живут ТОЛЬКО в raw_extra, НЕ в listings.house_type.
|
||||||
assert "house_type" not in raw
|
assert "house_type" not in raw
|
||||||
assert raw["domclick_building_guid"] == "abc-guid-123"
|
assert raw["domclick_building_guid"] == "abc-guid-123"
|
||||||
assert raw["egrn_area"] == 38.2
|
assert raw["egrn_area"] == {"status": "success", "value": 38.2}
|
||||||
assert raw["demand"]["calls"] == 5
|
assert raw["demand"]["calls"] == 5
|
||||||
assert raw["demand"]["favorites"] == 12
|
assert raw["demand"]["favorites"] == 12
|
||||||
# AVM (Layer C, top-level pricePrediction) → raw_extra.avm
|
# AVM (Layer C, top-level pricePrediction) → raw_extra.avm
|
||||||
|
|
|
||||||
|
|
@ -147,6 +147,13 @@ tests/test_3463_db_timeouts.py::test_lock_wait_over_ceiling_is_aborted
|
||||||
tests/test_3463_db_timeouts.py::test_set_local_statement_timeout_overrides_session_ceiling
|
tests/test_3463_db_timeouts.py::test_set_local_statement_timeout_overrides_session_ceiling
|
||||||
tests/test_3463_db_timeouts.py::test_set_local_is_scoped_to_its_transaction
|
tests/test_3463_db_timeouts.py::test_set_local_is_scoped_to_its_transaction
|
||||||
|
|
||||||
|
# Жилая площадь и балконы переживают переобход выдачи (#3252) — тот же `_live_session()`.
|
||||||
|
# Проверяют ПОВЕДЕНИЕ апсерта на живой схеме: SERP без полей не стирает то, что
|
||||||
|
# добыла карточка (COALESCE), и настоящее новое значение всё ещё перезаписывает.
|
||||||
|
# В ci-tradein.yml бегут по-настоящему (postgres-сервис); без БД — skip.
|
||||||
|
tests/test_3252_domclick_card_fields.py::test_serp_rescrape_keeps_detail_living_area_and_balconies
|
||||||
|
tests/test_3252_domclick_card_fields.py::test_real_new_value_still_overwrites
|
||||||
|
|
||||||
# Миграция 310 (#3385): настоящий SQL-файл с DELETE и RAISE-остановкой исполняет только
|
# Миграция 310 (#3385): настоящий SQL-файл с DELETE и RAISE-остановкой исполняет только
|
||||||
# Postgres. В ci-tradein.yml бегут по-настоящему (postgres-сервис, #2745); краснеют от
|
# Postgres. В ci-tradein.yml бегут по-настоящему (postgres-сервис, #2745); краснеют от
|
||||||
# снятия подписи батча, сужения окна до ×10 и снятия порога — проверено вручную 17.09.
|
# снятия подписи батча, сужения окна до ×10 и снятия порога — проверено вручную 17.09.
|
||||||
|
|
|
||||||
192
tradein-mvp/backend/tests/test_3252_domclick_card_fields.py
Normal file
192
tradein-mvp/backend/tests/test_3252_domclick_card_fields.py
Normal file
|
|
@ -0,0 +1,192 @@
|
||||||
|
"""#3252: три тихие дыры в полях карточки Домклика.
|
||||||
|
|
||||||
|
1. owners_count. egrnData отдаёт поля ЕГРН обёрткой ``{"status", "value"}``, а парсер
|
||||||
|
делал ``int()`` от словаря и молча получал None: на проде 0 из ~2970 карточек,
|
||||||
|
обогащённых с 29.08 (при 5075 из 6296 у ручного прогона 18.07, который обёртку
|
||||||
|
разворачивал).
|
||||||
|
2. Нулевые площади. ``livingArea: 0`` / ``kitchenArea: 0`` у Домклика значит «не
|
||||||
|
указано», а в колонку ложился 0.00 (303 кухни и 84 жилые площади).
|
||||||
|
3. Жилая площадь и балконы стирались переобходом выдачи: апсерт в scraper_kit.base
|
||||||
|
писал их сырым ``EXCLUDED``, а SERP Домклика и Авито этих полей не отдаёт. У
|
||||||
|
domklik, переобойдённых после обогащения, living_area_m2 = 0 из 5217.
|
||||||
|
|
||||||
|
Состояние карточек ниже — урезанный ``__SSR_STATE__`` живых карточек 2074362051 и
|
||||||
|
2078257603, снятый 17.09 (значения как есть, лишние ветки выкинуты).
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import uuid
|
||||||
|
from decimal import Decimal
|
||||||
|
from typing import Any
|
||||||
|
from unittest.mock import MagicMock
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
from scraper_kit.base import ScrapedLot, save_listings
|
||||||
|
from scraper_kit.providers.domclick.detail import parse_detail_html
|
||||||
|
from sqlalchemy import text
|
||||||
|
|
||||||
|
_LIVE_2074362051 = {
|
||||||
|
"productCard": {
|
||||||
|
"objectInfo": {
|
||||||
|
"area": 59.8,
|
||||||
|
"balconies": 1,
|
||||||
|
"kitchenArea": 14.9,
|
||||||
|
"livingArea": 25.8,
|
||||||
|
"renovation": "Косметический",
|
||||||
|
},
|
||||||
|
"egrnData": {
|
||||||
|
"area": {"status": "success", "value": 59.8},
|
||||||
|
"collateral": False,
|
||||||
|
"collateral_sber": False,
|
||||||
|
"floor": {"status": "success", "value": 14},
|
||||||
|
"owners_count": {"status": "error", "value": 6},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
_LIVE_2078257603 = {
|
||||||
|
"productCard": {
|
||||||
|
"objectInfo": {
|
||||||
|
"area": 67.4,
|
||||||
|
"balconies": 0,
|
||||||
|
"kitchenArea": 0,
|
||||||
|
"livingArea": 0,
|
||||||
|
"renovation": "",
|
||||||
|
},
|
||||||
|
"egrnData": {
|
||||||
|
"area": {"status": "error", "value": 65.3},
|
||||||
|
"collateral": False,
|
||||||
|
"collateral_sber": False,
|
||||||
|
"floor": {"status": "success", "value": 21},
|
||||||
|
"owners_count": {"status": "success", "value": 1},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _parse(state: dict, card_id: str):
|
||||||
|
html = f"<script>window.__SSR_STATE__ = {json.dumps(state, ensure_ascii=False)};</script>"
|
||||||
|
return parse_detail_html(html, f"https://ekaterinburg.domclick.ru/card/sale__flat__{card_id}")
|
||||||
|
|
||||||
|
|
||||||
|
# ── 1-2. Разбор живой формы ──────────────────────────────────────────────────
|
||||||
|
|
||||||
|
|
||||||
|
def test_owners_count_is_read_from_egrn_wrapper_whatever_the_status() -> None:
|
||||||
|
# status=error у owners_count = «много собственников», value — сам факт ЕГРН.
|
||||||
|
assert _parse(_LIVE_2074362051, "2074362051").owners_count == 6
|
||||||
|
assert _parse(_LIVE_2078257603, "2078257603").owners_count == 1
|
||||||
|
|
||||||
|
|
||||||
|
def test_egrn_area_keeps_wrapper_in_raw_payload() -> None:
|
||||||
|
e = _parse(_LIVE_2078257603, "2078257603")
|
||||||
|
assert e.raw_extra["egrn_area"] == {"status": "error", "value": 65.3}
|
||||||
|
|
||||||
|
|
||||||
|
def test_zero_areas_mean_unknown_not_zero() -> None:
|
||||||
|
zero = _parse(_LIVE_2078257603, "2078257603")
|
||||||
|
assert (zero.living_area_m2, zero.kitchen_area_m2) == (None, None)
|
||||||
|
real = _parse(_LIVE_2074362051, "2074362051")
|
||||||
|
assert (real.living_area_m2, real.kitchen_area_m2) == (25.8, 14.9)
|
||||||
|
|
||||||
|
|
||||||
|
# ── 3. Переобход выдачи не стирает то, что добыла карточка (живой Postgres) ──
|
||||||
|
|
||||||
|
|
||||||
|
def _live_session() -> Any | None:
|
||||||
|
try:
|
||||||
|
from sqlalchemy import create_engine
|
||||||
|
from sqlalchemy.orm import sessionmaker
|
||||||
|
|
||||||
|
dsn = os.environ.get("TEST_DATABASE_URL") or os.environ.get("DATABASE_URL", "")
|
||||||
|
if not dsn or "localhost:5432/test" in dsn:
|
||||||
|
return None
|
||||||
|
engine = create_engine(dsn, future=True)
|
||||||
|
with engine.connect() as conn:
|
||||||
|
conn.execute(text("SELECT 1"))
|
||||||
|
return sessionmaker(bind=engine, future=True)()
|
||||||
|
except Exception:
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def _matcher() -> MagicMock:
|
||||||
|
m = MagicMock()
|
||||||
|
m.match_or_create_house.return_value = (None, 0.0, "no_address")
|
||||||
|
m.upsert_listing_source.return_value = None
|
||||||
|
return m
|
||||||
|
|
||||||
|
|
||||||
|
def _serp_lot(sid: str, **detail: Any) -> ScrapedLot:
|
||||||
|
return ScrapedLot(
|
||||||
|
source="domklik",
|
||||||
|
source_url=f"https://ekaterinburg.domclick.ru/card/sale__flat__t3252{sid}",
|
||||||
|
source_id=f"t3252-{sid}",
|
||||||
|
price_rub=7_000_000,
|
||||||
|
**detail,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _row(db: Any, sid: str) -> Any:
|
||||||
|
return db.execute(
|
||||||
|
text(
|
||||||
|
"SELECT living_area_m2, balconies_count FROM listings "
|
||||||
|
"WHERE source='domklik' AND source_id = :sid"
|
||||||
|
),
|
||||||
|
{"sid": f"t3252-{sid}"},
|
||||||
|
).fetchone()
|
||||||
|
|
||||||
|
|
||||||
|
def _cleanup(db: Any) -> None:
|
||||||
|
try:
|
||||||
|
db.rollback()
|
||||||
|
ids = "(SELECT id FROM listings WHERE source='domklik' AND source_id LIKE 't3252-%')"
|
||||||
|
db.execute(text(f"DELETE FROM listings_snapshots WHERE listing_id IN {ids}"))
|
||||||
|
db.execute(text("DELETE FROM listing_sources WHERE ext_id LIKE 't3252-%'"))
|
||||||
|
db.execute(text("DELETE FROM listings WHERE source='domklik' AND source_id LIKE 't3252-%'"))
|
||||||
|
db.commit()
|
||||||
|
finally:
|
||||||
|
db.close()
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.skipif(_live_session() is None, reason="no reachable Postgres test DB")
|
||||||
|
def test_serp_rescrape_keeps_detail_living_area_and_balconies() -> None:
|
||||||
|
db = _live_session()
|
||||||
|
sid = uuid.uuid4().hex[:8]
|
||||||
|
try:
|
||||||
|
save_listings(
|
||||||
|
db,
|
||||||
|
[_serp_lot(sid, living_area_m2=25.8, balconies_count=1)],
|
||||||
|
matcher=_matcher(),
|
||||||
|
region_code=66,
|
||||||
|
)
|
||||||
|
save_listings(db, [_serp_lot(sid)], matcher=_matcher(), region_code=66) # выдача: полей нет
|
||||||
|
row = _row(db, sid)
|
||||||
|
assert (row.living_area_m2, row.balconies_count) == (Decimal("25.80"), 1)
|
||||||
|
finally:
|
||||||
|
_cleanup(db)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.skipif(_live_session() is None, reason="no reachable Postgres test DB")
|
||||||
|
def test_real_new_value_still_overwrites() -> None:
|
||||||
|
db = _live_session()
|
||||||
|
sid = uuid.uuid4().hex[:8]
|
||||||
|
try:
|
||||||
|
save_listings(
|
||||||
|
db,
|
||||||
|
[_serp_lot(sid, living_area_m2=25.8, balconies_count=1)],
|
||||||
|
matcher=_matcher(),
|
||||||
|
region_code=66,
|
||||||
|
)
|
||||||
|
save_listings(
|
||||||
|
db,
|
||||||
|
[_serp_lot(sid, living_area_m2=30.5, balconies_count=2)],
|
||||||
|
matcher=_matcher(),
|
||||||
|
region_code=66,
|
||||||
|
)
|
||||||
|
row = _row(db, sid)
|
||||||
|
assert (row.living_area_m2, row.balconies_count) == (Decimal("30.50"), 2)
|
||||||
|
finally:
|
||||||
|
_cleanup(db)
|
||||||
|
|
@ -587,9 +587,18 @@ def save_listings(
|
||||||
price_rub = EXCLUDED.price_rub,
|
price_rub = EXCLUDED.price_rub,
|
||||||
price_per_m2 = EXCLUDED.price_per_m2,
|
price_per_m2 = EXCLUDED.price_per_m2,
|
||||||
-- Cian-specific: обновляем при каждом re-scrape
|
-- Cian-specific: обновляем при каждом re-scrape
|
||||||
living_area_m2 = EXCLUDED.living_area_m2,
|
-- #3252: жилая площадь и балконы — COALESCE по той же причине, что
|
||||||
|
-- #3063 ниже: их добывает detail (avito, domklik), а SERP этих
|
||||||
|
-- источников отдаёт NULL. Прод 17.09: у domklik, переобойдённых
|
||||||
|
-- после обогащения, living_area_m2 = 0 из 5217 (без переобхода —
|
||||||
|
-- 2990 из 4058), у avito — 1 из 8639 (4122 из 6505).
|
||||||
|
living_area_m2 = COALESCE(
|
||||||
|
EXCLUDED.living_area_m2, listings.living_area_m2
|
||||||
|
),
|
||||||
bedrooms_count = EXCLUDED.bedrooms_count,
|
bedrooms_count = EXCLUDED.bedrooms_count,
|
||||||
balconies_count = EXCLUDED.balconies_count,
|
balconies_count = COALESCE(
|
||||||
|
EXCLUDED.balconies_count, listings.balconies_count
|
||||||
|
),
|
||||||
loggias_count = EXCLUDED.loggias_count,
|
loggias_count = EXCLUDED.loggias_count,
|
||||||
description_minhash = EXCLUDED.description_minhash,
|
description_minhash = EXCLUDED.description_minhash,
|
||||||
cadastral_number = EXCLUDED.cadastral_number,
|
cadastral_number = EXCLUDED.cadastral_number,
|
||||||
|
|
@ -742,8 +751,11 @@ def save_listings(
|
||||||
listings.price_previous_rub, listings.newbuilding_id,
|
listings.price_previous_rub, listings.newbuilding_id,
|
||||||
listings.newbuilding_url, listings.card_hash, listings.is_active
|
listings.newbuilding_url, listings.card_hash, listings.is_active
|
||||||
) IS DISTINCT FROM (
|
) IS DISTINCT FROM (
|
||||||
EXCLUDED.price_rub, EXCLUDED.price_per_m2, EXCLUDED.living_area_m2,
|
EXCLUDED.price_rub, EXCLUDED.price_per_m2,
|
||||||
EXCLUDED.bedrooms_count, EXCLUDED.balconies_count, EXCLUDED.loggias_count,
|
COALESCE(EXCLUDED.living_area_m2, listings.living_area_m2),
|
||||||
|
EXCLUDED.bedrooms_count,
|
||||||
|
COALESCE(EXCLUDED.balconies_count, listings.balconies_count),
|
||||||
|
EXCLUDED.loggias_count,
|
||||||
EXCLUDED.description_minhash, EXCLUDED.cadastral_number,
|
EXCLUDED.description_minhash, EXCLUDED.cadastral_number,
|
||||||
EXCLUDED.building_cadastral_number,
|
EXCLUDED.building_cadastral_number,
|
||||||
-- #3063: те же COALESCE, что в SET выше. Правая часть гейта ОБЯЗАНА
|
-- #3063: те же COALESCE, что в SET выше. Правая часть гейта ОБЯЗАНА
|
||||||
|
|
@ -823,9 +835,9 @@ def save_listings(
|
||||||
is_active = true,
|
is_active = true,
|
||||||
price_rub = :price_rub,
|
price_rub = :price_rub,
|
||||||
price_per_m2 = :ppm2,
|
price_per_m2 = :ppm2,
|
||||||
living_area_m2 = :living_area_m2,
|
living_area_m2 = COALESCE(:living_area_m2, living_area_m2),
|
||||||
bedrooms_count = :bedrooms_count,
|
bedrooms_count = :bedrooms_count,
|
||||||
balconies_count = :balconies_count,
|
balconies_count = COALESCE(:balconies_count, balconies_count),
|
||||||
loggias_count = :loggias_count,
|
loggias_count = :loggias_count,
|
||||||
description_minhash = :description_minhash,
|
description_minhash = :description_minhash,
|
||||||
cadastral_number = :cadastral_number,
|
cadastral_number = :cadastral_number,
|
||||||
|
|
@ -940,7 +952,7 @@ def save_listings(
|
||||||
skip_snapshot = today_row is not None
|
skip_snapshot = today_row is not None
|
||||||
if skip_snapshot:
|
if skip_snapshot:
|
||||||
logger.debug(
|
logger.debug(
|
||||||
"save_listings:snapshot_skipped (card unchanged) " "source=%s listing_id=%s",
|
"save_listings:snapshot_skipped (card unchanged) source=%s listing_id=%s",
|
||||||
lot.source,
|
lot.source,
|
||||||
listing_id,
|
listing_id,
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -27,7 +27,8 @@ _extract_ssr_state делает balanced-brace scan (с пропуском ск
|
||||||
Форма SSR-стейта подтверждена на живой карточке 2075729321 (2026-06-27):
|
Форма SSR-стейта подтверждена на живой карточке 2075729321 (2026-06-27):
|
||||||
productCard.objectInfo.{renovation,livingArea,kitchenArea}, productCard.priceInfo.
|
productCard.objectInfo.{renovation,livingArea,kitchenArea}, productCard.priceInfo.
|
||||||
priceHistory ({date ISO8601+tz, price, diff, state}), productCard.egrnData
|
priceHistory ({date ISO8601+tz, price, diff, state}), productCard.egrnData
|
||||||
(snake_case owners_count/collateral/collateral_sber), productCard.legalOptions.
|
(snake_case; area/floor/owners_count — обёртка {status, value}, collateral/
|
||||||
|
collateral_sber — bool; перепроверено 17.09, #3252), productCard.legalOptions.
|
||||||
saleType, productCard.viewsCount/callsCount, houseInfo.info.{wallType,floorType},
|
saleType, productCard.viewsCount/callsCount, houseInfo.info.{wallType,floorType},
|
||||||
и ТОП-УРОВНЕМ pricePrediction (DomClick AVM → raw_extra.avm).
|
и ТОП-УРОВНЕМ pricePrediction (DomClick AVM → raw_extra.avm).
|
||||||
|
|
||||||
|
|
@ -283,6 +284,29 @@ def _to_int(value: Any) -> int | None:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def _pos_float(value: Any) -> float | None:
|
||||||
|
"""float > 0, иначе None (#3252).
|
||||||
|
|
||||||
|
Домклик отдаёт ``livingArea: 0`` / ``kitchenArea: 0`` как «не указано» (живая
|
||||||
|
карточка 2078257603, 17.09). Без этого в колонку ложился 0.00, а COALESCE в
|
||||||
|
UPDATE затирал им ранее известную площадь: на проде 303 кухни и 84 жилые
|
||||||
|
площади = 0 — только у domklik и только у обогащённых после 26.08.
|
||||||
|
"""
|
||||||
|
f = _to_float(value)
|
||||||
|
return f if f is not None and f > 0 else None
|
||||||
|
|
||||||
|
|
||||||
|
def _egrn_value(value: Any) -> Any:
|
||||||
|
"""Узел egrnData → значение (#3252).
|
||||||
|
|
||||||
|
Поля ЕГРН приходят обёрткой ``{"status": "success"|"warning"|"error", "value": X}``
|
||||||
|
(живые карточки 17.09: area, floor, owners_count). ``status`` — сверка с тем, что
|
||||||
|
указал продавец, а ``value`` — сам факт из ЕГРН, поэтому берём value при любом
|
||||||
|
статусе. Скаляр пропускаем как есть.
|
||||||
|
"""
|
||||||
|
return value.get("value") if isinstance(value, dict) else value
|
||||||
|
|
||||||
|
|
||||||
def _parse_change_time(value: Any) -> datetime | None:
|
def _parse_change_time(value: Any) -> datetime | None:
|
||||||
"""Нормализует дату изменения цены → timezone-aware datetime.
|
"""Нормализует дату изменения цены → timezone-aware datetime.
|
||||||
|
|
||||||
|
|
@ -436,8 +460,8 @@ def parse_detail_html(html: str, source_url: str) -> DomClickDetailEnrichment:
|
||||||
repair_state = _map_repair_state(repair_type)
|
repair_state = _map_repair_state(repair_type)
|
||||||
|
|
||||||
# площади
|
# площади
|
||||||
living_area_m2 = _to_float(oi.get("livingArea"))
|
living_area_m2 = _pos_float(oi.get("livingArea"))
|
||||||
kitchen_area_m2 = _to_float(oi.get("kitchenArea"))
|
kitchen_area_m2 = _pos_float(oi.get("kitchenArea"))
|
||||||
|
|
||||||
# балконы: int → count+флаг; иное (строка/др.) → raw_extra, counts None
|
# балконы: int → count+флаг; иное (строка/др.) → raw_extra, counts None
|
||||||
balconies = oi.get("balconies")
|
balconies = oi.get("balconies")
|
||||||
|
|
@ -454,10 +478,11 @@ def parse_detail_html(html: str, source_url: str) -> DomClickDetailEnrichment:
|
||||||
|
|
||||||
sale_type = (pc.get("legalOptions") or {}).get("saleType")
|
sale_type = (pc.get("legalOptions") or {}).get("saleType")
|
||||||
|
|
||||||
# owners_count — защищаемся от двух написаний ключа
|
# owners_count — обёртка {status, value} (см. _egrn_value); до #3252 int()
|
||||||
owners_count = _to_int(egrn.get("owners_count"))
|
# от словаря молча давал None: 0 из ~2970 карточек, обогащённых с 29.08.
|
||||||
|
owners_count = _to_int(_egrn_value(egrn.get("owners_count")))
|
||||||
if owners_count is None:
|
if owners_count is None:
|
||||||
owners_count = _to_int(egrn.get("ownersCount"))
|
owners_count = _to_int(_egrn_value(egrn.get("ownersCount")))
|
||||||
|
|
||||||
# encumbrances_clean (ИНВЕРСИЯ): collateral/collateral_sber обозначают
|
# encumbrances_clean (ИНВЕРСИЯ): collateral/collateral_sber обозначают
|
||||||
# НАЛИЧИЕ обременения. Любой truthy → clean=False; оба отсутствуют/false
|
# НАЛИЧИЕ обременения. Любой truthy → clean=False; оба отсутствуют/false
|
||||||
|
|
@ -523,8 +548,10 @@ def parse_detail_html(html: str, source_url: str) -> DomClickDetailEnrichment:
|
||||||
|
|
||||||
# raw_extra → merge в listings.raw_payload. wall/floor type ОСТАЮТСЯ тут,
|
# raw_extra → merge в listings.raw_payload. wall/floor type ОСТАЮТСЯ тут,
|
||||||
# НЕ пишем в listings.house_type (кросс-источниковый словарь грязный).
|
# НЕ пишем в listings.house_type (кросс-источниковый словарь грязный).
|
||||||
# egrn area key — точное написание не подтверждено; пробуем несколько.
|
# egrnData.area — ключ подтверждён на живых карточках 17.09 (#3252). В raw_payload
|
||||||
egrn_area = egrn.get("area") or egrn.get("rosreestrArea") or egrn.get("object_area")
|
# кладём обёртку {status, value} целиком: status говорит, сошлась ли площадь
|
||||||
|
# ЕГРН с площадью объявления, и уже лежит в таком виде у 2156 строк.
|
||||||
|
egrn_area = egrn.get("area")
|
||||||
demand = _compact(
|
demand = _compact(
|
||||||
{
|
{
|
||||||
"calls": pc.get("callsCount"),
|
"calls": pc.get("callsCount"),
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue