Авито: распространить существующее хранилище сессий (как у Циана/Домклика) + липкость к exit-IP #3179

Open
opened 2026-08-28 16:57:51 +00:00 by bot-backend · 3 comments
Collaborator

Sub-issue #3177, Foundation — после honest ban_kind, до правки воркеров.

Что строим

Хранение сессии источника между прогонами. Сейчас его нет вовсе: каждый прогон стартует с чистого браузера, антибот-куки набираются заново или не набираются совсем.

Схема

Таблица scraper_sessions (или эквивалент):

поле смысл
source avito / domclick
account_label метка учётки; сами креды только в вольте, в БД — метка
cookies jsonb, полный набор с CDP Network.getAllCookies
proxy_id закреплённый узел пула
last_ok_at, blocks_since_ok, status здоровье сессии

Главная ловушка

Cookie авторизации — HttpOnly, в document.cookie её нет. Замер 28.08: через JS видны только buyer_location_id / buyer_from_page; антибот-группа sx / __zzatw-avito / f / cfidsw-avito и авторизация приходят только из CDP. Снимать и восстанавливать сессию нужно на уровне CDP, иначе перенесётся мусор без авторизации, и это будет выглядеть как «аккаунты не помогают».

Липкость

Один account_label всегда ходит через один proxy_id. Смена exit-IP под одной учёткой — быстрый путь к бану аккаунта. Текущая аренда прокси привязана к run_id и прогон не переживает.

Границы

Только хранилище и его API. Использование в воркерах — следующий sub-issue. Логин руками, автоматизация 2FA не в этом заходе.

Приёмка

  • Сессия переживает рестарт контейнера
  • Восстановленная сессия открывает карточку, которая анонимно даёт 439
  • Один аккаунт — один exit-IP, проверяется в тесте

Оценка M. Refs #3177

Sub-issue #3177, **Foundation** — после honest ban_kind, до правки воркеров. ## Что строим Хранение сессии источника между прогонами. Сейчас его нет вовсе: каждый прогон стартует с чистого браузера, антибот-куки набираются заново или не набираются совсем. ## Схема Таблица `scraper_sessions` (или эквивалент): | поле | смысл | |---|---| | `source` | `avito` / `domclick` | | `account_label` | метка учётки; сами креды **только в вольте**, в БД — метка | | `cookies` | jsonb, **полный набор с CDP `Network.getAllCookies`** | | `proxy_id` | закреплённый узел пула | | `last_ok_at`, `blocks_since_ok`, `status` | здоровье сессии | ## Главная ловушка **Cookie авторизации — `HttpOnly`, в `document.cookie` её нет.** Замер 28.08: через JS видны только `buyer_location_id` / `buyer_from_page`; антибот-группа `sx / __zzatw-avito / f / cfidsw-avito` и авторизация приходят только из CDP. Снимать и восстанавливать сессию нужно на уровне CDP, иначе перенесётся мусор без авторизации, и это будет выглядеть как «аккаунты не помогают». ## Липкость Один `account_label` всегда ходит через один `proxy_id`. Смена exit-IP под одной учёткой — быстрый путь к бану аккаунта. Текущая аренда прокси привязана к `run_id` и прогон не переживает. ## Границы Только хранилище и его API. Использование в воркерах — следующий sub-issue. Логин руками, автоматизация 2FA не в этом заходе. ## Приёмка - [ ] Сессия переживает рестарт контейнера - [ ] Восстановленная сессия открывает карточку, которая анонимно даёт 439 - [ ] Один аккаунт — один exit-IP, проверяется в тесте Оценка M. Refs #3177
bot-backend added the
enhancement
priority/p1
scope/backend
scrapers
tradein
labels 2026-08-28 16:57:51 +00:00
Author
Collaborator

Существенная поправка: хранилище сессий изобретать не надо — оно уже построено и работает в проде. Для Циана и Домклика. Для Авито его просто не сделали.

Проверено на живом проде:

cian_session_cookies      2 записи
domclick_session_cookies  1 запись
avito                     -- нет ничего

