fix(tradein/avito): парсер карточки читал только первый блок параметров — параметры дома терялись молча #3048

Merged
lekss361 merged 3 commits from fix/avito-detail-fields into main 2026-08-21 20:54:36 +00:00
Owner

Закрывает #3047. Подготовка к массовому обогащению: гонять 11 021 карточку, зная, что парсер недобирает поля, — значит гонять их дважды.

Правка сделана по двум живым эталонам, снятым с прода 2026-08-21 через браузерный sidecar, а не по догадкам. Обе карточки лежат в фикстурах и специально различаются: одна от частника, другая от агентства — иначе ветку is_homeowner нечем проверить.

Корневой дефект

Авито отдаёт «О квартире» и «О доме» как два отдельных <div data-marker='item-view/item-params'>, каждый со своим <ul>. Код брал tree.css_first(...) — только первый.

sale_type лежит в первом блоке, поэтому читался. А house_type, total_floors_house и лифты — во втором, и терялись молча на каждой карточке. Ветка, которая должна была это ловить, проверяла len(ul_els) >= 2 внутри первого div — не срабатывала никогда, потому что там всегда ровно один <ul>. Комментарий рядом («некоторые страницы совмещают всё в одном ul») описывал прежнюю вёрстку.

Теперь собираем <ul> из всех блоков с этим маркером — устойчиво и к старой форме, и к порядку блоков.

Побочно это объясняет houses_enriched: 0 в прогоне 17:33: #3040 пишет параметры дома из карточки в houses, а параметры всегда приходили пустыми. Отдельной правки там, судя по всему, не требуется — данные просто начнут появляться.

Остальные поля

metro_stations — было 28 записей из 54 855 против 21 112 у ЦИАН. Причина: тянулось регуляркой из текста описания, шаблон ловил суффиксы -ская/-инская, а «Уралмаш» и «Машиностроителей» мимо. Переведено на разметку: div#item-view-address, станция — по иконке «Пешком до метро», имя читается как «текст обёртки минус текст времени», что устойчиво к смене хэшированных классов. Описание осталось фолбэком.

is_homeowner — не парсился вовсе. Взят структурный признак [data-marker='seller-info/label']: «Частное лицо» → True, «Агентство» → False. Именно структурный, а не поиск слова в тексте — иначе получился бы второй _extract_metro.

days_on_market — на странице в явном виде нет, выводится из даты публикации. Попутно нашлось, что publish_date тоже никогда не парсился: дата лежит в соседнем [data-marker='item-view/item-date'], а код искал её внутри блока item-view/item-id. Починено, старый вложенный путь оставлен фолбэком, регресс-тесты на переход года проходят. Семантика max((today - publish_date).days, 0) — та же, что у Яндекса.

cadastral_numberподтверждено отсутствие, ничего не парсим. Ни текста «кадастр», ни значения на обеих карточках; единственный след — пустое domotekaReportTeaser.cadastralNumber в JSON-блоке, схема зарезервирована Авито и не заполняется. Колонка пуста у всех источников. Зафиксировано в докстринге, чтобы не искали снова.

Докстринг файла приведён в соответствие реальности. Прежний перечислял поля, которых код не извлекает — на это купился я сам, когда назвал «обещанными» три поля, которых парсер никогда не обещал.

Что это НЕ меняет

Ни одно из полей не влияет на цену. По estimator.py: sale_type, is_homeowner, metro_stations не упоминаются; cadastral_number там относится к другой колонке. estimator.py не тронут.

