refactor(tradein): avito search — remove jitter (C-5) + mobile API dead code + parse houseLinks #441
No reviewers
Labels
No labels
admin
analytics
auth
automation
bug
business
chore
ci
compliance
data
data-moat
docs
duplicate
dx
enhancement
Fable 5 ревью
feedback/max
generative
GG-форсайт
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
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference: lekss361/gendesign#441
Loading…
Add table
Reference in a new issue
No description provided.
Delete branch "feat/tradein-avito-search-refactor"
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?
Summary
Stage 2a of Avito Scraper v2. Closes C-5 silent corruption (anchor+jitter в search-карточках → точечная маркировка), удаляет ~200 LOC мёртвого кода (mobile API Strategy A,
__initialState__extraction), добавляет parse/catalog/houses/...для связи listing → house (Stage 2c/3).LOC delta:
-232 / +57(net −175 lines).Changes
tradein-mvp/backend/app/services/scrapers/avito.py_fetch_via_mobile_api+_mobile_item_to_lot(~95 LOC, H-6 dead — mobile API endpoint 404 since long)_RE_AVITO_STATE+_extract_avito_state+_items_from_state(state empty since 2025)_state_item_to_lot(unused after state path removal)_dom_card_to_lot:lat=lon=Noneвместо jitter. Точные coords придут изavito_detail.py(Stage 2b) илиgeocode-missingcron подтянет изaddress<a href="/catalog/houses/...">→house_source/house_ext_id/house_urllisting_segmentdetection (vtorichka/novostroyki) из item URL patternimport json,import random(больше не используются)tradein-mvp/backend/app/services/scrapers/base.pyScrapedLot(Pydantic):house_source,house_ext_id,house_url,listing_segmentsave_listingsINSERT: 4 новых колонки + params (ON CONFLICT не меняем — house linking устаканивается на первом INSERT)Smoke
ruff check app/services/scrapers/avito.pycleanfrom app.services.scrapers.avito import AvitoScraperOK_USER_AGENTSстрокахbase.py— НЕ тронуты (out of scope)Behavioral changes
lat=lon=NULLhouse_ext_id(по плану)__initialState__JSON parsing (мёртвый код)После deploy
geocode-missingcron подтянет coords изaddress(или deferred до Stage 2b detail enrichment даст точные).Test plan
lat=lon=NULLдля всех новых listings (psql:SELECT count(*) FROM listings WHERE source='avito' AND scraped_at > NOW() - INTERVAL '1 hour' AND lat IS NULL)house_ext_idfilled для большинства (psql:SELECT count(*) FILTER (WHERE house_ext_id IS NOT NULL)::float / count(*) FROM listings WHERE source='avito' AND scraped_at > NOW() - INTERVAL '1 hour')Next
После merge → spawn 3 parallel workers (или sequentially per user preference) для Stage 2b (
avito_detail.py), 2c (avito_houses.py), 2d (avito_imv.py).Deep Code Review — verdict APPROVE
Status: APPROVE (merge)
Files reviewed: 2 (P1:
tradein-mvp/backend/app/services/scrapers/avito.py, P1:.../base.py)Lines: +57 / -232 (net −175)
Critical (BLOCK) — none
High — none
Medium (follow-up, не блокирующее)
save_listingsON CONFLICT не обновляет house linking (base.py:224-229)Existing listings (insert'нутые до этого PR) с
house_ext_id=NULLтак и останутся NULL при re-scrape — Avito показывает же объявление, dedup_hash совпадает, обновляем только price.Fix follow-up PR: добавить
house_source = COALESCE(listings.house_source, EXCLUDED.house_source)и аналогично дляhouse_ext_id/house_url/listing_segment. Это backfill через первый same-listing rescrape.House regex pattern strict enough? (
avito.py:178)parts[-1].isdigit()— корректно отсеивает/catalog/houses/about/, но любой numeric последний segment пройдёт. Avito может ссылаться на/catalog/houses/info/2026(год) если такая страница появится. Низкий риск, но мониторьSELECT count(*) FROM listings WHERE house_ext_id ~ '^(19|20)\d{2}$'после deploy — если >0%, regex нужно ужесточить (например min 5 digits).Pre-existing 4 E501 в
base.py_USER_AGENTS— out of scope, ок, но в follow-up cleanup PR.Low / nits — none
Cross-file impact analysis
ScrapedLotPydantic extension: 4 новых поля всеOptional[str] = None(base.py:81-84) → backward compat OK. Verified all callers used named kwargs:avito.py:190— заполняет новые поляcian.py:232, 363— не использует, defaultsNoneкорректноyandex_realty.py:211— не использует, defaultsNonen1.py:182— не использует, defaultsNonesave_listingsSQL: psycopg v3 named params (:house_ext_idetc), placeholders count matches column count, типы text — корректно соответствуют schema011_listings_alter.sql:22-26._fetch_via_mobile_api,_mobile_item_to_lot,_extract_avito_state,_items_from_state,_state_item_to_lot,_RE_AVITO_STATE): grep поtradein-mvp/backend/— 0 lingering references._items_from_stateсуществует отдельно вyandex_realty.py:261(другая функция, не conflict).import json/import random— больше не используются вavito.py, удалены корректно.Blast radius (NULL coords) — APPROVED
Critical discovery:
estimator.py:379УЖЕ исключает Avito из radius search (AND source <> 'avito') с комментарием:Значит:
WHERE source <> 'avito', oценка работала без Avito.ST_DWithin(NULL geom), всё равно не учитывается.AND source <> 'avito').geocode-missingcron verification — PASSapp/api/v1/admin.py:139POST /api/v1/admin/geocode-missing?limit=100&target=listings— exists, рабочий.deploy/cron-scrape.sh:86-96— после каждого scrape запускает 15 чанков по 100 = до 1500 адресов с budget 240s/chunk.admin.py:158extra_filteraddress NOT LIKE '%(Avito)%'— раньше СКИПАЛ jittered Avito строки с fallback address "Екатеринбург (Avito)". После PR address либо реальный (улица), либо NULL → cronWHERE COALESCE(address, '') != ''отфильтрует NULL, реальные адреса геокодируются. FilterNOT LIKE '%(Avito)%'теперь dead-code, но не вредит. OK.Tradein DB consistency
011_listings_alter.sql:22-26уже добавил все 4 column'а как nullable text +house_id_fk bigint REFERENCES houses(id)(FK). PR пишетhouse_ext_id(text), resolutiontext → FK house_id_fkчерез JOIN — Stage 2c. Pattern correct.listings_house_ext_id_idx ON listings (house_source, house_ext_id) WHERE house_ext_id IS NOT NULL(011:65-66) — оптимально для Stage 2c lookup.Performance findings
save_listingsper-row INSERT (как и до PR) — приемлемо, batch size небольшой (50 max per scrape).Positive observations
ScrapedLotextension с Optional defaults сохраняет backward compat.data-marker="item"ancestor scope корректен (не глобальный селектор).import json,import randomправильно убраны._state_item_to_lotdeleted вместе с парс-логикой — без orphan функции.Post-merge smoke checklist
tradein-mvp/backend/**✓ verified.forgejo/workflows/deploy-tradein.yml:36)Risk score
Merging.