fix(tradein): фильтр выдачи Яндекса мёртв — slug ЖК ищем по странице объекта (#2860) #2923

Merged
bot-backend merged 1 commit from fix/2860-yandex-slug-resolver into main 2026-08-19 08:37:41 +00:00
Collaborator

Разобрал по порядку, который задан в задаче.

1. «Почему прогон стал недельным» — он не ломался

scrape_schedules: yandex_newbuilding_sweep
default_params = {"city":"ekaterinburg","limit":5,"interval_days":7,...}
last_run 2026-08-17 03:38 · next_run 2026-08-24 02:03

interval_days: 7 — недельный по конфигу. Разрыв 03.08 → 10.08, который выглядел
пропуском, это и есть штатное расписание. Отдельный вопрос — правильна ли недельная
частота при очереди в 397 домов и лимите 5 за прогон, но это решение, а не поломка.

2. На чём спотыкается разрешение адресов

Прогнал резолвер тем же трактом, каким ходит обход — через браузерный сайдкар прода
(docker exec tradein-scraper), а не своим curl'ом:

INFO  httpx: POST http://tradein-browser:3000/fetch "200 OK"
WARN  resolve_yandex_jk_slug jk_id=286394: no matching slug in SERP HTML (markup drift?)

Фетч работает. Дальше разобрал сам HTML:

URL      /ekaterinburg/kupit/novostrojka/?siteId=286394
длина    1 726 390 байт
title    «Новостройки в Екатеринбурге — ЖК от застройщиков…»   ← общий список
совпадений старого regex   128        ← разметка ЦЕЛА
уникальных id на странице   35
запрошенный id среди них    НЕТ
вхождений 286394            41 — все внутри retpath ссылки «Войти»

Фильтр ?siteId= Яндексом больше не применяется. Отдаётся общий список новостроек.
Проверил на втором id (2671892) — та же страница, те же 35 ЖК, запрошенного снова нет.

То есть предположение в коде — markup drift?неверно: парсер исправен, умерла
стратегия. Вопросительный знак убран вместе с догадкой (это ровно тот случай, когда
«диагностика со знаком вопроса» дальше читается как факт).

3. Было ли иначе

Последний прогон с succeeded > 01489 от 15.07, и даже он давал succeeded: 1 при
failed_resolve: 4. Витрина стоит на 34 строках с 15.07.02:51. С 16.07 — четырнадцать
прогонов подряд по нулям.

Как чиню

В URL Яндекса авторитетен id, slug — косметика. Страница с плейсхолдером вместо slug
доезжает до нужного ЖК (проверено на проде 19.08):

/kupit/novostrojka/zhk-286394/   → ЖК «Успенский»  → slug uspenskij
/kupit/novostrojka/zhk-2671892/  → «ШишкINN»       → slug shishkinn
/kupit/novostrojka/zhk-999999999/ → общий список   → None   ← отрицательный контроль

Плейсхолдер отфильтрован из результата: страница ссылается сама на себя запрошенным
адресом, и без фильтра мы бы «разрешили» slug в тот, что сами и придумали — то есть
записали бы выдумку как факт.

Regex теперь привязан к конкретному id. Общий _JK_SLUG_RE матчил ЛЮБУЮ пару slug-id и
на общем списке давал 128 совпадений — привязка к id и есть разница между «нашли ЖК» и
«нашли какой-то ЖК».

Статус прогона

processed > 0 при succeeded == 0 больше не done, а failed с названной причиной.
Счётчик и раньше был честный — не хватало вывода из него (#2674: «механизм
исполняется, счётчик честный, вывод не делает никто»).

Счётчик не подгоняю — как и просит задача: если дома не разрешаются, честный исход
назвать прогон неуспешным.

Проверка

Тесты двусторонние, против кода из main:

резолвер:  3 failed из 4
    FAILED requests_jk_page_not_serp_filter
    FAILED reads_real_slug_from_page
    FAILED does_not_return_placeholder
    passed returns_none_on_general_list      ← контроль: тут и была поломка
статус:    1 failed из 3
    FAILED zero_resolved_marks_run_failed
    passed partial_success_still_done        ← контроль обратной крайности
    passed empty_queue_is_not_a_failure      ← контроль: пустая очередь ≠ отказ
после правки: 15 passed
  • pytest резолвер + статус + существующий sweep — 25 passed
  • ruff check — clean

Чего этот PR НЕ чинит

Очередь упирается в те же пять домов. 396 из 397 ожидающих не имеют slug, выборка —
ORDER BY h.id LIMIT 5, а неудача ничего не помечает. Поэтому каждую неделю берутся
буквально одни и те же пять домов (6706, 6741, 6757, 6774, 6785). Пока резолвер был
сломан, это не было видно; после этой правки голова очереди сдвинется, но любой
неразрешимый дом снова заблокирует её навсегда
. Нужен last_attempt_at/backoff —
это миграция, завожу отдельно.

При лимите 5 и недельном интервале разбор 397 домов займёт полтора года — второй довод
к тому же разговору о расписании.

Refs #2860

Разобрал по порядку, который задан в задаче. ## 1. «Почему прогон стал недельным» — он не ломался ``` scrape_schedules: yandex_newbuilding_sweep default_params = {"city":"ekaterinburg","limit":5,"interval_days":7,...} last_run 2026-08-17 03:38 · next_run 2026-08-24 02:03 ``` `interval_days: 7` — недельный **по конфигу**. Разрыв 03.08 → 10.08, который выглядел пропуском, это и есть штатное расписание. Отдельный вопрос — правильна ли недельная частота при очереди в 397 домов и лимите 5 за прогон, но это решение, а не поломка. ## 2. На чём спотыкается разрешение адресов Прогнал резолвер **тем же трактом, каким ходит обход** — через браузерный сайдкар прода (`docker exec tradein-scraper`), а не своим curl'ом: ``` INFO httpx: POST http://tradein-browser:3000/fetch "200 OK" WARN resolve_yandex_jk_slug jk_id=286394: no matching slug in SERP HTML (markup drift?) ``` Фетч работает. Дальше разобрал сам HTML: ``` URL /ekaterinburg/kupit/novostrojka/?siteId=286394 длина 1 726 390 байт title «Новостройки в Екатеринбурге — ЖК от застройщиков…» ← общий список совпадений старого regex 128 ← разметка ЦЕЛА уникальных id на странице 35 запрошенный id среди них НЕТ вхождений 286394 41 — все внутри retpath ссылки «Войти» ``` **Фильтр `?siteId=` Яндексом больше не применяется.** Отдаётся общий список новостроек. Проверил на втором id (2671892) — та же страница, те же 35 ЖК, запрошенного снова нет. То есть предположение в коде — `markup drift?` — **неверно**: парсер исправен, умерла стратегия. Вопросительный знак убран вместе с догадкой (это ровно тот случай, когда «диагностика со знаком вопроса» дальше читается как факт). ## 3. Было ли иначе Последний прогон с `succeeded > 0` — **1489 от 15.07**, и даже он давал `succeeded: 1` при `failed_resolve: 4`. Витрина стоит на 34 строках с 15.07.02:51. С 16.07 — четырнадцать прогонов подряд по нулям. ## Как чиню В URL Яндекса авторитетен **id**, slug — косметика. Страница с плейсхолдером вместо slug доезжает до нужного ЖК (проверено на проде 19.08): ``` /kupit/novostrojka/zhk-286394/ → ЖК «Успенский» → slug uspenskij /kupit/novostrojka/zhk-2671892/ → «ШишкINN» → slug shishkinn /kupit/novostrojka/zhk-999999999/ → общий список → None ← отрицательный контроль ``` Плейсхолдер отфильтрован из результата: страница ссылается сама на себя запрошенным адресом, и без фильтра мы бы «разрешили» slug в тот, что сами и придумали — то есть записали бы выдумку как факт. Regex теперь привязан к конкретному id. Общий `_JK_SLUG_RE` матчил ЛЮБУЮ пару slug-id и на общем списке давал 128 совпадений — привязка к id и есть разница между «нашли ЖК» и «нашли какой-то ЖК». ## Статус прогона `processed > 0` при `succeeded == 0` больше не `done`, а `failed` с названной причиной. Счётчик и раньше был честный — не хватало **вывода** из него (#2674: «механизм исполняется, счётчик честный, вывод не делает никто»). Счётчик **не подгоняю** — как и просит задача: если дома не разрешаются, честный исход назвать прогон неуспешным. ## Проверка Тесты двусторонние, против кода из main: ``` резолвер: 3 failed из 4 FAILED requests_jk_page_not_serp_filter FAILED reads_real_slug_from_page FAILED does_not_return_placeholder passed returns_none_on_general_list ← контроль: тут и была поломка статус: 1 failed из 3 FAILED zero_resolved_marks_run_failed passed partial_success_still_done ← контроль обратной крайности passed empty_queue_is_not_a_failure ← контроль: пустая очередь ≠ отказ после правки: 15 passed ``` - [x] `pytest` резолвер + статус + существующий sweep — 25 passed - [x] `ruff check` — clean ## Чего этот PR НЕ чинит **Очередь упирается в те же пять домов.** 396 из 397 ожидающих не имеют slug, выборка — `ORDER BY h.id LIMIT 5`, а неудача ничего не помечает. Поэтому каждую неделю берутся буквально одни и те же пять домов (6706, 6741, 6757, 6774, 6785). Пока резолвер был сломан, это не было видно; после этой правки голова очереди сдвинется, но **любой неразрешимый дом снова заблокирует её навсегда**. Нужен `last_attempt_at`/backoff — это миграция, завожу отдельно. При лимите 5 и недельном интервале разбор 397 домов займёт полтора года — второй довод к тому же разговору о расписании. Refs #2860
bot-backend added 1 commit 2026-08-19 08:12:40 +00:00
fix(tradein): фильтр выдачи Яндекса мёртв — slug ЖК ищем по странице объекта
All checks were successful
CI Trade-In / changes (pull_request) Successful in 8s
CI Trade-In / browser-tests (pull_request) Has been skipped
CI Trade-In / frontend-checks (pull_request) Has been skipped
CI / changes (pull_request) Successful in 11s
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
73dd3e041f
Обход yandex_newbuilding_sweep не разрешил НИ ОДНОГО дома с 16.07.2026:
четырнадцать прогонов подряд `processed 5, succeeded 0`, витрина
market.yandex_jk_enrichment замерла на 34 строках, очередь выросла 351 → 397.
При этом статус у всех — `done`, error_text пуст.

ЧТО ИМЕННО СЛОМАЛОСЬ (замер 19.08 через браузерный сайдкар прода, тем же
трактом, каким ходит сам обход)

Резолвер искал slug на выдаче с фильтром `?siteId=<jk_id>`. Фильтр Яндексом
больше НЕ применяется: страница отдаёт общий список новостроек — 1.7 МБ,
35 разных ЖК, запрошенного id среди них нет. Проверено на двух разных id,
ответы почти совпадают.

Разметка при этом ЦЕЛА: прежний regex находил 128 ссылок нужной формы. То
есть парсер работал, а стратегия умерла — и предположение «markup drift?»
в логе было неверным. Вопросительный знак из кода убран вместе с догадкой.

ПОЧЕМУ НОВЫЙ ПУТЬ РАБОТАЕТ

В URL Яндекса авторитетен id, slug — косметика. Запрос страницы с
плейсхолдером вместо slug доезжает до нужного ЖК:
  /kupit/novostrojka/zhk-286394/  → ЖК «Успенский», slug uspenskij
  /kupit/novostrojka/zhk-2671892/ → «ШишкINN»,      slug shishkinn
Отрицательный контроль: несуществующий id 999999999 отдаёт общий список,
ссылок с этим id нет → None, а не чужой slug.

Плейсхолдер отфильтрован из результата: страница ссылается сама на себя
запрошенным адресом, и без этого мы бы «разрешили» slug в тот, что сами же
и придумали.

СТАТУС ПРОГОНА

processed > 0 при succeeded == 0 больше не `done`. Счётчик был честный —
не хватало вывода из него (эпик #2674, «механизм исполняется, вывод не
делает никто»). Счётчик НЕ подгоняем: если дома не разрешаются, честный
исход — назвать прогон неуспешным.

Тесты двусторонние: против main падают 3 из 4 у резолвера
(requests_jk_page, reads_real_slug, does_not_return_placeholder) и 1 из 3
у статуса (zero_resolved_marks_run_failed). Остальные — контроли:
«общий список → None» зелёный с обеих сторон (в этом и была поломка),
«1 из 5 разрешён → done» и «пустая очередь → не отказ» защищают от
обратной крайности.

Хунк форматирования — не мой: pre-commit ruff v0.7.4 против 0.15.12 (#2864).

Refs #2860
bot-backend merged commit d0071c57bc into main 2026-08-19 08:37:41 +00:00
bot-backend deleted branch fix/2860-yandex-slug-resolver 2026-08-19 08:37:41 +00:00
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
1 participant
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#2923
No description provided.