Единственное следствие для расчётов: days_on_market питает прогноз срока продажи est_days_on_market. У Авито он был практически всегда NULL (в коде на это есть явный комментарий, #2894), теперь появятся реальные значения — после деплоя стоит посмотреть, как это скажется на прогнозе срока для avito-объектов. По цене регрессий быть не может.

Честная оговорка

Продакшн-регрессию sale_type (июнь 59% → август 1,6%) две эталонные карточки не воспроизводят — обе парсятся верно и до, и после правки. Значит падение покрытия вызвано скорее блокировками на части фетчей, чем структурой этих страниц. Найденный дефект чтения при этом реален и исправлен, но приписывать ему всю регрессию было бы натяжкой.

Тесты

test_avito_detail_fields_3047.py — 11 тестов на обеих эталонных карточках. Проверка идёт по двум файлам намеренно: на одном селектор легко подогнать.

  • новый файл: 11 passed
  • все avito/detail-тесты: 443 passed, 2 skipped
  • ruff check чисто, pre-commit прошёл

Правка ссылок

Воркер сослался на #3086, которого не существует, и назвал по нему тест-файл. Заведён настоящий #3047 с замерами; все ссылки и имя файла приведены к нему отдельным коммитом.

Refs #3047, #3040, #3045

Закрывает #3047. Подготовка к массовому обогащению: гонять 11 021 карточку, зная, что парсер недобирает поля, — значит гонять их дважды. Правка сделана **по двум живым эталонам**, снятым с прода 2026-08-21 через браузерный sidecar, а не по догадкам. Обе карточки лежат в фикстурах и специально различаются: одна от частника, другая от агентства — иначе ветку `is_homeowner` нечем проверить. ## Корневой дефект Авито отдаёт «О квартире» и «О доме» как **два отдельных `<div data-marker='item-view/item-params'>`**, каждый со своим `<ul>`. Код брал `tree.css_first(...)` — только первый. `sale_type` лежит в первом блоке, поэтому читался. А `house_type`, `total_floors_house` и лифты — во втором, и **терялись молча на каждой карточке**. Ветка, которая должна была это ловить, проверяла `len(ul_els) >= 2` внутри первого div — не срабатывала никогда, потому что там всегда ровно один `<ul>`. Комментарий рядом («некоторые страницы совмещают всё в одном ul») описывал прежнюю вёрстку. Теперь собираем `<ul>` из **всех** блоков с этим маркером — устойчиво и к старой форме, и к порядку блоков. **Побочно это объясняет `houses_enriched: 0`** в прогоне 17:33: #3040 пишет параметры дома из карточки в `houses`, а параметры всегда приходили пустыми. Отдельной правки там, судя по всему, не требуется — данные просто начнут появляться. ## Остальные поля **`metro_stations`** — было 28 записей из 54 855 против 21 112 у ЦИАН. Причина: тянулось регуляркой из **текста описания**, шаблон ловил суффиксы `-ская`/`-инская`, а «Уралмаш» и «Машиностроителей» мимо. Переведено на разметку: `div#item-view-address`, станция — по иконке «Пешком до метро», имя читается как «текст обёртки минус текст времени», что устойчиво к смене хэшированных классов. Описание осталось фолбэком. **`is_homeowner`** — не парсился вовсе. Взят структурный признак `[data-marker='seller-info/label']`: «Частное лицо» → `True`, «Агентство» → `False`. Именно структурный, а не поиск слова в тексте — иначе получился бы второй `_extract_metro`. **`days_on_market`** — на странице в явном виде нет, выводится из даты публикации. Попутно нашлось, что **`publish_date` тоже никогда не парсился**: дата лежит в соседнем `[data-marker='item-view/item-date']`, а код искал её внутри блока `item-view/item-id`. Починено, старый вложенный путь оставлен фолбэком, регресс-тесты на переход года проходят. Семантика `max((today - publish_date).days, 0)` — та же, что у Яндекса. **`cadastral_number`** — **подтверждено отсутствие**, ничего не парсим. Ни текста «кадастр», ни значения на обеих карточках; единственный след — пустое `domotekaReportTeaser.cadastralNumber` в JSON-блоке, схема зарезервирована Авито и не заполняется. Колонка пуста у **всех** источников. Зафиксировано в докстринге, чтобы не искали снова. **Докстринг файла приведён в соответствие реальности.** Прежний перечислял поля, которых код не извлекает — на это купился я сам, когда назвал «обещанными» три поля, которых парсер никогда не обещал. ## Что это НЕ меняет Ни одно из полей **не влияет на цену**. По `estimator.py`: `sale_type`, `is_homeowner`, `metro_stations` не упоминаются; `cadastral_number` там относится к другой колонке. `estimator.py` не тронут. Единственное следствие для расчётов: `days_on_market` питает прогноз срока продажи `est_days_on_market`. У Авито он был практически всегда `NULL` (в коде на это есть явный комментарий, #2894), теперь появятся реальные значения — **после деплоя стоит посмотреть, как это скажется на прогнозе срока для avito-объектов**. По цене регрессий быть не может. ## Честная оговорка Продакшн-регрессию `sale_type` (июнь 59% → август 1,6%) две эталонные карточки **не воспроизводят** — обе парсятся верно и до, и после правки. Значит падение покрытия вызвано скорее блокировками на части фетчей, чем структурой этих страниц. Найденный дефект чтения при этом реален и исправлен, но приписывать ему всю регрессию было бы натяжкой. ## Тесты `test_avito_detail_fields_3047.py` — 11 тестов на обеих эталонных карточках. Проверка идёт по двум файлам намеренно: на одном селектор легко подогнать. - новый файл: 11 passed - все avito/detail-тесты: **443 passed**, 2 skipped - `ruff check` чисто, pre-commit прошёл ## Правка ссылок Воркер сослался на `#3086`, которого не существует, и назвал по нему тест-файл. Заведён настоящий #3047 с замерами; все ссылки и имя файла приведены к нему отдельным коммитом. Refs #3047, #3040, #3045
lekss361 added 2 commits 2026-08-21 20:41:08 +00:00
Разобрались по эталонной разметке (2 живые detail-карточки, 2026-08-21), что реально
парсится и что нет:

- sale_type: item-view/item-params — Avito отдаёт "О квартире" и "О доме" как ДВА
  ОТДЕЛЬНЫХ <div> с ОДНИМ И ТЕМ ЖЕ маркером, а не один div с двумя <ul>, как
  предполагал старый код. css_first брал только ПЕРВЫЙ div — sale_type это не
  задевало (он в первом блоке), но house_type/total_floors_house/лифты из второго
  блока терялись молча через мёртвую ветку `len(ul_els) >= 2`. Теперь читаем <ul>
  из ВСЕХ блоков с этим маркером — устойчиво к порядку блоков.
- metro_stations: раньше только эвристика по тексту описания (28 строк из 54 855).
  Основной источник теперь — структурная разметка (#item-view-address, иконка
  "Пешком до метро"), покрывает станции без ограничения на суффикс имени
  (METRO_RE ловил только "-ская"/"-инская"). Описание — фолбэк.
- is_homeowner: не парсился вовсе. [data-marker='seller-info/label'] — "Частное
  лицо" -> True, "Агентство" -> False. Подтверждено разными значениями на двух
  эталонах.
- days_on_market: на странице явно нет, но есть publish_date, из которого честно
  выводится. Заодно чинит сам publish_date — искали дату ВНУТРИ item-id-блока, а
  она в СОСЕДНЕМ [data-marker='item-view/item-date'] ("сегодня в HH:MM").
- cadastral_number: подтверждено отсутствие на странице (не парсим, не выдумываем).

Тесты на реальной разметке (backend/tests/test_avito_detail_fields_3086.py) на
обеих эталонных фикстурах (большие embedded JS-блобы вырезаны из фикстур —
не используются DOM-based парсером, экономят место). estimator.py не тронут —
ни одно поле не влияет на цену.
chore(tradein/avito): ссылки на реальный issue #3047 вместо выдуманного номера
All checks were successful
CI Trade-In / changes (pull_request) Successful in 8s
CI Trade-In / browser-tests (pull_request) Has been skipped
CI / changes (pull_request) Successful in 11s
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 4m48s
f34edeb512
Воркер сослался на #3086, которого не существует — тест с таким именем
остался бы загадкой для следующего читателя. Заведён настоящий issue #3047
с замерами покрытия и разбором дефекта; все ссылки и имя файла приведены
к нему.

Refs #3047
bot-backend added 1 commit 2026-08-21 20:46:41 +00:00
Merge remote-tracking branch 'forgejo/main' into fix/avito-detail-fields
All checks were successful
CI Trade-In / changes (pull_request) Successful in 9s
CI / changes (pull_request) Successful in 11s
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 4m42s
4c9f5b6e9f
lekss361 merged commit a67ed2bd07 into main 2026-08-21 20:54:36 +00:00
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
2 participants
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference: lekss361/gendesign#3048
No description provided.