fix(tradein/yandex): просмотры режутся по разделителю тысяч + Циан молчит о непонятой фразе (#2669) #2773

Merged
bot-backend merged 1 commit from fix/2669-views-total into main 2026-08-07 08:44:49 +00:00
Collaborator

Диагноз

Заявленная в #2669 причина (int() по фразе у Циана) подтвердилась, но уже была починена в PR #2705 (merged 2026-08-06, живёт на проде — сверено inspect.getsource в контейнере tradein-scraper). Проверка «а даёт ли она строки» вскрыла две вещи, которых в issue не было.

1. Яндекс кладёт в БД НЕВЕРНОЕ число (не пустоту)

RE_VIEWS был (\d+)\s+просмотр — при том что соседние RE_PRICE/RE_PPM2 в том же файле давно (\d[\d\s]+). Причина известна автору тех строк: блок OfferCardSummary, из которого и читаются просмотры, отдаёт числа с NBSP-разделителем. Дословно из сохранённого ответа tests/fixtures/yandex_offer_3402418396407468801.html:

<div data-test="OfferCardSummary">16 июня, 8 просмотров35,5 м², 1-комнатная квартира3 850 000 ₽108 451 ₽ за м²...

(\xa0 между всеми разрядами). На 1 234 просмотра поиск цеплялся за хвост и писал 234.

Косвенное подтверждение с прода на 2026-08-07: из 1620 непустых views_total_yandexровно ноль значений ≥1000, при 2512 таких у avito. Тихо неверное число хуже пустого: оно не отличимо от правды и уже утекло в 1620 строк.

2. Циан глотал непонятую фразу молча

_parse_views возвращала (None, None) без единой строки в журнале. Пустая колонка одинаково выглядела и при сломанном разборе, и при молчании площадки — ровно та слепота, из-за которой потеря держалась месяцами (эпик #2674). Теперь непустая, но непонятая строка уходит в logger.warning вместе с собой (обрезанной до 80 символов). Отсутствие ключа вовсе и штатное «0 просмотров» без хвоста «за сегодня» не шумят.

Проверено, а не пересказано

  • Настоящий образец Циана: "totalViewsFormattedString":"146 просмотров, 8 за сегодня" — из cian_flat_330982715.html.
  • Настоящий образец Яндекса: 8\xa0просмотров + 3\xa0850\xa0000 ₽ — из yandex_offer_3402418396407468801.html.
  • Даты сверки проставлены рядом с регулярками: разметка чужая и стареет сама.

Тесты красные на старом коде

FAILED test_yandex_detail.py::test_views_total_survives_thousands_separator[12 345 просмотров-12345]
  AssertionError: просмотры Яндекса снова режутся по разделителю тысяч
  assert 345 == 12345
FAILED test_yandex_detail.py::test_views_total_survives_thousands_separator[1 234 просмотра-1234]
FAILED test_yandex_detail.py::test_views_total_survives_thousands_separator[1 234 просмотра-1234]  (обычный пробел)
FAILED test_cian_views_parse.py::test_unparsed_phrase_is_logged_with_itself
4 failed, 53 passed

После правки — 57 passed; весь yandex/cian-срез: 452 passed, 1 skipped.

Test plan

  • Тесты падают на старом коде (показано выше), проходят на новом
  • 452 passed на срезе -k "yandex or cian or views" — RE_VIEWS общий, регрессий нет
  • ruff + ruff-format (pre-commit) зелёные; новых замечаний ноль
  • Прод-верификация числом — критерии ниже

Прод-верификация: критерий приёмки ДО факта

Яндекс — проверяемо, прогон ежедневный. yandex_detail_backfill последний раз отработал 2026-08-07 01:55, следующий — 2026-08-08 20:39 UTC.
Критерий: после первого прогона на новом образе среди строк с detail_enriched_at > <момент деплоя> появляется хотя бы одно views_total_yandex >= 1000. Сейчас таких 0 из 1620.

SELECT count(*) FILTER (WHERE views_total_yandex >= 1000) AS ge1000, count(*) AS enriched
FROM listings WHERE source='yandex' AND detail_enriched_at > TIMESTAMPTZ '<деплой>';

Отрицательный результат тоже информативен: если за 3 прогона не появится ни одного ≥1000, гипотеза «у Яндекса есть лоты с 1000+ просмотров» неверна и правка просто безвредна.

Циан — проверить нельзя, и это отдельная поломка. views_total останется пуст у всех 21 951 строк независимо от этого PR: cian_history_backfill skipped с причиной cian_cookies_expired, куки протухли 2026-06-30 (38 дней назад), последний удачный прогон — 2026-06-29. Следующий тик 2026-08-08 04:15 UTC снова даст skipped.
Критерий (условный, до перезалива кук): в первом прогоне со статусом done — доля непустых views_total среди обработанных в этом прогоне cian-строк ≥ 90%; ожидаемый объём первого прогона — batch_size (50), пул кандидатов 18 682.

Уже собранные 1571 detail-строки Циана не перезаполнятся: выборка backfill'а берёт только листинги без offer_price_history, а у 1569 из них она уже есть.

Отдельно: колонку никто не читает

Потребителей views_total в коде ноль — только писатели четырёх провайдеров и частичный индекс. «Признак ликвидности» из #2669 пока ничем не пользуется.

Предлагаю оставить и чинить (сделано здесь), а не удалять: 18 651 непустая строка уже собрана честно (avito 12 355, domklik 6 296), колонка стоит int + partial index. Но «копим впрок» без потребителя — это состояние со сроком годности: если к следующему пересмотру оценщика ликвидность так и не будет читаться, честнее удалить колонку, чем поддерживать четыре парсера ради NULL-склада.

Refs #2669, #2674

## Диагноз Заявленная в #2669 причина (`int()` по фразе у Циана) **подтвердилась, но уже была починена** в PR #2705 (merged 2026-08-06, живёт на проде — сверено `inspect.getsource` в контейнере `tradein-scraper`). Проверка «а даёт ли она строки» вскрыла две вещи, которых в issue не было. ### 1. Яндекс кладёт в БД НЕВЕРНОЕ число (не пустоту) `RE_VIEWS` был `(\d+)\s+просмотр` — при том что соседние `RE_PRICE`/`RE_PPM2` **в том же файле** давно `(\d[\d\s]+)`. Причина известна автору тех строк: блок `OfferCardSummary`, из которого и читаются просмотры, отдаёт числа с NBSP-разделителем. Дословно из сохранённого ответа `tests/fixtures/yandex_offer_3402418396407468801.html`: ``` <div data-test="OfferCardSummary">16 июня, 8 просмотров35,5 м², 1-комнатная квартира3 850 000 ₽108 451 ₽ за м²... ``` (`\xa0` между всеми разрядами). На `1 234 просмотра` поиск цеплялся за хвост и писал **234**. Косвенное подтверждение с прода на 2026-08-07: из 1620 непустых `views_total_yandex` — **ровно ноль** значений ≥1000, при 2512 таких у avito. Тихо неверное число хуже пустого: оно не отличимо от правды и уже утекло в 1620 строк. ### 2. Циан глотал непонятую фразу молча `_parse_views` возвращала `(None, None)` без единой строки в журнале. Пустая колонка одинаково выглядела и при сломанном разборе, и при молчании площадки — ровно та слепота, из-за которой потеря держалась месяцами (эпик #2674). Теперь непустая, но непонятая строка уходит в `logger.warning` **вместе с собой** (обрезанной до 80 символов). Отсутствие ключа вовсе и штатное «0 просмотров» без хвоста «за сегодня» не шумят. ## Проверено, а не пересказано - Настоящий образец Циана: `"totalViewsFormattedString":"146 просмотров, 8 за сегодня"` — из `cian_flat_330982715.html`. - Настоящий образец Яндекса: `8\xa0просмотров` + `3\xa0850\xa0000 ₽` — из `yandex_offer_3402418396407468801.html`. - Даты сверки проставлены рядом с регулярками: разметка чужая и стареет сама. ## Тесты красные на старом коде ``` FAILED test_yandex_detail.py::test_views_total_survives_thousands_separator[12 345 просмотров-12345] AssertionError: просмотры Яндекса снова режутся по разделителю тысяч assert 345 == 12345 FAILED test_yandex_detail.py::test_views_total_survives_thousands_separator[1 234 просмотра-1234] FAILED test_yandex_detail.py::test_views_total_survives_thousands_separator[1 234 просмотра-1234] (обычный пробел) FAILED test_cian_views_parse.py::test_unparsed_phrase_is_logged_with_itself 4 failed, 53 passed ``` После правки — 57 passed; весь yandex/cian-срез: 452 passed, 1 skipped. ## Test plan - [x] Тесты падают на старом коде (показано выше), проходят на новом - [x] `452 passed` на срезе `-k "yandex or cian or views"` — RE_VIEWS общий, регрессий нет - [x] ruff + ruff-format (pre-commit) зелёные; новых замечаний ноль - [ ] Прод-верификация числом — критерии ниже ## Прод-верификация: критерий приёмки ДО факта **Яндекс — проверяемо, прогон ежедневный.** `yandex_detail_backfill` последний раз отработал 2026-08-07 01:55, следующий — **2026-08-08 20:39 UTC**. Критерий: после первого прогона на новом образе среди строк с `detail_enriched_at > <момент деплоя>` появляется **хотя бы одно** `views_total_yandex >= 1000`. Сейчас таких 0 из 1620. ```sql SELECT count(*) FILTER (WHERE views_total_yandex >= 1000) AS ge1000, count(*) AS enriched FROM listings WHERE source='yandex' AND detail_enriched_at > TIMESTAMPTZ '<деплой>'; ``` Отрицательный результат тоже информативен: если за 3 прогона не появится ни одного ≥1000, гипотеза «у Яндекса есть лоты с 1000+ просмотров» неверна и правка просто безвредна. **Циан — проверить нельзя, и это отдельная поломка.** `views_total` останется пуст у всех 21 951 строк независимо от этого PR: `cian_history_backfill` **skipped с причиной `cian_cookies_expired`**, куки протухли **2026-06-30** (38 дней назад), последний удачный прогон — 2026-06-29. Следующий тик 2026-08-08 04:15 UTC снова даст `skipped`. Критерий (**условный, до перезалива кук**): в первом прогоне со статусом `done` — доля непустых `views_total` среди обработанных в этом прогоне cian-строк **≥ 90%**; ожидаемый объём первого прогона — batch_size (50), пул кандидатов 18 682. Уже собранные 1571 detail-строки Циана не перезаполнятся: выборка backfill'а берёт только листинги **без** `offer_price_history`, а у 1569 из них она уже есть. ## Отдельно: колонку никто не читает Потребителей `views_total` в коде **ноль** — только писатели четырёх провайдеров и частичный индекс. «Признак ликвидности» из #2669 пока ничем не пользуется. Предлагаю **оставить и чинить** (сделано здесь), а не удалять: 18 651 непустая строка уже собрана честно (avito 12 355, domklik 6 296), колонка стоит int + partial index. Но «копим впрок» без потребителя — это состояние со сроком годности: если к следующему пересмотру оценщика ликвидность так и не будет читаться, честнее удалить колонку, чем поддерживать четыре парсера ради `NULL`-склада. Refs #2669, #2674
bot-backend added 1 commit 2026-08-07 08:40:08 +00:00
fix(tradein/yandex): просмотры режутся по разделителю тысяч + Циан молчит о непонятой фразе (#2669)
All checks were successful
CI Trade-In / changes (pull_request) Successful in 8s
CI / 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 / 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 3m47s
22c3882271
Разбор #2705 закрыл int() у Циана, но проверка на проде вскрыла две вещи,
которых в issue не было.

1. Яндекс пишет НЕВЕРНОЕ число, а не пустоту. RE_VIEWS был `(\d+)\s+просмотр`,
   тогда как соседние RE_PRICE/RE_PPM2 в том же файле давно `(\d[\d\s]+)` —
   потому что тот же блок OfferCardSummary отдаёт числа через NBSP
   («3 850 000 ₽», «108 451 ₽ за м²», сверено с сохранённым ответом
   yandex_offer_3402418396407468801.html). На «1 234 просмотра» поиск цеплялся
   за хвост и клал в БД 234. Косвенно с прода: из 1620 непустых
   views_total_yandex ровно НОЛЬ значений ≥1000 при 2512 таких у avito.
   Тихо неверное число хуже пустого — оно не отличимо от правды.

2. Циан глотал неразобранную фразу молча. Пустая колонка одинаково выглядела
   и при сломанном разборе, и при молчании площадки — ровно та слепота, из-за
   которой потеря держалась месяцами (эпик #2674). Теперь непустая, но
   непонятая строка уходит в warning вместе с собой (обрезанной до 80).

Тесты падают на старом коде: yandex — `assert 345 == 12345`, cian — фраза не
видна в журнале. Покрыты NBSP, обычный пробел и «нет данных».

Refs #2669, #2674
bot-backend merged commit 339713c1aa into main 2026-08-07 08:44:49 +00:00
bot-backend deleted branch fix/2669-views-total 2026-08-07 08:44:49 +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#2773
No description provided.