Обе таблицы одной формы:

account_user_id / account_cas_id, cookies_encrypted,
expires_at_estimate, uploaded_at, last_used_at, last_invalid_at, notes

backend/app/services/cian_session.py — куки шифруются pgp_sym_encrypt, TTL 30 дней, есть предупреждение за 5 дней до протухания (COOKIE_EXPIRY_WARN_DAYS), фильтр по списку критичных кук (CIAN_REQUIRED_COOKIES), и там явно помечена httpOnly-кука DMIR_AUTH как основной auth-сигнал — то есть проблема с HttpOnly, которую я описывал как главную ловушку, в этом коде уже решена.

Рядом backend/app/services/domclick_session.py — тот же паттерн для второго источника.

И, что важнее всего, браузерный авто-логин уже реализован: POST /scrape/cian/auto-login (backend/app/api/v1/admin.py:484) гоняет вход через сайдкар (fetcher.login(...) с селекторами формы, проверкой success_cookie), забирает куки, фильтрует и кладёт в хранилище через cian_session_svc.save_session(...). Конфиг флоу — config.py:1067-1084 (#639, «провалидировано вживую 2026-05-31: email+пароль, без SMS/капчи»).

Что это меняет в задаче

Задача из «спроектировать и построить хранилище сессий» превращается в «распространить существующий паттерн на Авито». Это заметно меньше и заметно безопаснее: форма таблицы, шифрование, TTL, предупреждение о протухании, фильтр кук и браузерный логин уже написаны и обкатаны на двух источниках.

Переписанный объём:

  1. avito_session_cookies по образцу двух существующих (ключ — метка аккаунта; креды остаются в вольте).
  2. backend/app/services/avito_session.py по образцу cian_session.py; AVITO_REQUIRED_COOKIES = sx, __zzatw-avito, f, cfidsw-avito плюс авторизационная (уточнить имя по CDP-дампу — через JS она не видна).
  3. POST /scrape/avito/auto-login по образцу циановского; селекторы формы Авито в конфиг, success_cookie — авторизационная.
  4. Липкость к exit-IP — единственная действительно новая часть. У Циана и Домклика её нет; аренда прокси привязана к прогону. Нужно поле с закреплённым узлом в таблице сессий и учёт его при аренде.

Разобранные ложные следы

  • IDENTITY_STORE=auth в env скраппера к скрапперам отношения не имеет — это реестр людей и сессий продукта (app/services/identity_store.py, api/v1/auth.py, team.py). Проверил прежде чем строить на нём; чуть не принял за готовый store.
  • YANDEX_COOKIES_FILE=/app/yandex_cookies.jsonмёртвая конфигурация: файла в контейнере нет, и переменную не читает никто, кроме объявления в config.py:1065. Не образец для подражания.

Отдельная находка: флаг с противоположными значениями в разных контейнерах

tradein-scraper   AVITO_DETAIL_BACKFILL_USE_CURL=false   (браузер)
tradein-backend   AVITO_DETAIL_BACKFILL_USE_CURL=true    (curl)
tradein-browser   AVITO_DETAIL_BACKFILL_USE_CURL=true

Прогоны сейчас идут в браузерном режиме — это видно из их же error_text: firewall/soft-block (browser-mode). То есть задачу исполняет tradein-scraper. Но один и тот же флаг с противоположными значениями в двух контейнерах, каждый из которых может исполнять задачи, — мина: перенос задачи на другой воркер молча меняет режим сбора. Завести отдельным тикетом.

**Существенная поправка: хранилище сессий изобретать не надо — оно уже построено и работает в проде. Для Циана и Домклика. Для Авито его просто не сделали.** Проверено на живом проде: ``` cian_session_cookies 2 записи domclick_session_cookies 1 запись avito -- нет ничего ``` Обе таблицы одной формы: ``` account_user_id / account_cas_id, cookies_encrypted, expires_at_estimate, uploaded_at, last_used_at, last_invalid_at, notes ``` `backend/app/services/cian_session.py` — куки шифруются `pgp_sym_encrypt`, TTL 30 дней, есть предупреждение за 5 дней до протухания (`COOKIE_EXPIRY_WARN_DAYS`), фильтр по списку критичных кук (`CIAN_REQUIRED_COOKIES`), и там **явно помечена `httpOnly`-кука** `DMIR_AUTH` как основной auth-сигнал — то есть проблема с `HttpOnly`, которую я описывал как главную ловушку, в этом коде уже решена. Рядом `backend/app/services/domclick_session.py` — тот же паттерн для второго источника. И, что важнее всего, **браузерный авто-логин уже реализован**: `POST /scrape/cian/auto-login` (`backend/app/api/v1/admin.py:484`) гоняет вход через сайдкар (`fetcher.login(...)` с селекторами формы, проверкой `success_cookie`), забирает куки, фильтрует и кладёт в хранилище через `cian_session_svc.save_session(...)`. Конфиг флоу — `config.py:1067-1084` (#639, «провалидировано вживую 2026-05-31: email+пароль, без SMS/капчи»). ## Что это меняет в задаче Задача из «спроектировать и построить хранилище сессий» превращается в **«распространить существующий паттерн на Авито»**. Это заметно меньше и заметно безопаснее: форма таблицы, шифрование, TTL, предупреждение о протухании, фильтр кук и браузерный логин уже написаны и обкатаны на двух источниках. Переписанный объём: 1. `avito_session_cookies` по образцу двух существующих (ключ — метка аккаунта; креды остаются в вольте). 2. `backend/app/services/avito_session.py` по образцу `cian_session.py`; `AVITO_REQUIRED_COOKIES` = `sx`, `__zzatw-avito`, `f`, `cfidsw-avito` плюс авторизационная (уточнить имя по CDP-дампу — через JS она не видна). 3. `POST /scrape/avito/auto-login` по образцу циановского; селекторы формы Авито в конфиг, `success_cookie` — авторизационная. 4. Липкость к exit-IP — **единственная действительно новая часть**. У Циана и Домклика её нет; аренда прокси привязана к прогону. Нужно поле с закреплённым узлом в таблице сессий и учёт его при аренде. ## Разобранные ложные следы - `IDENTITY_STORE=auth` в env скраппера **к скрапперам отношения не имеет** — это реестр людей и сессий продукта (`app/services/identity_store.py`, `api/v1/auth.py`, `team.py`). Проверил прежде чем строить на нём; чуть не принял за готовый store. - `YANDEX_COOKIES_FILE=/app/yandex_cookies.json` — **мёртвая конфигурация**: файла в контейнере нет, и переменную не читает никто, кроме объявления в `config.py:1065`. Не образец для подражания. ## Отдельная находка: флаг с противоположными значениями в разных контейнерах ``` tradein-scraper AVITO_DETAIL_BACKFILL_USE_CURL=false (браузер) tradein-backend AVITO_DETAIL_BACKFILL_USE_CURL=true (curl) tradein-browser AVITO_DETAIL_BACKFILL_USE_CURL=true ``` Прогоны сейчас идут в браузерном режиме — это видно из их же `error_text`: `firewall/soft-block (browser-mode)`. То есть задачу исполняет `tradein-scraper`. Но один и тот же флаг с противоположными значениями в двух контейнерах, каждый из которых может исполнять задачи, — мина: перенос задачи на другой воркер молча меняет режим сбора. Завести отдельным тикетом.
bot-backend changed title from Авито: хранилище авторизованных сессий (куки включая HttpOnly) + привязка к exit-IP to Авито: распространить существующее хранилище сессий (как у Циана/Домклика) + липкость к exit-IP 2026-08-28 17:05:49 +00:00
Author
Collaborator

План после разбора со скептиками опубликован в эпике: #3177 (comment)

Твоя часть — шаги 1-5 (инфраструктура):

  1. Миграция 274 + kit-модуль providers/avito/session.py (образец — providers/cian/session.py, читает хранилище прямо из kit)
  2. Двусторонний контракт кук в сайдкаре — новый шаг, в исходной постановке его не было. Вход /fetch фабрикует домен из URL (server.py:1087-1094), из-за чего .avito.ru мутирует в .www.avito.ru; выход кук нет вовсе — page.close() (:1118) теряет обновлённые. Без этого сессия одноразовая, а критерий прогрева ложно краснеет.
  3. Пин прокси, живущий весь прогон. Ключевая поправка скептика: browser_fetcher ре-акquire'ит лизу после N провалов (:613-637), то есть пин рвался бы ровно в момент блокировок — в единственном сценарии, ради которого заводится.
  4. Лиза сессии (single-flight): три места конструируют BrowserFetcher(source="avito") (pipeline.py:718,1248,2057), а уникальный индекс на pinned_proxy_id держит одну.
  5. Админ-путь заливки и статуса (образцы — admin.py:427 cian, :604 domclick).

Шаги 1-2 и 3-5 разделяются на два PR без взаимной блокировки.

Существенная поправка к схеме: account_label сделан nullable. Прогрев производит анонимные антибот-куки, авторизации в них нет — при NOT NULL шаг с прогревом был бы несовместим с таблицей. Дефолтный режим — анонимная прогретая сессия, аккаунт становится опциональной надстройкой.

План после разбора со скептиками опубликован в эпике: https://git.gendsgn.ru/lekss361/gendesign/issues/3177#issuecomment-29085 Твоя часть — шаги 1-5 (инфраструктура): 1. Миграция 274 + kit-модуль `providers/avito/session.py` (образец — `providers/cian/session.py`, читает хранилище прямо из kit) 2. **Двусторонний контракт кук в сайдкаре** — новый шаг, в исходной постановке его не было. Вход `/fetch` фабрикует домен из URL (`server.py:1087-1094`), из-за чего `.avito.ru` мутирует в `.www.avito.ru`; выход кук нет вовсе — `page.close()` (`:1118`) теряет обновлённые. Без этого сессия одноразовая, а критерий прогрева ложно краснеет. 3. Пин прокси, живущий **весь прогон**. Ключевая поправка скептика: `browser_fetcher` ре-акquire'ит лизу после N провалов (`:613-637`), то есть пин рвался бы ровно в момент блокировок — в единственном сценарии, ради которого заводится. 4. Лиза сессии (single-flight): три места конструируют `BrowserFetcher(source="avito")` (`pipeline.py:718,1248,2057`), а уникальный индекс на `pinned_proxy_id` держит одну. 5. Админ-путь заливки и статуса (образцы — `admin.py:427` cian, `:604` domclick). Шаги 1-2 и 3-5 разделяются на два PR без взаимной блокировки. **Существенная поправка к схеме:** `account_label` сделан nullable. Прогрев производит **анонимные** антибот-куки, авторизации в них нет — при `NOT NULL` шаг с прогревом был бы несовместим с таблицей. Дефолтный режим — анонимная прогретая сессия, аккаунт становится опциональной надстройкой.
Author
Collaborator

Инвентарь того, что придётся консолидировать (замер на проде 28.08)

Пер-источниковых хранилищ уже три, и все живут по разным схемам. Это и есть то, что здесь предлагается свести в одну scraper_sessions.

Таблица Миграция Ключ записи Формат кук Строк на проде Живых Последняя заливка
cian_session_cookies 027 account_user_id (число) плоский dict[name→value] 3 3 2026-08-28
domclick_session_cookies 174 account_cas_id (число) плоский dict[name→value] 2 2 2026-08-28
yandex_session_cookies 274 account_label (text) полные CDP-объекты 0 0

Расхождения, которые миграция обязана снять:

  1. Тип ключа. Циан/Домклик держат числовой id учётки, Яндекс — текстовую метку (у Яндекса числового id в дампе просто нет). Схема из описания задачи (source + account_label) закрывает оба случая, но переносу 5 живых строк нужен маппинг id → label.
  2. Формат кук. Циан/Домклик хранят dict[name→value], потеряв domain/path/httpOnly. Яндекс хранит полный CDP-объект. Целевой формат в описании — Network.getAllCookies, то есть яндексовый; плоские записи Циана/Домклика не восстановимы обратно в полный вид, при переносе им придётся достраивать domain из источника, как это сейчас делает сайдкар (browser/server.py, инъекция кук).
  3. Фильтр на сохранение. Циан/Домклик режут payload по allowlist известных имён, Яндекс сохраняет всё, кроме аналитики. Единое хранилище должно выбрать одно правило — иначе перенос молча потеряет часть кук у двух источников из трёх.

Про yandex_session_cookies (274)

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

Таблица пуста и никем не читается. Отдельный DROP заводить не стал — правильное место для него здесь: это ровно та консолидация, ради которой задача и создана. Предлагаю в приёмку добавить пункт «после переноса пер-источниковые таблицы 027/174/274 удалены одной миграцией».

Ссылка на разбор: #3192. Смежное: #3194 (ключ шифрования и сами куки уезжают в GlitchTip через bind-параметры SQLAlchemy) — чинить до того, как единое хранилище начнёт обслуживать все источники, иначе радиус утечки станет шире.

## Инвентарь того, что придётся консолидировать (замер на проде 28.08) Пер-источниковых хранилищ уже три, и все живут по разным схемам. Это и есть то, что здесь предлагается свести в одну `scraper_sessions`. | Таблица | Миграция | Ключ записи | Формат кук | Строк на проде | Живых | Последняя заливка | |---|---|---|---|---|---|---| | `cian_session_cookies` | 027 | `account_user_id` (число) | плоский `dict[name→value]` | 3 | 3 | 2026-08-28 | | `domclick_session_cookies` | 174 | `account_cas_id` (число) | плоский `dict[name→value]` | 2 | 2 | 2026-08-28 | | `yandex_session_cookies` | 274 | `account_label` (text) | **полные CDP-объекты** | 0 | 0 | — | Расхождения, которые миграция обязана снять: 1. **Тип ключа.** Циан/Домклик держат числовой id учётки, Яндекс — текстовую метку (у Яндекса числового id в дампе просто нет). Схема из описания задачи (`source` + `account_label`) закрывает оба случая, но переносу 5 живых строк нужен маппинг `id → label`. 2. **Формат кук.** Циан/Домклик хранят `dict[name→value]`, потеряв `domain`/`path`/`httpOnly`. Яндекс хранит полный CDP-объект. Целевой формат в описании — `Network.getAllCookies`, то есть яндексовый; плоские записи Циана/Домклика **не восстановимы обратно** в полный вид, при переносе им придётся достраивать `domain` из источника, как это сейчас делает сайдкар (`browser/server.py`, инъекция кук). 3. **Фильтр на сохранение.** Циан/Домклик режут payload по allowlist известных имён, Яндекс сохраняет всё, кроме аналитики. Единое хранилище должно выбрать одно правило — иначе перенос молча потеряет часть кук у двух источников из трёх. ## Про `yandex_session_cookies` (274) Таблица создана в #3195 под гипотезу «авторизованная сессия раскрывает контакты продавца». Гипотеза **опровергнута** (замер в #3192): куки не дают по объявлению ничего, а то, что я принял за контакты, оказалось историей просмотров нашей же учётки. Таблица пуста и никем не читается. Отдельный `DROP` заводить не стал — правильное место для него здесь: это ровно та консолидация, ради которой задача и создана. Предлагаю в приёмку добавить пункт «после переноса пер-источниковые таблицы 027/174/274 удалены одной миграцией». Ссылка на разбор: #3192. Смежное: #3194 (ключ шифрования и сами куки уезжают в GlitchTip через bind-параметры SQLAlchemy) — чинить до того, как единое хранилище начнёт обслуживать все источники, иначе радиус утечки станет шире.
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#3179
No description provided.