Авито: притворяемся Chrome 120 в 2026 году, и заголовки внутренне противоречивы #3034

Closed
opened 2026-08-21 13:31:17 +00:00 by lekss361 · 3 comments
Owner

Найдено 2026-08-21 сверкой живого браузера со скраппером. Две правки в одну строку каждая, обе проверяются одним прогоном свипа.

1. impersonate="chrome120"

Живой Chrome шлёт:

user-agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) … Chrome/151.0.0.0 Safari/537.36
sec-ch-ua: "Not=A?Brand";v="99", "Google Chrome";v="151", "Chromium";v="151"

Мы — chrome120, зашитый в 9 местах: providers/avito/{serp,detail,imv,houses}.py, orchestration/pipeline.py ×4, плюс константа DEFAULT_IMPERSONATE в providers/_base.py:64.

Разрыв в 31 мажорную версию — и в TLS-отпечатке (JA3/JA4), и в заявленных client hints. «Chrome 120 в августе 2026» — эвристика нулевой стоимости для площадки.

В контейнере уже есть чем чинить: curl_cffi 0.15.0, профили chrome99 … chrome123, chrome124, chrome131, chrome133a, chrome136, chrome142, chrome145, chrome146 плюс алиас chrome.

Предлагается chrome146 явным значением, а не алиас chrome: алиас поедет сам при обновлении библиотеки, и поведение изменится молча. Свести все 9 мест к единственной константе, чтобы следующее обновление было однострочным.

2. Sec-Fetch-Site: "none" вместе с Referer

providers/_base.py:74, DOCUMENT_HEADERS:

"Sec-Fetch-Site": "none",

Захардкожено none, при этом на тот же запрос ставится Referer: https://yandex.ru/.

Настоящий Chrome так не делает никогда: none означает «адрес введён руками или открыт из закладки», и Referer в этом случае отсутствует по определению. Переход с чужого домена обязан давать cross-site.

Каждый наш document-запрос несёт комбинацию, которой в природе не бывает. От IP не зависит — сменой прокси не лечится.

Заголовок общий для всех провайдеров, не только Авито, — правка чинит и ЦИАН с Яндексом.

3. Мелочи того же рода

  • Реальный переход из яндексовой выдачи дал referer: https://ya.ru/; в коде _AVITO_WARM_YANDEX_REFERER = "https://yandex.ru/".
  • Органический клик добавил ?ysclid=mt2yqbrue3372107125. Прогревочный URL идёт без него — то есть выглядит как заход по прямой ссылке, что противоречит заявленному Referer.

4. Прогрев не проверяет, что антибот-куки пришли

Живой браузер после двух страниц держит 45 кук; профильные — __zzatw-avito (два скоупа), cfidsw-avito (три), __ai_fp_uuid, uxs_uid, sx, f, ft, luri, domain_sid, acs_3.

Прогрев (detail.py:276-307) рассчитан на их появление, но факт не проверяет: сессия идёт дальше и сжигает прокси на обречённых запросах. Дешевле упасть сразу с внятной причиной.

Порядок

Правки применять поштучно, с отдельным замером доли отказов после каждой. Иначе не узнаем, что сработало, и рискуем строить дорогую имитацию браузера там, где хватило бы смены версии профиля.

Приёмка

  • Один константный источник impersonate, значение chrome146
  • Sec-Fetch-Site согласован с наличием Referer
  • Замер доли banned/failed до и после, отдельно по каждой правке
  • Прогрев падает громко, если антибот-куки не появились

