tradein/cian: views_total пуст у всех 21 682 объявлений — парсер делает int() по строке «146 просмотров, 8 за сегодня» #2669

Closed
opened 2026-08-05 18:46:00 +00:00 by bot-backend · 5 comments
Collaborator

Найдено попутно при разборе #2435. Тот же класс, что и остальные сегодняшние находки: код написан, работает, и не даёт ни одной строки.

Что происходит

listings.views_total пуст (NULL) у всех 21 682 объявлений Циана без единого исключения.

Причина в парсере: Циан отдаёт число просмотров не числом, а фразой — stats.totalViewsFormattedString = "146 просмотров, 8 за сегодня". Функция разбора вызывает на этой строке int(), что всегда даёт ValueError, исключение проглатывается, и поле остаётся пустым.

То есть данные приходят с каждым объявлением, разбираются кодом, который для них не предназначен, и молча теряются — уже неизвестно сколько времени.

Почему это стоит починить

Число просмотров — прямой признак ликвидности лота: сколько людей смотрели объявление за время экспозиции. Для трейд-ина это один из немногих доступных сигналов «насколько живо предложение», и он мог бы участвовать и в оценке скорости продажи, и в объяснении для клиента.

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

Оговорка

Историю не восстановить: у уже собранных 21 682 объявлений исходная строка не сохранена, есть только пустая колонка. Данные пойдут с новых проходов детального парсера — а он, по состоянию на сегодня, стоит с 22 июля (см. #2435), так что эффект появится только после его возобновления.

Связано: #2435 (там же вскрыт мёртвый путь записи данных о доме), #2657, #2658 — сегодняшняя серия находок одного класса.

Найдено попутно при разборе #2435. Тот же класс, что и остальные сегодняшние находки: код написан, работает, и **не даёт ни одной строки**. ## Что происходит `listings.views_total` пуст (`NULL`) у **всех 21 682 объявлений Циана** без единого исключения. Причина в парсере: Циан отдаёт число просмотров не числом, а фразой — `stats.totalViewsFormattedString` = `"146 просмотров, 8 за сегодня"`. Функция разбора вызывает на этой строке `int()`, что всегда даёт `ValueError`, исключение проглатывается, и поле остаётся пустым. То есть данные приходят с каждым объявлением, разбираются кодом, который для них не предназначен, и молча теряются — уже неизвестно сколько времени. ## Почему это стоит починить Число просмотров — прямой признак ликвидности лота: сколько людей смотрели объявление за время экспозиции. Для трейд-ина это один из немногих доступных сигналов «насколько живо предложение», и он мог бы участвовать и в оценке скорости продажи, и в объяснении для клиента. Правка сама по себе маленькая — вытащить число из начала строки. Заодно стоит посмотреть, отдаёт ли Циан там же «за сегодня» отдельным полем и нет ли аналогичной потери у других площадок. ## Оговорка Историю не восстановить: у уже собранных 21 682 объявлений исходная строка не сохранена, есть только пустая колонка. Данные пойдут с новых проходов детального парсера — а он, по состоянию на сегодня, **стоит с 22 июля** (см. #2435), так что эффект появится только после его возобновления. Связано: #2435 (там же вскрыт мёртвый путь записи данных о доме), #2657, #2658 — сегодняшняя серия находок одного класса.
Author
Collaborator

Проверил заявленное — предпосылка issue верна, но уже неактуальна

int() по фразе у Циана действительно был, и уже починен в PR #2705 (merged 2026-08-06). Код живёт на проде — сверено inspect.getsource в контейнере tradein-scraper, не по тегу релиза.

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

Циан: ноль заблокирован ВЫШЕ, разбор ни при чём

cian_history_backfill не отработал ни разу с 2026-06-29. Сегодняшний тик (2026-08-07 02:03) — status='skipped', причина машиночитаемая: cian_cookies_expired. Куки Циана протухли 2026-06-30, 38 дней назад:

account_user_id | expires_at_estimate           | days_expired
       75544897 | 2026-06-30 08:21:24+00        |           38
      999999999 | 2026-06-23 16:22:25+00        |           45

Пока куки не перезальют через админку, views_total у Циана останется пуст при любом состоянии парсера. Правка разбора здесь верна, но проверить её нечем.

Ещё одна деталь, которая делает «эффект появится после возобновления» из issue слишком оптимистичным: выборка backfill'а берёт только листинги без offer_price_history. У 1569 из 1571 уже обогащённых она есть — эти строки не перезаполнятся никогда, их views_total пуст навсегда. Кандидатов с нуля — 18 682.

Яндекс: та же болезнь в худшей форме — не пустота, а НЕВЕРНОЕ число

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

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

На 1 234 просмотра поиск цеплялся за хвост и клал в БД 234. Прод на 2026-08-07: из 1620 непустых views_total_yandexровно ноль значений ≥1000, максимум 947; у avito таких 2512 из 12 355. Тихо неверное число хуже пустого: оно не отличимо от правды и уже разошлось по 1620 строкам.

Слепота, из-за которой это жило месяцами (#2674)

_parse_views возвращала (None, None) без единой строки в журнале — пустая колонка выглядела одинаково и при сломанном разборе, и при молчании площадки. Теперь непонятая непустая фраза уходит в warning вместе с собой (обрезанной). Рядом с регулярками проставлены даты сверки образцов: разметка чужая и стареет сама, без единой нашей правки.

Сделано

PR #2773 (merged, задеплоен — контейнер tradein-scraper перезапущен 2026-08-07 08:51 UTC, живая проверка кода: 1 234 просмотра1234).

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

Критерий приёмки — зафиксирован ДО прогона

Замер на 2026-08-07 08:55 UTC, деплой уже на проде:

  • views_total_yandex >= 1000: 0 (из 1620 непустых, max 947)
  • views_total у Циана: 0 из 21 951

Яндекс. Ближайший прогон yandex_detail_backfill2026-08-08 20:39 UTC.
Принимается, если среди строк с detail_enriched_at > 2026-08-07 08:51 UTC появится хотя бы одно views_total_yandex >= 1000:

SELECT count(*) FILTER (WHERE views_total_yandex >= 1000) AS ge1000, count(*) AS enriched
FROM listings
WHERE source='yandex' AND detail_enriched_at > TIMESTAMPTZ '2026-08-07 08:51:22+00';

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

Циан. Проверить нельзя и не будет можно, пока не перезальют куки — это блокер, а не наша правка. Критерий условный, на первый прогон со status='done': доля непустых views_total среди обработанных в этом прогоне строк ≥ 90% (batch_size 50).

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

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

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

Опровергнуто

  • «Парсер делает int(), и это причина сегодняшней пустоты» — int() был реальной ошибкой, но починен сутки назад. Сегодняшний ноль у Циана держится на протухших куках, и никакая правка разбора его не сдвинет.
  • «Данные пойдут с новых проходов детального парсера» — только для 18 682 листингов без offer_price_history; уже обогащённые 1569 не вернутся под выборку никогда.
  • «Аналогичной потери у других площадок» — у Яндекса не потеря, а искажение: 1620 строк с обрезанными числами. Это ценнее исходной находки.
## Проверил заявленное — предпосылка issue верна, но уже неактуальна `int()` по фразе у Циана действительно был, **и уже починен** в PR #2705 (merged 2026-08-06). Код живёт на проде — сверено `inspect.getsource` в контейнере `tradein-scraper`, не по тегу релиза. Значит вопрос не «почему код не написан», а «почему написанный код не даёт строк». Ответ — **два разных нуля**, и только один из них про разбор. ### Циан: ноль заблокирован ВЫШЕ, разбор ни при чём `cian_history_backfill` не отработал ни разу с 2026-06-29. Сегодняшний тик (2026-08-07 02:03) — `status='skipped'`, причина машиночитаемая: **`cian_cookies_expired`**. Куки Циана протухли **2026-06-30**, 38 дней назад: ``` account_user_id | expires_at_estimate | days_expired 75544897 | 2026-06-30 08:21:24+00 | 38 999999999 | 2026-06-23 16:22:25+00 | 45 ``` Пока куки не перезальют через админку, `views_total` у Циана останется пуст при любом состоянии парсера. Правка разбора здесь верна, но проверить её нечем. Ещё одна деталь, которая делает «эффект появится после возобновления» из issue слишком оптимистичным: выборка backfill'а берёт только листинги **без** `offer_price_history`. У 1569 из 1571 уже обогащённых она есть — эти строки не перезаполнятся никогда, их `views_total` пуст навсегда. Кандидатов с нуля — 18 682. ### Яндекс: та же болезнь в худшей форме — не пустота, а НЕВЕРНОЕ число Побочная находка, ради которой стоило смотреть. `RE_VIEWS` был `(\d+)\s+просмотр`, тогда как соседние `RE_PRICE`/`RE_PPM2` **в том же файле** давно `(\d[\d\s]+)` — автор тех строк знал, что Яндекс отдаёт числа с NBSP-разделителем, а про просмотры забыл. Дословно из сохранённого ответа `yandex_offer_3402418396407468801.html`: ``` <div data-test="OfferCardSummary">16 июня, 8 просмотров35,5 м², 1-комнатная квартира3 850 000 ₽108 451 ₽ за м²... ``` На `1 234 просмотра` поиск цеплялся за хвост и клал в БД **234**. Прод на 2026-08-07: из **1620** непустых `views_total_yandex` — **ровно ноль** значений ≥1000, максимум 947; у avito таких 2512 из 12 355. Тихо неверное число хуже пустого: оно не отличимо от правды и уже разошлось по 1620 строкам. ### Слепота, из-за которой это жило месяцами (#2674) `_parse_views` возвращала `(None, None)` без единой строки в журнале — пустая колонка выглядела одинаково и при сломанном разборе, и при молчании площадки. Теперь непонятая непустая фраза уходит в `warning` вместе с собой (обрезанной). Рядом с регулярками проставлены даты сверки образцов: разметка чужая и стареет сама, без единой нашей правки. ## Сделано PR #2773 (merged, задеплоен — контейнер `tradein-scraper` перезапущен 2026-08-07 08:51 UTC, живая проверка кода: `1 234 просмотра` → `1234`). Тесты падали на старом коде: `assert 345 == 12345` у Яндекса, «фраза не видна в журнале» у Циана. Покрыты NBSP, обычный пробел, «нет данных». ## Критерий приёмки — зафиксирован ДО прогона Замер на 2026-08-07 08:55 UTC, деплой уже на проде: - `views_total_yandex >= 1000`: **0** (из 1620 непустых, max 947) - `views_total` у Циана: **0** из 21 951 **Яндекс.** Ближайший прогон `yandex_detail_backfill` — **2026-08-08 20:39 UTC**. Принимается, если среди строк с `detail_enriched_at > 2026-08-07 08:51 UTC` появится **хотя бы одно** `views_total_yandex >= 1000`: ```sql SELECT count(*) FILTER (WHERE views_total_yandex >= 1000) AS ge1000, count(*) AS enriched FROM listings WHERE source='yandex' AND detail_enriched_at > TIMESTAMPTZ '2026-08-07 08:51:22+00'; ``` Отрицательный результат тоже читаемый: если за 3 прогона ни одного ≥1000 — значит лотов с 1000+ просмотров у Яндекса просто нет, правка безвредна, а гипотезу надо закрыть явно, а не оставлять «непроверенной» бессрочно. **Срок годности вердикта — 2026-08-12.** **Циан.** Проверить нельзя и не будет можно, пока не перезальют куки — это блокер, а не наша правка. Критерий условный, на первый прогон со `status='done'`: доля непустых `views_total` среди обработанных в этом прогоне строк **≥ 90%** (batch_size 50). ## Отдельно: колонку никто не читает Потребителей `views_total` в коде **ноль** — только писатели четырёх провайдеров и частичный индекс. «Признак ликвидности» из постановки пока ничем не пользуется. Решил **оставить и чинить**, а не удалять: 18 651 непустая строка уже собрана честно (avito 12 355, domklik 6 296), цена хранения — int + partial index. Но это состояние со сроком годности: если к следующему пересмотру оценщика ликвидность так и не будет читаться, честнее удалить колонку, чем поддерживать четыре парсера ради NULL-склада. ## Опровергнуто - «Парсер делает `int()`, и это причина сегодняшней пустоты» — `int()` был реальной ошибкой, но починен сутки назад. **Сегодняшний ноль у Циана держится на протухших куках**, и никакая правка разбора его не сдвинет. - «Данные пойдут с новых проходов детального парсера» — только для 18 682 листингов без `offer_price_history`; уже обогащённые 1569 не вернутся под выборку никогда. - «Аналогичной потери у других площадок» — у Яндекса не потеря, а **искажение**: 1620 строк с обрезанными числами. Это ценнее исходной находки.
Author
Collaborator

Проверка на проде 2026-08-07: код доехал, эффекта нет — заблокировано выше по потоку

Правка живёт в контейнере. Проверено не по релизу, а вызовом в живом tradein-scraper:

_parse_views("146 просмотров, 8 за сегодня") → (146, 8)

Колонка по-прежнему пуста: 0 из 21 951 cian-объявления (было 0 из 21 682 — выросло только
число строк). Причина не в правке: она стоит в providers/cian/detail.py::fetch_detail, а
detail-путь Циана отдаёт 403 на 50 из 50 попыток каждую ночь (#2700). detail_enriched_at у
Циана заморожен на 2026-07-22, обогащений с момента мержа правки — 0.

Это «заблокировано выше по потоку», а не «не починено». Оговорка в шапке задачи предсказала
ровно это, только называла причиной остановку detail-парсера (#2435); на самом деле парсер
бежит — его отбивает площадка, потому что куки протухли 30.06 (пункт владельца #2704 п.4).

Побочный вопрос задачи закрыт: аналогичной потери у других площадок нет.

источник detail-обогащено из них с views_total где живёт
avito 12 357 12 355 views_total
domklik 6 296 6 296 views_total
yandex 1 702 0 своя колонка views_total_yandex — 1 620 непустых, max 947
cian 1 571 0 views_total
n1 0 0 detail-обогащения нет вовсе

У Яндекса отдельная колонка по построению (conflict_resolution.py:127-128 фиксирует это
явно), а не потеря. Ноль у Циана — единственный.

Критерий приёмки (записан ДО факта)

SELECT count(views_total) FROM listings WHERE source='cian' > 0 после первого прогона
cian_city_sweep, у которого detail_enriched > 0.

Ближайший прогон — 2026-08-08 03:05 UTC, но он даст ноль, пока не восстановлены куки:
дата тут не решает, решает #2704 п.4. Задачу оставляю открытой до этого замера.

## Проверка на проде 2026-08-07: код доехал, эффекта нет — заблокировано выше по потоку **Правка живёт в контейнере.** Проверено не по релизу, а вызовом в живом `tradein-scraper`: ``` _parse_views("146 просмотров, 8 за сегодня") → (146, 8) ``` **Колонка по-прежнему пуста: 0 из 21 951** cian-объявления (было 0 из 21 682 — выросло только число строк). Причина не в правке: она стоит в `providers/cian/detail.py::fetch_detail`, а detail-путь Циана **отдаёт 403 на 50 из 50 попыток каждую ночь** (#2700). `detail_enriched_at` у Циана заморожен на **2026-07-22**, обогащений с момента мержа правки — **0**. Это «заблокировано выше по потоку», а не «не починено». Оговорка в шапке задачи предсказала ровно это, только называла причиной остановку detail-парсера (#2435); на самом деле парсер бежит — его отбивает площадка, потому что куки протухли 30.06 (пункт владельца #2704 п.4). **Побочный вопрос задачи закрыт: аналогичной потери у других площадок нет.** | источник | detail-обогащено | из них с `views_total` | где живёт | |---|---:|---:|---| | avito | 12 357 | **12 355** | `views_total` | | domklik | 6 296 | **6 296** | `views_total` | | yandex | 1 702 | 0 | **своя колонка** `views_total_yandex` — 1 620 непустых, max 947 | | cian | 1 571 | **0** | `views_total` | | n1 | 0 | 0 | detail-обогащения нет вовсе | У Яндекса отдельная колонка по построению (`conflict_resolution.py:127-128` фиксирует это явно), а не потеря. Ноль у Циана — единственный. ## Критерий приёмки (записан ДО факта) `SELECT count(views_total) FROM listings WHERE source='cian'` **> 0** после первого прогона `cian_city_sweep`, у которого `detail_enriched > 0`. Ближайший прогон — **2026-08-08 03:05 UTC**, но он даст ноль, пока не восстановлены куки: дата тут не решает, решает #2704 п.4. Задачу оставляю открытой до этого замера.
Author
Collaborator

Поправка: «примерно 330 испорченных строк» — моё число, и оно завышено вдесятеро

Я вывел его переносом доли Авито (20.3% значений ≥1000) на 1620 строк Яндекса. Три независимые проверки это опровергли, и опровергли измерением в той же таблице, а не рассуждением.

Почему перенос доли не работает

Остаток от срезания — последние три цифры, и я молча считал их равномерными на [0, 999]. Это проверяемо прямо в базе. Доля значений с остатком ≥300 среди реальных счётчиков ≥1000:

площадка доля из скольких
avito 61.7% 1550 из 2512
domklik 45.9% из 172

Равномерности нет и быть не может: просмотры распределены степенно, значения ≥1000 жмутся к самой тысяче, поэтому остаток смещён к нулю. Чем легче хвост площадки, тем ниже доля. Хвост Яндекса легче обоих — p99 равен 280 против 1483 у Домклика и 8401 у Авито, — значит его доля не выше 46%.

Наблюдаемых значений в диапазоне 300–999 на проде — 15. Отсюда:

  • верхняя граница испорченных: 15 / 0.46 ≈ 33 строки, с пуассоновским шумом до ~40 — то есть около 2.5%, а не 20%;
  • точечная оценка: избыток над честным хвостом ≈ 9 → ~20 строк.

Направление вывода это не меняет (десятки, а не сотни), но моё число было неверным, а неверным в удобную сторону — оно делало находку крупнее, чем она есть.

Второе, важнее числа: диагноз нуля я поставил неправильно

Я написал «ноль из 1620 при 20.3% у соседа — подпись дефекта». Формулировка небрежна, и вот в чём.

Под старой регуляркой значение ≥1000 с разделителем разрядов невозможно было записать вообще: 1 234 → 234, 12 345 → 345 (прогнано на настоящем образце, \s в Python матчит неразрывный пробел). То есть ноль в корзине «≥1000» механически гарантирован парсером и не несёт никакой информации о площадке.

Это не «неприменимо» и не «подпись дефекта» — это «заблокировано выше по потоку». Третий вид нуля, и я его смешал с первым.

Отдельно круговой довод, которым это едва не подпёрли: «у Яндекса p99 = 279, площадка просто не выдаёт четырёхзначных счётчиков». Перцентиль посчитан на тех самых данных, которые считаются испорченными — срезанные строки лежат как маленькие числа и сами придавливают его.

Честная формулировка: доказательство порчи не получено ни в одну сторону. Механизм реален и проверен, масштаб — десятки строк, но контрольной группы, разобранной уже исправленным выражением, на момент замера не существовало.

Что устояло и даже усилилось

Решение не трогать колонку подтверждено, и сильнее, чем я говорил: читателей ноль не только в коде, но и в каталоге прода — ноль совпадений в pg_views, pg_matviews, pg_get_functiondef. Единственная ссылка живёт в LISTING_FIELD_PRIORITY, её резолвер зовётся только из тестов, а единственная экспортируемая наружу функция update_canonical_fieldsзаглушка pass, которую никто не вызывает. Вся таблица приоритетов инертна.

Восстановить старые значения нечем: raw_payload у всех 1620 — это SERP-JSON без поля просмотров, HTML детальной страницы не хранится.

План проверки тоже не годился

Критерий «после ближайшего прогона 08.08 вопрос закрыт» несостоятелен по двум независимым причинам:

  • прогона может не быть. Из последних 15 прогонов yandex_detail_backfill десять обогатили ноль строк (обрыв по max_consecutive_blocks за 0 секунд), один упал, и лишь четыре дали строки (492 / 134 / 74 / 10). Шанс, что когорта наберётся, — примерно один к трём;
  • даже полная когорта не различит гипотезы. Максимум прогона 492 строки; при оценке 1% это ~5 строк ≥1000, при 2.5% — ~12. Доверительный интервал на 7 из 492 идёт примерно от 0.6% до 3% и накрывает оба.

Переписываю критерий: накопительная когорта не менее 1500 строк либо срок 2026-08-12, что раньше, с порогом, который отличает 1% от 2.5%. И в COMMENT ON COLUMN — «испорченных единицы-первые десятки строк из 1620, точнее сказать нельзя», а не конкретное число.

## Поправка: «примерно 330 испорченных строк» — моё число, и оно завышено вдесятеро Я вывел его переносом доли Авито (20.3% значений ≥1000) на 1620 строк Яндекса. Три независимые проверки это опровергли, и опровергли **измерением в той же таблице**, а не рассуждением. ### Почему перенос доли не работает Остаток от срезания — последние три цифры, и я молча считал их равномерными на [0, 999]. Это проверяемо прямо в базе. Доля значений с остатком ≥300 среди **реальных** счётчиков ≥1000: | площадка | доля | из скольких | |---|---|---| | avito | **61.7%** | 1550 из 2512 | | domklik | **45.9%** | из 172 | Равномерности нет и быть не может: просмотры распределены степенн**о**, значения ≥1000 жмутся к самой тысяче, поэтому остаток смещён к нулю. Чем легче хвост площадки, тем ниже доля. Хвост Яндекса легче обоих — p99 равен 280 против 1483 у Домклика и 8401 у Авито, — значит его доля **не выше 46%**. Наблюдаемых значений в диапазоне 300–999 на проде — **15**. Отсюда: * верхняя граница испорченных: 15 / 0.46 ≈ **33 строки**, с пуассоновским шумом до **~40** — то есть около **2.5%**, а не 20%; * точечная оценка: избыток над честным хвостом ≈ 9 → **~20 строк**. Направление вывода это не меняет (десятки, а не сотни), но моё число было неверным, а неверным в удобную сторону — оно делало находку крупнее, чем она есть. ### Второе, важнее числа: диагноз нуля я поставил неправильно Я написал «ноль из 1620 при 20.3% у соседа — подпись дефекта». Формулировка небрежна, и вот в чём. Под старой регуляркой значение ≥1000 с разделителем разрядов **невозможно было записать вообще**: `1 234` → 234, `12 345` → 345 (прогнано на настоящем образце, `\s` в Python матчит неразрывный пробел). То есть ноль в корзине «≥1000» **механически гарантирован парсером** и не несёт никакой информации о площадке. Это не «неприменимо» и не «подпись дефекта» — это **«заблокировано выше по потоку»**. Третий вид нуля, и я его смешал с первым. Отдельно круговой довод, которым это едва не подпёрли: «у Яндекса p99 = 279, площадка просто не выдаёт четырёхзначных счётчиков». Перцентиль посчитан **на тех самых данных, которые считаются испорченными** — срезанные строки лежат как маленькие числа и сами придавливают его. Честная формулировка: **доказательство порчи не получено ни в одну сторону.** Механизм реален и проверен, масштаб — десятки строк, но контрольной группы, разобранной уже исправленным выражением, на момент замера не существовало. ### Что устояло и даже усилилось Решение **не трогать колонку** подтверждено, и сильнее, чем я говорил: читателей ноль не только в коде, но и в каталоге прода — ноль совпадений в `pg_views`, `pg_matviews`, `pg_get_functiondef`. Единственная ссылка живёт в `LISTING_FIELD_PRIORITY`, её резолвер зовётся только из тестов, а единственная экспортируемая наружу функция `update_canonical_fields` — **заглушка `pass`**, которую никто не вызывает. Вся таблица приоритетов инертна. Восстановить старые значения нечем: `raw_payload` у всех 1620 — это SERP-JSON без поля просмотров, HTML детальной страницы не хранится. ### План проверки тоже не годился Критерий «после ближайшего прогона 08.08 вопрос закрыт» несостоятелен по двум независимым причинам: * **прогона может не быть.** Из последних 15 прогонов `yandex_detail_backfill` **десять обогатили ноль строк** (обрыв по `max_consecutive_blocks` за 0 секунд), один упал, и лишь четыре дали строки (492 / 134 / 74 / 10). Шанс, что когорта наберётся, — примерно один к трём; * **даже полная когорта не различит гипотезы.** Максимум прогона 492 строки; при оценке 1% это ~5 строк ≥1000, при 2.5% — ~12. Доверительный интервал на 7 из 492 идёт примерно от 0.6% до 3% и накрывает оба. Переписываю критерий: **накопительная когорта не менее 1500 строк либо срок 2026-08-12, что раньше**, с порогом, который отличает 1% от 2.5%. И в `COMMENT ON COLUMN` — «испорченных единицы-первые десятки строк из 1620, точнее сказать нельзя», а не конкретное число.
Author
Collaborator

Вердикт 12.08 — оба критерия разрешены, закрываю

Срок годности вердикта по Яндексу истекал сегодня. «Не проверено» снимаю.

Яндекс: когорта набралась, и гипотеза порчи ОПРОВЕРГНУТА контрольной группой

Критерий от 07.08: «накопительная когорта не менее 1500 строк либо срок 2026-08-12, что раньше». Когорта набралась — считать по сроку не пришлось:

строки с detail_enriched_at > 2026-08-07 08:51:22 (момент деплоя #2773):
  обогащено ............... 2347
  из них с просмотрами .... 2050
  из них >= 1000 .......... 0
  максимум ................ 516

2050 строк, разобранных уже исправленным выражением, — это ровно та контрольная группа, которой на 07.08 не существовало. Ни одного четырёхзначного счётчика.

Это опровергает моё же число. При доле испорченных 1% в когорте 2050 ожидалось бы ~20 строк ≥1000, при 2.5% — ~51. Наблюдается 0, верхняя 95%-граница ≈ 0.15%. Значит объявлений с 1000+ просмотров у Яндекса в нашем пуле практически нет, и старый ноль в корзине «≥1000» не был подписью дефекта — он совпадал с правдой площадки.

Опасение «прогона может не быть» тоже не подтвердилось: шесть прогонов yandex_detail_backfill с 07.08, все done, enriched 492 / 414 / 460 / 451 / 482 / 540.

Правка при этом верна и остаётся: механизм срезания реален и был воспроизведён на сохранённом образце. Цена его на исторических данных — единицы строк, а не десятки. Формулировку в COMMENT ON COLUMN («испорченных единицы-первые десятки») стоит при случае поправить в сторону «скорее ноль», но держать ради этого задачу открытой не за чем.

Общая картина Яндекса сегодня: 3670 непустых, из них ≥1000 — 0, в диапазоне 300-999 — 32, max 947 (старая строка).

Циан: блокировка снялась сама, и правка подтверждена ДАННЫМИ

07.08 я записал: ближайший прогон 08.08 03:05 «даст ноль, пока не восстановлены куки». Он дал не ноль. cian_city_sweep 3418 (08.08 03:05, done) обогатил 50 из 50, самая ранняя строка — detail_enriched_at = 2026-08-08 03:09:16.

То есть 403-стена detail-пути Циана перестала стоять уже на следующую ночь, а задача провисела ещё четверо суток с диагнозом «заблокировано выше по потоку», который к тому моменту был неверен. Куки действительно протухли — но они блокируют cian_history_backfill (он и стоял skipped до 10.08), а не detail внутри свипа. Мой диагноз склеил два разных пути и приписал общему нулю одну причину.

Критерий (записан 07.08 ДО факта): на первый прогон со status='done' доля непустых views_total среди обработанных ≥ 90% при batch_size 50. Первый такой прогон — 3418:

обработано ...... 50
с просмотрами ... 45  = 90.0%   (порог 90%)
min 1 / max 510

Ровно на пороге, но критерий записан до факта и выполнен.

Совокупно с момента деплоя правки: views_total у Циана 0 из 21 951 → 434 из 22 889; среди 466 обогащённых после 07.08 08:51 доля 93.1%, max 7041, 14 значений ≥1000. Четырёхзначное значение — прямое доказательство, что int("146 просмотров, 8 за сегодня") больше не выполняется: старый код такого записать не мог физически.

Честная оговорка: доля по прогонам гуляет — 100% (10/10) на малых городских батчах, 96.2% на 3669, но 70.0% на 3743 (12.08, 35 из 50). Устойчиво ≥90% она не держится. Похоже на отсутствие блока статистики у части карточек, а не на отказ разбора; отдельную задачу не завожу, потому что колонку по-прежнему никто не читает.

Что остаётся в силе

Решение «оставить и чинить, а не удалять» — с прежней оговоркой: потребителей views_total в коде ноль. Срок годности решения — следующий пересмотр оценщика.

## Вердикт 12.08 — оба критерия разрешены, закрываю Срок годности вердикта по Яндексу истекал сегодня. «Не проверено» снимаю. ### Яндекс: когорта набралась, и гипотеза порчи ОПРОВЕРГНУТА контрольной группой Критерий от 07.08: «накопительная когорта не менее 1500 строк либо срок 2026-08-12, что раньше». Когорта набралась — считать по сроку не пришлось: ``` строки с detail_enriched_at > 2026-08-07 08:51:22 (момент деплоя #2773): обогащено ............... 2347 из них с просмотрами .... 2050 из них >= 1000 .......... 0 максимум ................ 516 ``` 2050 строк, разобранных **уже исправленным** выражением, — это ровно та контрольная группа, которой на 07.08 не существовало. Ни одного четырёхзначного счётчика. Это опровергает моё же число. При доле испорченных 1% в когорте 2050 ожидалось бы ~20 строк ≥1000, при 2.5% — ~51. Наблюдается 0, верхняя 95%-граница ≈ 0.15%. Значит объявлений с 1000+ просмотров у Яндекса в нашем пуле практически нет, и старый ноль в корзине «≥1000» **не был подписью дефекта** — он совпадал с правдой площадки. Опасение «прогона может не быть» тоже не подтвердилось: шесть прогонов `yandex_detail_backfill` с 07.08, все `done`, enriched 492 / 414 / 460 / 451 / 482 / 540. Правка при этом верна и остаётся: механизм срезания реален и был воспроизведён на сохранённом образце. Цена его на исторических данных — единицы строк, а не десятки. Формулировку в `COMMENT ON COLUMN` («испорченных единицы-первые десятки») стоит при случае поправить в сторону «скорее ноль», но держать ради этого задачу открытой не за чем. Общая картина Яндекса сегодня: 3670 непустых, из них ≥1000 — 0, в диапазоне 300-999 — 32, max 947 (старая строка). ### Циан: блокировка снялась сама, и правка подтверждена ДАННЫМИ 07.08 я записал: ближайший прогон 08.08 03:05 «даст ноль, пока не восстановлены куки». **Он дал не ноль.** `cian_city_sweep` 3418 (08.08 03:05, `done`) обогатил 50 из 50, самая ранняя строка — `detail_enriched_at = 2026-08-08 03:09:16`. То есть 403-стена detail-пути Циана перестала стоять уже на следующую ночь, а задача провисела ещё четверо суток с диагнозом «заблокировано выше по потоку», который к тому моменту был неверен. Куки действительно протухли — но они блокируют `cian_history_backfill` (он и стоял `skipped` до 10.08), а не detail внутри свипа. Мой диагноз склеил два разных пути и приписал общему нулю одну причину. Критерий (записан 07.08 ДО факта): на первый прогон со `status='done'` доля непустых `views_total` среди обработанных ≥ 90% при batch_size 50. Первый такой прогон — **3418**: ``` обработано ...... 50 с просмотрами ... 45 = 90.0% (порог 90%) min 1 / max 510 ``` Ровно на пороге, но критерий записан до факта и выполнен. Совокупно с момента деплоя правки: `views_total` у Циана **0 из 21 951 → 434 из 22 889**; среди 466 обогащённых после 07.08 08:51 доля **93.1%**, **max 7041**, 14 значений ≥1000. Четырёхзначное значение — прямое доказательство, что `int("146 просмотров, 8 за сегодня")` больше не выполняется: старый код такого записать не мог физически. Честная оговорка: доля по прогонам гуляет — 100% (10/10) на малых городских батчах, 96.2% на 3669, но **70.0% на 3743** (12.08, 35 из 50). Устойчиво ≥90% она не держится. Похоже на отсутствие блока статистики у части карточек, а не на отказ разбора; отдельную задачу не завожу, потому что колонку по-прежнему никто не читает. ### Что остаётся в силе Решение «оставить и чинить, а не удалять» — с прежней оговоркой: потребителей `views_total` в коде ноль. Срок годности решения — следующий пересмотр оценщика.
Author
Collaborator

Контрольная группа ответила: испорченных строк было ~ноль, а не десятки

Критерий был «когорта ≥1500 строк, разобранных исправленным выражением». Набралась: 1709 строк с просмотрами, обогащённых после 09.08 08:51. Результат:

когорта с просмотрами из них ≥1000 максимум 99-й перцентиль
после фикса (исправленное выражение) 1709 0 516 282
до фикса (сломанное выражение) 1961 0 947

Исправленное выражение, разобрав почти две тысячи страниц, не нашло ни одного четырёхзначного счётчика. Значит старый ноль в корзине «≥1000» совпадал с правдой площадки, а не был подписью дефекта.

Три моих оценки подряд, и все завышены

когда оценка испорченных строк чем получена
09.08 ≈330 перенос доли Авито (20.3%) на 1620 строк Яндекса
09.08, поправка 33–40 измеренная доля остатков ≥300 у соседей вместо равномерности
12.08, факт ≈0 (верхняя граница 0.15%) контрольная группа на исправленном выражении

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

Что это говорит о самом приёме

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

Это стоит записать отдельно от прежнего вывода: контрольная группа отвечает на «есть ли дефект», и не отвечает на «сколько строк он испортил». Второе даёт только прогон починенного кода на новых данных.

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

Циан ожил, и там дефект был настоящий

было (07.08) стало
строк с просмотрами 0 из 21 951 444
максимум 7041
значений ≥1000 0 14

Вот у Циана четырёхзначные счётчики есть, и они пошли. То есть ноль у Циана был настоящей пустотой из-за блокировки detail-страниц, а не свойством площадки.

И вердикт «заблокировано куками» протух через сутки после записи

07.08 я записал: «Циан не проверяется, пока не перезальют куки; критерий условный». Прогон 3418 от 08.08 03:05 обогатил 50 из 50 — куки не понадобились.

Диагноз склеил два разных пути: куки блокируют исторический бэкфилл, а detail-страницы внутри свипа авторизации не требуют. Из-за этой склейки задача провисела ещё четверо суток с неверной причиной.

Закрываю.

## Контрольная группа ответила: испорченных строк было ~ноль, а не десятки Критерий был «когорта ≥1500 строк, разобранных исправленным выражением». Набралась: **1709 строк с просмотрами**, обогащённых после 09.08 08:51. Результат: | когорта | с просмотрами | из них ≥1000 | максимум | 99-й перцентиль | |---|---:|---:|---:|---:| | **после фикса** (исправленное выражение) | **1709** | **0** | **516** | 282 | | до фикса (сломанное выражение) | 1961 | 0 | 947 | — | Исправленное выражение, разобрав почти две тысячи страниц, **не нашло ни одного четырёхзначного счётчика**. Значит старый ноль в корзине «≥1000» совпадал с правдой площадки, а не был подписью дефекта. ### Три моих оценки подряд, и все завышены | когда | оценка испорченных строк | чем получена | |---|---|---| | 09.08 | **≈330** | перенос доли Авито (20.3%) на 1620 строк Яндекса | | 09.08, поправка | **33–40** | измеренная доля остатков ≥300 у соседей вместо равномерности | | **12.08, факт** | **≈0** (верхняя граница 0.15%) | контрольная группа на исправленном выражении | Первую поправку я сделал правильным приёмом и получил число втрое ближе к истине — но всё ещё завышенное впятеро. Правильным был только третий способ: **дать исправленному коду отработать и посмотреть, что он находит**. ### Что это говорит о самом приёме Сравнение распределений с соседней площадкой **сработало как детектор**: механизм срезания разрядов был реален, я его нашёл и он был починен. Но оценить **масштаб** тем же приёмом не вышло ни разу — ни переносом доли, ни поправкой на форму хвоста. Это стоит записать отдельно от прежнего вывода: контрольная группа отвечает на «есть ли дефект», и не отвечает на «сколько строк он испортил». Второе даёт только прогон починенного кода на новых данных. Практически: правка не была лишней (выражение действительно теряло разряды и починено верно), но **ценность её близка к нулю**, и говорить о ней как о находке масштаба «десятки испорченных чисел» было неправильно. ## Циан ожил, и там дефект был настоящий | | было (07.08) | стало | |---|---:|---:| | строк с просмотрами | **0** из 21 951 | **444** | | максимум | — | **7041** | | значений ≥1000 | 0 | **14** | Вот у Циана четырёхзначные счётчики есть, и они пошли. То есть ноль у Циана был настоящей пустотой из-за блокировки detail-страниц, а не свойством площадки. ### И вердикт «заблокировано куками» протух через сутки после записи 07.08 я записал: «Циан не проверяется, пока не перезальют куки; критерий условный». Прогон 3418 от **08.08 03:05** обогатил **50 из 50** — куки не понадобились. Диагноз склеил два разных пути: куки блокируют исторический бэкфилл, а detail-страницы внутри свипа авторизации не требуют. Из-за этой склейки задача провисела ещё четверо суток с неверной причиной. Закрываю.
Sign in to join this conversation.
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#2669
No description provided.