fix(scraper-kit): бедный re-scrape больше не стирает признаки продавца #3067

Merged
lekss361 merged 3 commits from fix/yandex-seller-fields-erosion into main 2026-08-23 21:52:38 +00:00

3 commits

Author SHA1 Message Date
bot-backend
85fc18dcb3 fix(tests): phones в фикстуре — list[dict], а не список строк
All checks were successful
CI Trade-In / changes (pull_request) Successful in 7s
CI / changes (pull_request) Successful in 9s
CI Trade-In / browser-tests (pull_request) Has been skipped
CI Trade-In / frontend-checks (pull_request) Has been skipped
CI / backend-tests (pull_request) Has been skipped
CI / frontend-tests (pull_request) Has been skipped
CI / openapi-codegen-check (pull_request) Has been skipped
CI Trade-In / backend-tests (pull_request) Successful in 4m36s
CI (там живой postgres, и эти два теста реально исполняются) поймал
ValidationError: ScrapedLot.phones — jsonb вида
[{countryCode, number, type}, ...] (019_listings_alter_cian.sql:41), а я
передавал ["+79000000000"].

Заодно проверено конструирование лота напрямую, без БД — это ловит любые
pydantic-ошибки, не дожидаясь прогона CI. Всплыла деталь, подтверждающая
разбор в самом фиксе: у бедного лота phones не None, а ПУСТОЙ СПИСОК.
Сборка параметров (`_to_json(lot.phones) if lot.phones else None`)
схлопывает его в None, то есть в SQL приходит NULL и COALESCE удерживает
прежнее значение. Если бы пустой список доезжал как '[]', фикс для phones
и metro_stations молча не работал бы.

Refs #3063
2026-08-24 00:46:02 +03:00
bot-backend
0cce97bd75 fix(tests): zip(strict=True) в гарде паритета — ruff B905
Some checks failed
CI / changes (pull_request) Successful in 9s
CI Trade-In / browser-tests (pull_request) Has been skipped
CI Trade-In / frontend-checks (pull_request) Has been skipped
CI / frontend-tests (pull_request) Has been skipped
CI / openapi-codegen-check (pull_request) Has been skipped
CI Trade-In / changes (pull_request) Successful in 8s
CI / backend-tests (pull_request) Has been skipped
CI Trade-In / backend-tests (pull_request) Failing after 4m38s
CI поймал то, что локально не проверилось: ruff в этом окружении вызывался
не тем интерпретатором и молча не запускался. strict=True здесь не просто
успокаивает линтер — равенство длин кортежей уже проверено assert'ом выше,
так что строгий zip выражает ровно то же намерение явно.

Refs #3063
2026-08-24 00:38:41 +03:00
bot-backend
d3d75801ca fix(scraper-kit): бедный re-scrape больше не стирает признаки продавца
Some checks failed
CI Trade-In / changes (pull_request) Successful in 8s
CI / changes (pull_request) Successful in 9s
CI Trade-In / browser-tests (pull_request) Has been skipped
CI Trade-In / frontend-checks (pull_request) Has been skipped
CI / openapi-codegen-check (pull_request) Has been skipped
CI / backend-tests (pull_request) Has been skipped
CI / frontend-tests (pull_request) Has been skipped
CI Trade-In / backend-tests (pull_request) Failing after 1m0s
Апсерт listings присваивал шесть полей напрямую (= EXCLUDED.X): phones,
is_homeowner, is_pro_seller, bargain_allowed, sale_type, metro_stations.

Комментарий рядом («Cian-specific: обновляем при каждом re-scrape») был
верен, пока в listings писал один cian, который отдаёт эти поля на каждом
проходе. Сейчас в ту же таблицу пишут yandex/avito/domklik, чей SERP их не
отдаёт вовсе — и NULL молча затирал значение, добытое detail-обогащением.
Ни ошибки, ни лога, ни изменения статуса прогона: колонка просто
откатывалась к NULL на обычном переобходе.

Прод 23-24.08, yandex: is_homeowner NULL у 98.6% (16 232 из 16 460)
активных листингов, is_pro_seller — у 76.6%.

Соседние поля в том же операторе уже защищены COALESCE ровно от этого, с
явными объяснениями #2007 / #2594 / #2777 — address, city, kitchen_area_m2,
ceiling_height_m, mortgage_available, is_apartments, is_rosreestr_checked.
Эти шесть в защиту просто не попали.

Правка в ДВУХ местах, не в одном. Кроме SET изменена правая часть гейта
`IS DISTINCT FROM` (#2992): он решает, переписывать ли строку, сравнивая
текущие значения с ИТОГОВЫМИ (post-COALESCE). Оставить его на сыром
EXCLUDED значило бы, что гейт видит «NULL против значения» и считает
строку изменившейся там, где она не меняется — вернулись бы ровно те
лишние UPDATE и TOAST-чанки, ради которых #2992 делался.

Пустые phones/metro_stations сериализуются как None (`_to_json(...) if
lot.X else None`), то есть приходят SQL NULL — COALESCE их удерживает, а
не подменяет пустым массивом.

Тест сторожит и паритет тоже: сравнивает оба кортежа гейта поэлементно,
поэтому поймает будущее расхождение по ЛЮБОЙ колонке, не только по этим
шести.

Проверено:
  - фальсификация: все 7 тестов падают на коде до правки
  - 770 passed, 10 skipped (-k "upsert or listing or scrape or writer or
    snapshot or timestamps")
  - выравнивание кортежей гейта: 39 = 39 элементов, порядок колонок совпал

Из #3063 сделан только пункт 1. Пункт 2 (выводить is_pro_seller из
agency_name) НЕ вошёл — см. комментарий в issue: у него есть риск ложного
срабатывания, которого нет у этой правки.

Refs #3063
2026-08-24 00:32:02 +03:00