Scope: packages/scraper-kit/src/scraper_kit/providers/_base.py, providers/avito/*.py, orchestration/pipeline.py.

Найдено 2026-08-21 сверкой живого браузера со скраппером. Две правки в одну строку каждая, обе проверяются одним прогоном свипа. ## 1. `impersonate="chrome120"` Живой Chrome шлёт: ``` user-agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) … Chrome/151.0.0.0 Safari/537.36 sec-ch-ua: "Not=A?Brand";v="99", "Google Chrome";v="151", "Chromium";v="151" ``` Мы — `chrome120`, зашитый в **9 местах**: `providers/avito/{serp,detail,imv,houses}.py`, `orchestration/pipeline.py` ×4, плюс константа `DEFAULT_IMPERSONATE` в `providers/_base.py:64`. Разрыв в **31 мажорную версию** — и в TLS-отпечатке (JA3/JA4), и в заявленных client hints. «Chrome 120 в августе 2026» — эвристика нулевой стоимости для площадки. **В контейнере уже есть чем чинить:** `curl_cffi 0.15.0`, профили `chrome99 … chrome123, chrome124, chrome131, chrome133a, chrome136, chrome142, chrome145, chrome146` плюс алиас `chrome`. Предлагается **chrome146** явным значением, а не алиас `chrome`: алиас поедет сам при обновлении библиотеки, и поведение изменится молча. Свести все 9 мест к единственной константе, чтобы следующее обновление было однострочным. ## 2. `Sec-Fetch-Site: "none"` вместе с `Referer` `providers/_base.py:74`, `DOCUMENT_HEADERS`: ```python "Sec-Fetch-Site": "none", ``` Захардкожено `none`, при этом на тот же запрос ставится `Referer: https://yandex.ru/`. Настоящий Chrome так **не делает никогда**: `none` означает «адрес введён руками или открыт из закладки», и Referer в этом случае отсутствует по определению. Переход с чужого домена обязан давать `cross-site`. Каждый наш document-запрос несёт комбинацию, которой в природе не бывает. **От IP не зависит — сменой прокси не лечится.** Заголовок общий для всех провайдеров, не только Авито, — правка чинит и ЦИАН с Яндексом. ## 3. Мелочи того же рода - Реальный переход из яндексовой выдачи дал `referer: https://ya.ru/`; в коде `_AVITO_WARM_YANDEX_REFERER = "https://yandex.ru/"`. - Органический клик добавил `?ysclid=mt2yqbrue3372107125`. Прогревочный URL идёт без него — то есть выглядит как заход по прямой ссылке, что противоречит заявленному Referer. ## 4. Прогрев не проверяет, что антибот-куки пришли Живой браузер после двух страниц держит 45 кук; профильные — `__zzatw-avito` (два скоупа), `cfidsw-avito` (три), `__ai_fp_uuid`, `uxs_uid`, `sx`, `f`, `ft`, `luri`, `domain_sid`, `acs_3`. Прогрев (`detail.py:276-307`) рассчитан на их появление, но факт не проверяет: сессия идёт дальше и сжигает прокси на обречённых запросах. Дешевле упасть сразу с внятной причиной. ## Порядок Правки применять **поштучно, с отдельным замером** доли отказов после каждой. Иначе не узнаем, что сработало, и рискуем строить дорогую имитацию браузера там, где хватило бы смены версии профиля. ## Приёмка - [ ] Один константный источник `impersonate`, значение `chrome146` - [ ] `Sec-Fetch-Site` согласован с наличием Referer - [ ] Замер доли `banned`/`failed` до и после, отдельно по каждой правке - [ ] Прогрев падает громко, если антибот-куки не появились Scope: `packages/scraper-kit/src/scraper_kit/providers/_base.py`, `providers/avito/*.py`, `orchestration/pipeline.py`.
lekss361 added the
bug
priority/p1
scope/backend
scrapers
tradein
labels 2026-08-21 13:32:30 +00:00
Author
Owner

Базовая точка замера — снята 2026-08-21 13:35 UTC, ДО правок

Без зафиксированного «до» замер «после» ничего не значит. Данные за 21 день, scrape_runs, все avito-задания:

Задание done banned failed всего отказов
avito_full_load 1 5 0 6 83,3%
avito_full_load_exhaustive 0 2 0 3 66,7%
avito_detail_backfill 8 5 9 22 63,6%
avito_city_sweep 15 4 5 24 37,5%
avito_city_sweep_verkhnyaya_pyshma 6 0 3 9 33,3%
avito_city_sweep_pervouralsk 7 1 1 9 22,2%
avito_city_sweep_kamensk_uralskiy 7 1 1 9 22,2%
avito_city_sweep_nizhniy_tagil 8 1 1 10 20,0%
avito_city_sweep_serov 8 1 0 9 11,1%
avito_newbuilding_sweep 19 2 0 21 9,5%

Показательно: чем полнее обход, тем выше отказ. full_load (83%) и exhaustive (67%) против newbuilding_sweep (9,5%). Это косвенно подтверждает, что объём — самостоятельный фактор бана, и подпирает #3033: сокращение выдачи впятеро может дать больше, чем любая правка отпечатка.

⚠️ Важный конфаунд: прокси сменили сегодня

Владелец обновил IP на всех прокси около 12:30 UTC 2026-08-21. Эффект уже виден: avito_city_sweep, запущенный вручную в 12:53, прошёл done — 128 объявлений, 120 новых, при том что два предыдущих подряд ловили Avito SERP firewall — IP banned.

Значит цифры выше смешивают два периода. При замере «после» сравнивать надо не с 21-дневным средним, а с окном после смены прокси и до мержа этого PR — иначе улучшение от новых IP запишется на счёт правок отпечатка.

Запрос для замера, границу подставить по факту мержа:

SELECT source,
  count(*) FILTER (WHERE status='done')   AS done,
  count(*) FILTER (WHERE status='banned') AS banned,
  count(*) FILTER (WHERE status='failed') AS failed,
  count(*) AS total,
  round(100.0 * count(*) FILTER (WHERE status IN ('banned','failed'))
        / nullif(count(*),0), 1) AS bad_pct
FROM scrape_runs
WHERE source LIKE 'avito%'
  AND started_at > '<граница>'
GROUP BY source ORDER BY total DESC;

Чтобы набрать статистику, нужно минимум несколько суток: городские свипы ходят раз в 1-3 дня, full_load — раз в неделю. Один зелёный прогон ничего не доказывает — у avito_city_sweep и в плохие недели было 15 успешных из 24.

Refs #3033, #3035

## Базовая точка замера — снята 2026-08-21 13:35 UTC, ДО правок Без зафиксированного «до» замер «после» ничего не значит. Данные за 21 день, `scrape_runs`, все avito-задания: | Задание | done | banned | failed | всего | **отказов** | |---|---|---|---|---|---| | `avito_full_load` | 1 | 5 | 0 | 6 | **83,3%** | | `avito_full_load_exhaustive` | 0 | 2 | 0 | 3 | **66,7%** | | `avito_detail_backfill` | 8 | 5 | 9 | 22 | **63,6%** | | `avito_city_sweep` | 15 | 4 | 5 | 24 | **37,5%** | | `avito_city_sweep_verkhnyaya_pyshma` | 6 | 0 | 3 | 9 | 33,3% | | `avito_city_sweep_pervouralsk` | 7 | 1 | 1 | 9 | 22,2% | | `avito_city_sweep_kamensk_uralskiy` | 7 | 1 | 1 | 9 | 22,2% | | `avito_city_sweep_nizhniy_tagil` | 8 | 1 | 1 | 10 | 20,0% | | `avito_city_sweep_serov` | 8 | 1 | 0 | 9 | 11,1% | | `avito_newbuilding_sweep` | 19 | 2 | 0 | 21 | 9,5% | Показательно: **чем полнее обход, тем выше отказ.** `full_load` (83%) и `exhaustive` (67%) против `newbuilding_sweep` (9,5%). Это косвенно подтверждает, что объём — самостоятельный фактор бана, и подпирает #3033: сокращение выдачи впятеро может дать больше, чем любая правка отпечатка. ## ⚠️ Важный конфаунд: прокси сменили сегодня Владелец обновил IP на всех прокси около **12:30 UTC 2026-08-21**. Эффект уже виден: `avito_city_sweep`, запущенный вручную в 12:53, прошёл `done` — 128 объявлений, 120 новых, при том что два предыдущих подряд ловили `Avito SERP firewall — IP banned`. Значит цифры выше **смешивают два периода**. При замере «после» сравнивать надо не с 21-дневным средним, а с окном **после смены прокси и до мержа этого PR** — иначе улучшение от новых IP запишется на счёт правок отпечатка. Запрос для замера, границу подставить по факту мержа: ```sql SELECT source, count(*) FILTER (WHERE status='done') AS done, count(*) FILTER (WHERE status='banned') AS banned, count(*) FILTER (WHERE status='failed') AS failed, count(*) AS total, round(100.0 * count(*) FILTER (WHERE status IN ('banned','failed')) / nullif(count(*),0), 1) AS bad_pct FROM scrape_runs WHERE source LIKE 'avito%' AND started_at > '<граница>' GROUP BY source ORDER BY total DESC; ``` Чтобы набрать статистику, нужно минимум несколько суток: городские свипы ходят раз в 1-3 дня, `full_load` — раз в неделю. Один зелёный прогон ничего не доказывает — у `avito_city_sweep` и в плохие недели было 15 успешных из 24. Refs #3033, #3035
Author
Owner

Все три пункта в коде и на проде; эффект измерен — правка сработала

Проверял не по диффу, а по тому, что реально исполняется в прод-контейнере tradein-scraper:

impersonate в контейнере: chrome146
Sec-Fetch-Site по умолчанию: none
с Referer: {'Referer': 'https://ya.ru/', 'Sec-Fetch-Site': 'cross-site'}
warm-referer: https://ya.ru/
пункт состояние
1. chrome120 → единая константа chrome146 DEFAULT_IMPERSONATE в _base.py, все 9 мест читают её
2. Sec-Fetch-Site: none рядом с Referer referer_headers() ставит cross-site
3. yandex.ruya.ru _AVITO_WARM_YANDEX_REFERER = "https://ya.ru/"

Профиль chrome146 в установленной библиотеке подтвердил отдельно: curl_cffi 0.15.0, 19 chrome-профилей, chrome146 в списке.

Эффект — рост примерно в 35 раз, продержавшийся пять суток

Судить по status в scrape_runs тут нельзя: banned ставится honest-status после сбора, поэтому агрегат «успешных прогонов» показывал ноль и создавал впечатление, что правка не помогла. Считать надо по counters->>'enriched':

сутки обогащено блоков
16–20.08 (до правки) 2–10 0–1
21.08 (правка в 17:02) 178 77
22.08 129 95
23.08 147 134
24.08 138 93
25.08 111 107
26.08 23 66
27.08 0 25

С ~4 в сутки до ~140. Гипотеза issue подтвердилась замером.

Обвал 26–27.08 — другая причина, вынесена в отдельный PR

Не отпечаток. Обе ветки повтора после блока в fetch_detail пересоздавали сессию без config, то есть без прокси, и уходили с адреса самой машины. Пока адрес не был под ограничением, это часто срабатывало и выглядело как успех; после его смены каждый повтор стал обречён.

Замер из прод-контейнера, один URL:

через прокси — 200, 3.3 МБ страницы
напрямую     — 429, «доступ ограничен», firewall

Разбор и правка — #3143. Там же естественное подтверждение: в serp.py та же операция сделана правильно, и ровно те скраперы не пострадали (city_sweep 57%, newbuilding 83% против 0% у detail_backfill).

Предлагаю этот issue закрывать — всё, что в нём записано, сделано и подтверждено замером. Оставшаяся часть отказов Авито живёт в #3143 (проксирование повторов) и #3045 (QRATOR proof-of-work).

## Все три пункта в коде и на проде; эффект измерен — правка сработала Проверял не по диффу, а по тому, что реально исполняется в прод-контейнере `tradein-scraper`: ``` impersonate в контейнере: chrome146 Sec-Fetch-Site по умолчанию: none с Referer: {'Referer': 'https://ya.ru/', 'Sec-Fetch-Site': 'cross-site'} warm-referer: https://ya.ru/ ``` | пункт | состояние | |---|---| | 1. `chrome120` → единая константа `chrome146` | ✅ `DEFAULT_IMPERSONATE` в `_base.py`, все 9 мест читают её | | 2. `Sec-Fetch-Site: none` рядом с `Referer` | ✅ `referer_headers()` ставит `cross-site` | | 3. `yandex.ru` → `ya.ru` | ✅ `_AVITO_WARM_YANDEX_REFERER = "https://ya.ru/"` | Профиль `chrome146` в установленной библиотеке подтвердил отдельно: `curl_cffi 0.15.0`, 19 chrome-профилей, `chrome146` в списке. ## Эффект — рост примерно в 35 раз, продержавшийся пять суток Судить по `status` в `scrape_runs` тут нельзя: `banned` ставится honest-status **после** сбора, поэтому агрегат «успешных прогонов» показывал ноль и создавал впечатление, что правка не помогла. Считать надо по `counters->>'enriched'`: | сутки | обогащено | блоков | |---|---:|---:| | 16–20.08 (до правки) | 2–10 | 0–1 | | **21.08** (правка в 17:02) | **178** | 77 | | 22.08 | 129 | 95 | | 23.08 | 147 | 134 | | 24.08 | 138 | 93 | | 25.08 | 111 | 107 | | 26.08 | 23 | 66 | | 27.08 | 0 | 25 | С ~4 в сутки до ~140. Гипотеза issue подтвердилась замером. ## Обвал 26–27.08 — другая причина, вынесена в отдельный PR Не отпечаток. Обе ветки повтора после блока в `fetch_detail` пересоздавали сессию **без `config`**, то есть без прокси, и уходили с адреса самой машины. Пока адрес не был под ограничением, это часто срабатывало и выглядело как успех; после его смены каждый повтор стал обречён. Замер из прод-контейнера, один URL: ``` через прокси — 200, 3.3 МБ страницы напрямую — 429, «доступ ограничен», firewall ``` Разбор и правка — **#3143**. Там же естественное подтверждение: в `serp.py` та же операция сделана правильно, и ровно те скраперы не пострадали (`city_sweep` 57%, `newbuilding` 83% против 0% у `detail_backfill`). **Предлагаю этот issue закрывать** — всё, что в нём записано, сделано и подтверждено замером. Оставшаяся часть отказов Авито живёт в #3143 (проксирование повторов) и #3045 (QRATOR proof-of-work).
Collaborator

Перепроверил в прод-контейнере и закрываю — но периметр правки оказался уже, чем звучит заголовок

Проверял не по диффу, а по тому, что исполняется в tradein-scraper:

DOCUMENT_HEADERS Sec-Fetch-Site: none
referer_headers("https://ya.ru/"): {'Referer': 'https://ya.ru/', 'Sec-Fetch-Site': 'cross-site'}
curl_cffi: 0.15.0

Все три пункта на месте: единая константа DEFAULT_IMPERSONATE = chrome146, Sec-Fetch-Site рядом с Referer становится cross-site, прогревочный referer — ya.ru. Внутри scraper_kit литерала chrome120 не осталось ни одного, сторож это закрепляет.

Чего проверка не покрыла — и это стоит знать до закрытия

Заголовок говорит «притворяемся Chrome 120 в 2026 году» без оговорок, а вычищен только пакет. Три живых прод-пути бэкенда остались на chrome120:

Файл Что делает
app/services/cian_session.py:164 верификация куки Циана
app/services/yandex_address_backfill.py:153 бэкфилл адресов Яндекса
app/tasks/yandex_detail_backfill.py:303 detail-бэкфилл Яндекса

Сторож их не видел по построению: он сканирует только packages/scraper-kit/, а его докстрока объявляла остальное «отдельным периметром вне scope #3034» со ссылкой на #2361 F4a — который на тот момент уже был закрыт. Исключение осталось, адресат исключения исчез.

Хуже того, tests/test_cian_session.py:488 прибивал chrome120 гвоздём: подъём профиля в kit ронял бы этот тест, и «починка» состояла бы в возврате к устаревшему профилю. То есть тест закреплял ровно тот дефект, ради которого этот issue заводился.

Вынесено в #3148, правка — PR #3149 (три вызова на константу, тест Циана сверяется с DEFAULT_IMPERSONATE, сторож расширен на backend/app/).

Закрываю #3034: то, что в нём записано, сделано и подтверждено замером роста обогащения с ~4 до ~140 в сутки. Остальные отказы Авито живут в #3143 (повторы уходили без прокси) и #3045 (QRATOR proof-of-work).

## Перепроверил в прод-контейнере и закрываю — но периметр правки оказался уже, чем звучит заголовок Проверял не по диффу, а по тому, что исполняется в `tradein-scraper`: ``` DOCUMENT_HEADERS Sec-Fetch-Site: none referer_headers("https://ya.ru/"): {'Referer': 'https://ya.ru/', 'Sec-Fetch-Site': 'cross-site'} curl_cffi: 0.15.0 ``` Все три пункта на месте: единая константа `DEFAULT_IMPERSONATE = chrome146`, `Sec-Fetch-Site` рядом с `Referer` становится `cross-site`, прогревочный referer — `ya.ru`. Внутри `scraper_kit` литерала `chrome120` не осталось ни одного, сторож это закрепляет. ## Чего проверка не покрыла — и это стоит знать до закрытия Заголовок говорит «притворяемся Chrome 120 в 2026 году» без оговорок, а вычищен только пакет. **Три живых прод-пути бэкенда остались на `chrome120`:** | Файл | Что делает | |---|---| | `app/services/cian_session.py:164` | верификация куки Циана | | `app/services/yandex_address_backfill.py:153` | бэкфилл адресов Яндекса | | `app/tasks/yandex_detail_backfill.py:303` | detail-бэкфилл Яндекса | Сторож их не видел по построению: он сканирует только `packages/scraper-kit/`, а его докстрока объявляла остальное «отдельным периметром вне scope #3034» со ссылкой на #2361 F4a — **который на тот момент уже был закрыт**. Исключение осталось, адресат исключения исчез. Хуже того, `tests/test_cian_session.py:488` прибивал `chrome120` гвоздём: подъём профиля в kit ронял бы этот тест, и «починка» состояла бы в возврате к устаревшему профилю. То есть тест закреплял ровно тот дефект, ради которого этот issue заводился. Вынесено в #3148, правка — PR #3149 (три вызова на константу, тест Циана сверяется с `DEFAULT_IMPERSONATE`, сторож расширен на `backend/app/`). Закрываю #3034: то, что в нём записано, сделано и подтверждено замером роста обогащения с ~4 до ~140 в сутки. Остальные отказы Авито живут в #3143 (повторы уходили без прокси) и #3045 (QRATOR proof-of-work).
Sign in to join this conversation.
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#3034
No description provided.