fix(scraper-kit): бедный re-scrape больше не стирает признаки продавца #3067
No reviewers
Labels
No labels
Fable 5 ревью
GG-форсайт
admin
analytics
auth
automation
bug
business
chore
ci
compliance
data
data-moat
docs
duplicate
dx
enhancement
feedback/max
generative
needs-discussion
needs-human
observability
pause-bots
performance
priority/p0
priority/p1
priority/p2
priority/p3
scope/backend
scope/db
scope/devops
scope/frontend
scope/qa
scrapers
security
site-finder
stage/1
stage/2
status/blocked
status/done
status/needs-analysis
status/needs-fix
status/qa
status/ready
status/review
status/wip
tech-debt
tradein
ux
week ревью 1
wontfix
ИРД
вторичка
No milestone
No project
No assignees
2 participants
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference: lekss361/gendesign#3067
Loading…
Add table
Reference in a new issue
No description provided.
Delete branch "fix/yandex-seller-fields-erosion"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Закрывает пункт 1 из #3063 — баг активен прямо сейчас, данные теряются с каждым проходом.
Что происходило
Апсерт
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, из 16 460 активных листингов:
is_homeowneris_pro_sellerСоседние поля в том же операторе уже защищены
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 и делался.Проверено выравнивание кортежей гейта: 39 = 39 элементов, порядок колонок совпал.
Тонкость с пустыми значениями
phonesиmetro_stations— jsonb. Если бы пустой список сериализовался как[], COALESCE ничего бы не спас (не-NULL перезаписал бы значение пустым массивом). Проверено: в сборке параметров стоит_to_json(lot.X) if lot.X else None, то есть пусто → SQL NULL → COALESCE удерживает. Фикс для них реально работает.Test plan
770 passed, 10 skipped(-k "upsert or listing or scrape or writer or snapshot or timestamps")Второй из них существенен: без него фикс мог бы «защитить» данные ценой того, что поля стали бы write-once. Настоящая смена (частник → агентство) приходит не NULL'ом и обязана записаться.
Чего здесь НЕТ
Пункт 2 из #3063 — выводить
is_pro_sellerизagency_name— сознательно не вошёл. У него есть риск, которого нет у этой правки: если хоть один провайдер кладёт вagency_nameимя частного продавца, вывод молча пометит 4 469 листингов как агентские, и это поедет в оценщик. Проверять это нужно чтением каждого провайдера отдельно. Здешняя же правка может только перестать разрушать данные — обратного эффекта у неё нет.Подробности комментарием в #3063.
Refs #3063
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