fix(ptica): «не продана» перестаёт означать «продана» (#2464) #2979

Merged
bot-backend merged 1 commit from fix/2464-status-negation into main 2026-08-20 16:27:21 +00:00
Collaborator

Дефект

Регекс статусов каталога ДОМ.РФ искал ключевые слова без защиты от отрицания. Русские отрицательные формы содержат положительные как подстроку, поэтому статус получался обратный:

текст на странице совпало статус верно?
Статус: нереализована реализована sold нет
Квартира не продана продана sold нет
Не реализовано ни одной квартиры реализовано sold нет
не забронирована забронирована reserved нет
не в продаже в продаже free нет

Свободная квартира уехала бы в domrf_kn_flats.status проданной.

Правка

Отрицание гасит токен, а не переворачивает его. не забронирована не означает ни sold, ни free, а не продана → free — это вывод, а не факт со страницы. Лучше отсутствие статуса, чем неверный.

Заодно вылечен второй дефект того же места: разбор брал первое совпадение в блоке. Квартира не продана. Статус: в продаже на main даёт sold — новый _status_in_text перебирает все вхождения и берёт первое неотрицаемое, так что настоящий статус в блоке больше не теряется.

Граница слова (\bне) не даёт зацепиться за хвост чужого слова: Цене снижена, квартира продана по-прежнему sold.

Текущий эффект на проде — нулевой, и это проверено

Не «вероятно не влияет», а измерено:

domrf_kn_flats: catalog_updated_at IS NOT NULL →      0 из 983 088
статусы в таблице: free 25 656 · sold 3 122 · booked 641 · NULL 953 669
beat_schedule.py:436 — "tasks.scrape_kn_catalog_flats" ЗАКОММЕНТИРОВАНА (WAF-cooldown)

Каталожный скрапер не записал ни одной строки: он всегда ставит catalog_updated_at вместе со status, а этот столбец пуст поголовно. Существующие статусы пришли из kn-API — среди них booked, которого нет в константах этого модуля (STATUS_RESERVED = "reserved").

То есть правка предупредительная: сегодня инвертировать нечего, но при включении пути статусы поехали бы молча и неотличимо от правильных.

Как проверено

  • Двусторонне: против origin/main 7 тестов красные, каждый с конкретным неверным значением в сообщении («Нереализована» классифицировано как 'sold', assert 'sold' != 'sold') — краснота от значения, а не от отсутствующего символа.
  • Контроли зелёные с обеих сторон: 7 обычных форм (Продана, Реализована, Забронирована, Бронь, В продаже, Свободна, Продано) и случай «не» в хвосте слова. Они ловят «починку» через грубое гашение всего рядом со словом «не».
  • Тесты идут через parse_catalog_flat (полный разбор HTML), а не через хелпер — красное значение это ровно то, что доехало бы до БД.
  • pytest backend/tests/services/scrapers/ — 320 passed, 6 skipped. Существующие 15 тестов статусов не тронуты.

Часть эпика #2464.

## Дефект Регекс статусов каталога ДОМ.РФ искал ключевые слова без защиты от отрицания. Русские отрицательные формы содержат положительные как подстроку, поэтому статус получался **обратный**: | текст на странице | совпало | статус | верно? | |---|---|---|---| | `Статус: нереализована` | `реализована` | **sold** | нет | | `Квартира не продана` | `продана` | **sold** | нет | | `Не реализовано ни одной квартиры` | `реализовано` | **sold** | нет | | `не забронирована` | `забронирована` | **reserved** | нет | | `не в продаже` | `в продаже` | **free** | нет | Свободная квартира уехала бы в `domrf_kn_flats.status` проданной. ## Правка Отрицание **гасит** токен, а не переворачивает его. `не забронирована` не означает ни sold, ни free, а `не продана → free` — это вывод, а не факт со страницы. Лучше отсутствие статуса, чем неверный. Заодно вылечен второй дефект того же места: разбор брал **первое** совпадение в блоке. `Квартира не продана. Статус: в продаже` на main даёт `sold` — новый `_status_in_text` перебирает все вхождения и берёт первое неотрицаемое, так что настоящий статус в блоке больше не теряется. Граница слова (`\bне`) не даёт зацепиться за хвост чужого слова: `Цене снижена, квартира продана` по-прежнему `sold`. ## Текущий эффект на проде — нулевой, и это проверено Не «вероятно не влияет», а измерено: ``` domrf_kn_flats: catalog_updated_at IS NOT NULL → 0 из 983 088 статусы в таблице: free 25 656 · sold 3 122 · booked 641 · NULL 953 669 beat_schedule.py:436 — "tasks.scrape_kn_catalog_flats" ЗАКОММЕНТИРОВАНА (WAF-cooldown) ``` Каталожный скрапер не записал **ни одной** строки: он всегда ставит `catalog_updated_at` вместе со `status`, а этот столбец пуст поголовно. Существующие статусы пришли из kn-API — среди них `booked`, которого нет в константах этого модуля (`STATUS_RESERVED = "reserved"`). То есть правка **предупредительная**: сегодня инвертировать нечего, но при включении пути статусы поехали бы молча и неотличимо от правильных. ## Как проверено - **Двусторонне:** против `origin/main` 7 тестов красные, каждый с конкретным неверным значением в сообщении (`«Нереализована» классифицировано как 'sold'`, `assert 'sold' != 'sold'`) — краснота от значения, а не от отсутствующего символа. - **Контроли зелёные с обеих сторон:** 7 обычных форм (`Продана`, `Реализована`, `Забронирована`, `Бронь`, `В продаже`, `Свободна`, `Продано`) и случай `«не» в хвосте слова`. Они ловят «починку» через грубое гашение всего рядом со словом «не». - Тесты идут через `parse_catalog_flat` (полный разбор HTML), а не через хелпер — красное значение это ровно то, что доехало бы до БД. - `pytest backend/tests/services/scrapers/` — 320 passed, 6 skipped. Существующие 15 тестов статусов не тронуты. Часть эпика #2464.
bot-backend added 1 commit 2026-08-20 12:45:45 +00:00
fix(ptica): «не продана» перестаёт означать «продана» (#2464)
All checks were successful
CI Trade-In / changes (pull_request) Successful in 8s
CI Trade-In / backend-tests (pull_request) Has been skipped
CI Trade-In / browser-tests (pull_request) Has been skipped
CI / changes (pull_request) Successful in 12s
CI Trade-In / frontend-checks (pull_request) Has been skipped
CI / frontend-tests (pull_request) Has been skipped
CI / openapi-codegen-check (pull_request) Successful in 1m58s
CI / backend-tests (pull_request) Successful in 17m22s
91b0747b3e
Регекс статусов каталога ДОМ.РФ искал ключевые слова без защиты от
отрицания, поэтому русские отрицательные формы давали ОБРАТНЫЙ статус:

    «нереализована»    → содержит «реализована»   → sold
    «не продана»       → содержит «продана»       → sold
    «не забронирована» → содержит «забронирована» → reserved
    «не в продаже»     → содержит «в продаже»     → free

Свободная квартира попадала бы в domrf_kn_flats.status проданной.

Отрицание теперь гасит токен, а не переворачивает его. «Не забронирована»
не означает ни sold, ни free; «не продана → free» — это вывод, а не факт
со страницы. Лучше отсутствие статуса, чем неверный.

Заодно вылечен второй дефект того же места: разбор брал ПЕРВОЕ совпадение
в блоке, поэтому «Квартира не продана. Статус: в продаже» на main даёт
sold. Новый _status_in_text перебирает все вхождения и берёт первое
неотрицаемое — настоящий статус в блоке больше не теряется.

Текущий эффект на проде НУЛЕВОЙ, и это проверено, а не предположено:
catalog_updated_at пуст у всех 983 088 строк domrf_kn_flats (скрапер не
записал ни одной), таска scrape_kn_catalog_flats закомментирована в
beat_schedule.py из-за WAF-cooldown. Существующие значения status
(free 25 656 / sold 3 122 / booked 641) пришли из kn-API — среди них
'booked', которого нет в константах этого модуля. Правка
предупредительная: при включении пути дефект инвертировал бы статусы молча.

Двусторонне: против origin/main 7 тестов красные, каждый с конкретным
неверным значением («Нереализована» → 'sold'). Контроли (7 обычных форм
и «не» в хвосте слова «Цене») зелёные с обеих сторон. Старая сюита
парсера — 320 passed, регрессий нет.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
bot-backend merged commit 9a3183d6a8 into main 2026-08-20 16:27:21 +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#2979
No description provided.