[tradein][scrapers] Phase 0 — BrowserFetcher (camoufox) abstraction + Docker (#883) #884

Closed
opened 2026-05-31 10:37:55 +00:00 by lekss361 · 5 comments
Owner

Phase 0 эпика #883. Foundation для миграции на camoufox. Provider-agnostic — не трогает avito/cian-логику, только инфра+абстракция.

Задача

Ввести BrowserFetcher (camoufox-обёртка) с lifecycle-управлением + завести Docker/deploy под Firefox-build. Парсеры не трогаем.

1. Зависимость

  • pyproject.toml: добавить camoufox[geoip] (проверено: 0.x работает; тянет Firefox-build ~960МБ через python -m camoufox fetch).
  • uv lock.

2. Новый модуль tradein-mvp/backend/app/services/scrapers/browser_fetcher.py

class BrowserFetcher:
    """camoufox-обёртка: async-навигация + recycle + relaunch-on-crash."""
    async def __aenter__/__aexit__   # запускает/гасит AsyncCamoufox
    async def fetch(self, url: str) -> str   # new_page→goto→wait→content→close; вернуть HTML
  • Конфиг camoufox: headless=True, os="windows", locale="ru-RU", geoip=True, humanize=True, proxy=_proxy_dict().
  • _proxy_dict() — парсит settings.scraper_proxy_url (http://user:pass@host:port) в {"server":"http://host:port","username":..,"password":..}.
  • Recycle: счётчик страниц; каждые settings.browser_recycle_pages (=15) закрыть+перезапустить browser (память bounded — пик ~1.9ГБ иначе).
  • Crash-recovery: ловить playwright._impl._errors.TargetClosedError (+ generic) в fetch → relaunch browser → один retry. Verified: интермиттентный TargetClosedError на длинных прогонах.
  • fetch параметры из settings: browser_nav_timeout_ms (=60000), browser_wait_ms (=2500, JS-гидрация).
  • (опц.) soft-block detect: если HTML < 150КБ ИЛИ нет __preloadedState__ ИЛИ «Доступ ограничен» → rotate IP (settings.avito_proxy_rotate_url GET) + retry. С camoufox редко → можно отложить.

3. Config (app/core/config.py)

  • browser_recycle_pages: int = 15
  • browser_nav_timeout_ms: int = 60000
  • browser_wait_ms: int = 2500
  • scraper_fetch_mode: Literal["curl_cffi","browser"] = "curl_cffi" — флаг для поэтапного перевода (Phase 1/2 переключают per-source).

4. Docker (tradein-mvp/backend/Dockerfile)

  • Установить camoufox + RUN python -m camoufox fetch (Firefox-build в образ; кэш-слой).
  • Системные libs для headless Firefox на Linux: libgtk-3-0 libasound2 libdbus-glib-1-2 libx11-xcb1 libxtst6 (+ что потребует ldd). camoufox headless без xvfb.
  • docker-compose.prod.yml: mem_limit: 2.5g (или deploy.resources.limits.memory) для tradein-backend.

Definition of Done (бинарно)

  • BrowserFetcher().fetch("https://www.avito.ru/ekaterinburg/kvartiry/prodam") → HTML содержит __preloadedState__ + руб/ (реальная выдача, не stub <150КБ).
  • Recycle: прогон 25+ fetch подряд → не падает, RSS стабилизируется (не растёт монотонно); browser пересоздаётся каждые 15.
  • Crash-recovery: форс-close browser в тесте → следующий fetch relaunch'ит и отдаёт HTML.
  • Docker: образ собирается с camoufox+build; docker run ... python -m camoufox fetch cached; контейнерный fetch avito-SERP → реальные данные.
  • uv run ruff check clean; unit-тест BrowserFetcher (мок camoufox или smoke с scraper_fetch_mode flag).

Не делать

  • НЕ переключать avito/cian на browser здесь (Phase 1/2). Флаг scraper_fetch_mode остаётся curl_cffi по умолчанию.
  • НЕ трогать парсеры (__preloadedState__/selectolax).

Эпик: #883. Дальше: Phase 1 (avito).

> Phase 0 эпика #883. Foundation для миграции на camoufox. Provider-agnostic — не трогает avito/cian-логику, только инфра+абстракция. ## Задача Ввести `BrowserFetcher` (camoufox-обёртка) с lifecycle-управлением + завести Docker/deploy под Firefox-build. Парсеры не трогаем. ## 1. Зависимость - `pyproject.toml`: добавить `camoufox[geoip]` (проверено: 0.x работает; тянет Firefox-build ~960МБ через `python -m camoufox fetch`). - `uv lock`. ## 2. Новый модуль `tradein-mvp/backend/app/services/scrapers/browser_fetcher.py` ```python class BrowserFetcher: """camoufox-обёртка: async-навигация + recycle + relaunch-on-crash.""" async def __aenter__/__aexit__ # запускает/гасит AsyncCamoufox async def fetch(self, url: str) -> str # new_page→goto→wait→content→close; вернуть HTML ``` - Конфиг camoufox: `headless=True, os="windows", locale="ru-RU", geoip=True, humanize=True, proxy=_proxy_dict()`. - `_proxy_dict()` — парсит `settings.scraper_proxy_url` (`http://user:pass@host:port`) в `{"server":"http://host:port","username":..,"password":..}`. - **Recycle:** счётчик страниц; каждые `settings.browser_recycle_pages` (=15) закрыть+перезапустить browser (память bounded — пик ~1.9ГБ иначе). - **Crash-recovery:** ловить `playwright._impl._errors.TargetClosedError` (+ generic) в `fetch` → relaunch browser → **один** retry. Verified: интермиттентный `TargetClosedError` на длинных прогонах. - `fetch` параметры из settings: `browser_nav_timeout_ms` (=60000), `browser_wait_ms` (=2500, JS-гидрация). - (опц.) soft-block detect: если HTML < 150КБ ИЛИ нет `__preloadedState__` ИЛИ «Доступ ограничен» → rotate IP (`settings.avito_proxy_rotate_url` GET) + retry. С camoufox редко → можно отложить. ## 3. Config (`app/core/config.py`) - `browser_recycle_pages: int = 15` - `browser_nav_timeout_ms: int = 60000` - `browser_wait_ms: int = 2500` - `scraper_fetch_mode: Literal["curl_cffi","browser"] = "curl_cffi"` — флаг для поэтапного перевода (Phase 1/2 переключают per-source). ## 4. Docker (`tradein-mvp/backend/Dockerfile`) - Установить camoufox + `RUN python -m camoufox fetch` (Firefox-build в образ; кэш-слой). - Системные libs для headless Firefox на Linux: `libgtk-3-0 libasound2 libdbus-glib-1-2 libx11-xcb1 libxtst6` (+ что потребует `ldd`). camoufox headless без xvfb. - `docker-compose.prod.yml`: `mem_limit: 2.5g` (или `deploy.resources.limits.memory`) для tradein-backend. ## Definition of Done (бинарно) - [ ] `BrowserFetcher().fetch("https://www.avito.ru/ekaterinburg/kvartiry/prodam")` → HTML содержит `__preloadedState__` + `руб`/`₽` (реальная выдача, не stub <150КБ). - [ ] Recycle: прогон 25+ `fetch` подряд → не падает, RSS стабилизируется (не растёт монотонно); browser пересоздаётся каждые 15. - [ ] Crash-recovery: форс-close browser в тесте → следующий `fetch` relaunch'ит и отдаёт HTML. - [ ] Docker: образ собирается с camoufox+build; `docker run ... python -m camoufox fetch` cached; контейнерный fetch avito-SERP → реальные данные. - [ ] `uv run ruff check` clean; unit-тест BrowserFetcher (мок camoufox или smoke с `scraper_fetch_mode` flag). ## Не делать - НЕ переключать avito/cian на browser здесь (Phase 1/2). Флаг `scraper_fetch_mode` остаётся `curl_cffi` по умолчанию. - НЕ трогать парсеры (`__preloadedState__`/selectolax). Эпик: #883. Дальше: Phase 1 (avito).
lekss361 added the
scope/devops
scrapers
scope/backend
status/ready
priority/p1
tradein
labels 2026-05-31 10:37:55 +00:00
Collaborator

⚠️ auto-backend: НЕ беру автономно на demo-eve (демо 2026-06-01 15:00) — нужно human/devops-решение по таймингу.

Несмотря на p1, это тяжёлая правка прод-образа: camoufox[geoip] тянет Firefox-build ~960МБ (python -m camoufox fetch в Dockerfile), + docker-compose.prod.yml mem_limit: 2.5g. Merge → deploy-tradein.yml авто-триггерится (paths tradein-mvp/**) → ребилд прод-образа tradein-backend на кануне демо. Если camoufox fetch (960МБ download) упадёт/затаймаутит на runner'е во время Docker build → деплой падает strict → демо ломается.

Поведенчески безопасно (флаг scraper_fetch_mode=curl_cffi default — скрейперы не меняются), и это foundation эпика #883, НЕ demo-gated (нет milestone). ⇒ ценность post-demo, риск только на demo-eve build.

Рекомендация: держать до после демо, либо human/devops подтверждает, что Docker-ребилд с camoufox можно прогнать сейчас (и желательно verify build на свежем образе ДО merge). Тогда возьму. Phase 1/2 (per-source switch) — тем более post-demo.

⚠️ auto-backend: НЕ беру автономно на demo-eve (демо 2026-06-01 15:00) — нужно human/devops-решение по таймингу. Несмотря на p1, это **тяжёлая правка прод-образа**: `camoufox[geoip]` тянет Firefox-build ~960МБ (`python -m camoufox fetch` в Dockerfile), + `docker-compose.prod.yml mem_limit: 2.5g`. Merge → `deploy-tradein.yml` авто-триггерится (paths `tradein-mvp/**`) → **ребилд прод-образа tradein-backend на кануне демо**. Если `camoufox fetch` (960МБ download) упадёт/затаймаутит на runner'е во время Docker build → деплой падает strict → демо ломается. Поведенчески безопасно (флаг `scraper_fetch_mode=curl_cffi` default — скрейперы не меняются), и это foundation эпика #883, **НЕ demo-gated** (нет milestone). ⇒ ценность post-demo, риск только на demo-eve build. Рекомендация: **держать до после демо**, либо human/devops подтверждает, что Docker-ребилд с camoufox можно прогнать сейчас (и желательно verify build на свежем образе ДО merge). Тогда возьму. Phase 1/2 (per-source switch) — тем более post-demo.
Collaborator

analyst gate (needs-analysis): это ops/live-задача, НЕ headless-claimableneeds-human + scope/backend priority/p1.

Почему needs-human (как #814): enable scrape_schedules = разовая мутация прод-БД (UPDATE scrape_schedules SET enabled=true) + проверка «не банится» = наблюдение живого ночного прогона (окно 02:00-05:00 UTC). Автономный backend cron-бот это не закроет — claim'нет и зависнет на ожидании ночи. Это resolver/ops-окно (на машине с prod-доступом).

Дедуп / overlap:

  • avito-часть полностью дублирует #814 (enable avito_city_sweep schedule id=1 + verify проксированного свипа, уже needs-human). НЕ дублировать — avito делать в #814.
  • Этот #884 = расширение на cian + yandex schedules (+ зонтик «все источники»). Связан с epic #815 (launch readiness всех скрейперов).
  • ⚠️ Перед enable cian: cian-сбор заблокирован на DMIR_AUTH cookies (#639, needs-human, не автоматизируется) — enable cian-schedule бессмысленен пока куки не залиты. yandex — проверить, что schedule id=104 + proxy готовы (#775/#860).

Что нужно человеку/resolver: (1) #814 avito enable+verify; (2) #639 cian cookies → потом enable cian; (3) yandex enable id=104; (4) дождаться ночного прогона, coverage check. Держать #884 как ops-зонтик, НЕ ready.

analyst gate (needs-analysis): это **ops/live-задача, НЕ headless-claimable** → `needs-human` + `scope/backend` `priority/p1`. **Почему needs-human (как #814):** `enable scrape_schedules` = разовая мутация прод-БД (`UPDATE scrape_schedules SET enabled=true`) + проверка «не банится» = наблюдение живого ночного прогона (окно 02:00-05:00 UTC). Автономный backend cron-бот это не закроет — claim'нет и зависнет на ожидании ночи. Это resolver/ops-окно (на машине с prod-доступом). **Дедуп / overlap:** - **avito-часть полностью дублирует #814** (enable avito_city_sweep schedule id=1 + verify проксированного свипа, уже needs-human). НЕ дублировать — avito делать в #814. - Этот #884 = расширение на **cian + yandex** schedules (+ зонтик «все источники»). Связан с epic #815 (launch readiness всех скрейперов). - ⚠️ Перед enable cian: cian-сбор заблокирован на `DMIR_AUTH` cookies (#639, needs-human, не автоматизируется) — enable cian-schedule бессмысленен пока куки не залиты. yandex — проверить, что schedule id=104 + proxy готовы (#775/#860). **Что нужно человеку/resolver:** (1) #814 avito enable+verify; (2) #639 cian cookies → потом enable cian; (3) yandex enable id=104; (4) дождаться ночного прогона, coverage check. Держать #884 как ops-зонтик, НЕ ready.
bot-analyst added the
needs-human
label 2026-05-31 10:44:46 +00:00
Collaborator

↑ analyst: мой предыдущий коммент (про «enable scrape_schedules / needs-human») ОШИБОЧЕН — я перепутал содержание issue. #884 это Phase 0 эпика #883 — BrowserFetcher (camoufox) abstraction + Docker: чисто код+инфра-задача (новый модуль browser_fetcher.py, config, Dockerfile, unit-тест), полностью headless-claimable backend-воркером. НЕ про enable schedules, НЕ needs-human. Тело issue уже actionable (бинарный DoD, точные файлы). Возвращаю корректные лейблы: scope/backend + scope/devops (Docker) + priority/p1 + status/ready. Прошу прощения за путаницу.

↑ analyst: мой предыдущий коммент (про «enable scrape_schedules / needs-human») **ОШИБОЧЕН** — я перепутал содержание issue. #884 это **Phase 0 эпика #883 — BrowserFetcher (camoufox) abstraction + Docker**: чисто код+инфра-задача (новый модуль `browser_fetcher.py`, config, Dockerfile, unit-тест), полностью headless-claimable backend-воркером. НЕ про enable schedules, НЕ needs-human. Тело issue уже actionable (бинарный DoD, точные файлы). Возвращаю корректные лейблы: `scope/backend` + `scope/devops` (Docker) + `priority/p1` + `status/ready`. Прошу прощения за путаницу.
bot-analyst removed the
needs-human
label 2026-05-31 10:46:10 +00:00
bot-backend self-assigned this 2026-05-31 12:33:27 +00:00
bot-backend added
status/wip
and removed
status/ready
labels 2026-05-31 12:33:29 +00:00
bot-backend added
status/review
and removed
status/wip
labels 2026-05-31 12:42:43 +00:00
lekss361 added
status/qa
and removed
status/review
labels 2026-06-06 18:43:18 +00:00
Author
Owner

Status actualize (06.06): Phase 0 влита в main#899 (751059c, camoufox cache-path под app-user) + контейнеризация в рамках #905. Открытых PR нет. status/reviewstatus/qa.

Status actualize (06.06): Phase 0 влита в `main` — #899 (`751059c`, camoufox cache-path под app-user) + контейнеризация в рамках #905. Открытых PR нет. `status/review` → `status/qa`.
Collaborator

Закрыто — BrowserFetcher живёт (tradein-mvp/backend/app/services/scrapers/browser_fetcher.py) как HTTP-клиент к camoufox-сайдкару tradein-browser. Дизайн эволюционировал из in-process AsyncCamoufox в HTTP-сайдкар (playwright≥1.45 убрал browserServerImpl.js). Все config-ключи (browser_recycle_pages=15, browser_nav_timeout_ms=60000, …) на месте.

✅ Закрыто — `BrowserFetcher` живёт (tradein-mvp/backend/app/services/scrapers/browser_fetcher.py) как HTTP-клиент к camoufox-сайдкару `tradein-browser`. Дизайн эволюционировал из in-process AsyncCamoufox в HTTP-сайдкар (playwright≥1.45 убрал browserServerImpl.js). Все config-ключи (browser_recycle_pages=15, browser_nav_timeout_ms=60000, …) на месте.
Sign in to join this conversation.
No milestone
No project
No assignees
3 participants
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#884
No description provided.