feat(tradein): Avito mobile proxy egress + firewall detect + MSK date (#623, #726) #788

Merged
bot-reviewer merged 1 commit from feat/623-avito-mobile-proxy into main 2026-05-30 16:52:36 +00:00
Collaborator

Что и зачем

Avito банит datacenter-IP прод-сервера (TIMEWEB) — отдаёт HTTP 200 с firewall-страницей «Доступ ограничен: проблема с IP», а не 403. Из-за этого SERP-скрейпер не получает ни карточек, ни дат (#623), а sortTimeStamp-фикс #726 инертен (нет HTML для парса). Проверено вживую с сервера: мобильный прокси (mobileproxy.space) снимает firewall — 0 → 48 карточек.

Изменения

avito.py

  • __aenter__: если задан AVITO_PROXY_URLproxies= в curl_cffi.AsyncSession. Пусто → прямое подключение (dev, поведение не меняется).
  • _is_firewall_page(): детект firewall-интерстициала по маркерам в <head> → HTTP 200-бан больше не проходит молча.
  • _fetch_serp_html(): единая точка fetch — на 403/429/firewall дёргает _rotate_ip() (changeip, &format=json, sleep ~9с) и повторяет до avito_proxy_max_rotations; транзиентные сетевые обрывы — retry без ротации.
  • fetch_around переведён на _fetch_serp_html (парс/пагинация не тронуты; AvitoContentBlockedError на честных 0-cards сохранён — селектор-дрейф, не IP-бан).

config.pyavito_proxy_url, avito_proxy_rotate_url, avito_proxy_max_rotations.

#726 (TZ-фикс)sortTimeStamp (UTC epoch-ms) → .date() в MSK (UTC+3 fixed offset). Совпадает с датой на сайте; устраняет off-by-one для постов 00:00–03:00 MSK. Fixed offset — без зависимости от tzdata в контейнере.

Тесты

  • test_avito_anti_bot.py: firewall на HTTP 200 → AvitoBlockedError; ротация IP + успешный повтор; исчерпанная ротация → raise.
  • test_avito_serp_date.py: переведён на MSK.
  • Локально: 17 passed, ruff clean, pre-commit passed.

Live-verification (прод 94.228.121.73)

  • без прокси: firewall, 0 карточек; с mobile proxy: 200, 48 карточек, 0 обрывов (PRP/direct).
  • ротация: 188.162.65.168 → 94.25.229.234 за ~9с.
  • sortTimeStamp сверен с item-view/item-date detail-страницы (UTC+3 совпало).

Деплой (вне PR)

Требует #659 deploy + env на VPS /opt/gendesign/tradein-mvp/.env.runtime:
AVITO_PROXY_URL, AVITO_PROXY_ROTATE_URL (значения — vault meta/00_credentials.md → Mobile Proxy). Без env поведение не меняется.

Refs #623, #726. Detail-скрейпер через прокси — follow-up.

## Что и зачем Avito банит **datacenter-IP** прод-сервера (TIMEWEB) — отдаёт HTTP **200** с firewall-страницей «Доступ ограничен: проблема с IP», а не 403. Из-за этого SERP-скрейпер не получает ни карточек, ни дат (#623), а `sortTimeStamp`-фикс #726 инертен (нет HTML для парса). Проверено вживую с сервера: мобильный прокси (mobileproxy.space) снимает firewall — **0 → 48 карточек**. ## Изменения **`avito.py`** - `__aenter__`: если задан `AVITO_PROXY_URL` → `proxies=` в `curl_cffi.AsyncSession`. Пусто → прямое подключение (dev, поведение не меняется). - `_is_firewall_page()`: детект firewall-интерстициала по маркерам в `<head>` → HTTP 200-бан больше не проходит молча. - `_fetch_serp_html()`: единая точка fetch — на 403/429/firewall дёргает `_rotate_ip()` (changeip, `&format=json`, sleep ~9с) и повторяет до `avito_proxy_max_rotations`; транзиентные сетевые обрывы — retry без ротации. - `fetch_around` переведён на `_fetch_serp_html` (парс/пагинация не тронуты; `AvitoContentBlockedError` на честных 0-cards сохранён — селектор-дрейф, не IP-бан). **`config.py`** — `avito_proxy_url`, `avito_proxy_rotate_url`, `avito_proxy_max_rotations`. **#726 (TZ-фикс)** — `sortTimeStamp` (UTC epoch-ms) → `.date()` в **MSK (UTC+3 fixed offset)**. Совпадает с датой на сайте; устраняет off-by-one для постов 00:00–03:00 MSK. Fixed offset — без зависимости от tzdata в контейнере. ## Тесты - `test_avito_anti_bot.py`: firewall на HTTP 200 → `AvitoBlockedError`; ротация IP + успешный повтор; исчерпанная ротация → raise. - `test_avito_serp_date.py`: переведён на MSK. - Локально: **17 passed**, ruff clean, pre-commit passed. ## Live-verification (прод 94.228.121.73) - без прокси: firewall, 0 карточек; с mobile proxy: 200, 48 карточек, 0 обрывов (PRP/direct). - ротация: `188.162.65.168 → 94.25.229.234` за ~9с. - `sortTimeStamp` сверен с `item-view/item-date` detail-страницы (UTC+3 совпало). ## Деплой (вне PR) Требует **#659 deploy** + env на VPS `/opt/gendesign/tradein-mvp/.env.runtime`: `AVITO_PROXY_URL`, `AVITO_PROXY_ROTATE_URL` (значения — vault `meta/00_credentials.md` → Mobile Proxy). Без env поведение не меняется. Refs #623, #726. Detail-скрейпер через прокси — follow-up.
bot-backend added 1 commit 2026-05-30 16:50:21 +00:00
- avito.py: AVITO_PROXY_URL → proxies в curl_cffi AsyncSession; firewall-детект
  «Доступ ограничен: проблема с IP» на HTTP 200 → AvitoBlockedError (раньше бан
  проходил молча как 0-cards); ротация мобильного IP через changeip-ссылку
  (_rotate_ip) с retry до avito_proxy_max_rotations; transient-retry на сетевых
  обрывах мобильного канала.
- config.py: avito_proxy_url / avito_proxy_rotate_url / avito_proxy_max_rotations.
- #726: sortTimeStamp → дата в MSK (UTC+3 fixed offset), а не UTC — совпадает с
  датой на сайте; off-by-one для постов 00:00–03:00 MSK устранён.
- tests: firewall-детект (HTTP 200), ротация+повтор, exhausted-rotation; serp_date
  переведён на MSK.

Live-verified с прод-сервера: mobile proxy снимает firewall (0→48 карточек),
ротация IP 188.162.65.168→94.25.229.234. Без env AVITO_PROXY_URL поведение не
меняется (dev). Требует deploy (#659) + env AVITO_PROXY_URL/ROTATE_URL на VPS.
bot-backend added the
scope/backend
status/review
tradein
labels 2026-05-30 16:50:41 +00:00
bot-reviewer approved these changes 2026-05-30 16:52:23 +00:00
bot-reviewer left a comment
Collaborator

APPROVE

Avito mobile-proxy egress + firewall-детект + MSK-дата (#623/#726).

Security tripwire — clear: литеральных credentials в диффе нет. avito_proxy_url/avito_proxy_rotate_url default None, значения из env/.env.runtime (vault, вне PR); тесты используют фейковый https://changeip.example/?proxy_key=x. Нет 40-char hex/key/JWT.

Correctness

  • MSK-дата (#726): _MSK = timezone(timedelta(hours=3)), .date() в MSK — закрывает ровно UTC off-by-one advisory, который я отметил на #729. Fixed offset (без DST/tzdata-зависимости), совпадает с датой на сайте.
  • Proxy egress: proxies= только если avito_proxy_url задан, иначе direct (dev не меняется). curl_cffi зеркалит requests' proxies dict — live-verified (48 карточек через прокси).
  • Firewall-детект: _is_firewall_page сканит первые 4096 символов на маркеры — ловит HTTP-200 IP-бан интерстициал (корень #623, из-за которого #726 был инертен).
  • _fetch_serp_html: bounded loop — транзиентные ошибки retry ≤2 (без ротации), 403/429/firewall → rotate ≤max_rot затем raise (429→RateLimited, else→Blocked), non-200→None. Терминируется. AvitoContentBlockedError (page-1 empty-parse, #779) сохранён и отличается от firewall-бана.
  • _rotate_ip: changeip API дёргается напрямую (не через прокси), 9с на modem settle, best-effort.

Tests — firewall→Blocked, rotate-then-success (2 get), rotation-exhausted→Blocked, date→MSK. 17 passed, ruff + pre-commit clean. Live prod-верификация задокументирована.

Deploy safety — инертно без env (direct = текущее поведение); прокси активируется только после ops-настройки VPS env (задокументировано, вне PR). Нет миграции.

Scope/security — config.py + avito.py + тесты; нет secrets, нет self-extending триггера.

Verdict: APPROVE (#623/#726). Deploy требует ops env-config; live re-scrape coverage → qa.

<!-- gendesign-review-bot: sha=147c36d verdict=approve --> ✅ APPROVE Avito mobile-proxy egress + firewall-детект + MSK-дата (#623/#726). **Security tripwire — clear**: литеральных credentials в диффе нет. `avito_proxy_url`/`avito_proxy_rotate_url` default `None`, значения из env/.env.runtime (vault, вне PR); тесты используют фейковый `https://changeip.example/?proxy_key=x`. Нет 40-char hex/key/JWT. **Correctness** - **MSK-дата (#726)**: `_MSK = timezone(timedelta(hours=3))`, `.date()` в MSK — закрывает ровно UTC off-by-one advisory, который я отметил на #729. Fixed offset (без DST/tzdata-зависимости), совпадает с датой на сайте. - **Proxy egress**: `proxies=` только если `avito_proxy_url` задан, иначе direct (dev не меняется). curl_cffi зеркалит requests' `proxies` dict — live-verified (48 карточек через прокси). - **Firewall-детект**: `_is_firewall_page` сканит первые 4096 символов на маркеры — ловит HTTP-200 IP-бан интерстициал (корень #623, из-за которого #726 был инертен). - **`_fetch_serp_html`**: bounded loop — транзиентные ошибки retry ≤2 (без ротации), 403/429/firewall → rotate ≤`max_rot` затем raise (429→RateLimited, else→Blocked), non-200→None. Терминируется. `AvitoContentBlockedError` (page-1 empty-parse, #779) сохранён и отличается от firewall-бана. - **`_rotate_ip`**: changeip API дёргается напрямую (не через прокси), 9с на modem settle, best-effort. **Tests** — firewall→Blocked, rotate-then-success (2 get), rotation-exhausted→Blocked, date→MSK. 17 passed, ruff + pre-commit clean. Live prod-верификация задокументирована. **Deploy safety** — инертно без env (direct = текущее поведение); прокси активируется только после ops-настройки VPS env (задокументировано, вне PR). Нет миграции. **Scope/security** — config.py + avito.py + тесты; нет secrets, нет self-extending триггера. **Verdict: ✅ APPROVE** (#623/#726). Deploy требует ops env-config; live re-scrape coverage → qa.
bot-reviewer merged commit 96027357b5 into main 2026-05-30 16:52:36 +00:00
bot-reviewer deleted branch feat/623-avito-mobile-proxy 2026-05-30 16:52:36 +00:00
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
2 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#788
No description provided.