|
All checks were successful
CI Trade-In / changes (pull_request) Successful in 8s
CI / changes (pull_request) Successful in 10s
CI Trade-In / backend-tests (pull_request) Has been skipped
CI Trade-In / browser-tests (pull_request) Has been skipped
CI Trade-In / frontend-checks (pull_request) Has been skipped
CI / backend-tests (pull_request) Has been skipped
CI / frontend-tests (pull_request) Has been skipped
CI / openapi-codegen-check (pull_request) Has been skipped
Коридоры считаются под конкретный URL выдачи. При --resume код брал свежий args.base_url, поэтому запуск без повтора --base-url молча качал другую выдачу под тем же batch_id. Теперь URL из плана, расхождение с аргументом — явная ошибка вместо тихого выбора одного из двух. |
||
|---|---|---|
| .. | ||
| collect.py | ||
| README.md | ||
Локальный сбор SERP Авито по Москве и МО (эпик #2989, трек 1)
collect.py — ручной скрипт с машины владельца. Собирает карточки выдачи Авито
(вторичка, Москва + МО) и заливает их в прод-схему msk_raw.
Прод-скрейпер, его расписания, прокси-пул и сайдкар не задействованы вообще.
Браузер — уже открытый Chrome владельца (подключение по CDP), парсер — импорт из
packages/scraper-kit, заливка — поток в psql через ssh.
Предусловия
-
Chrome владельца запущен с залогиненным техническим аккаунтом Авито и remote debugging:
& "C:\Program Files\Google\Chrome\Application\chrome.exe" --remote-debugging-port=9222Проверка:
curl http://localhost:9222/json/versionотдаёт JSON сBrowser: Chrome/.... Другой адрес — переменнаяAVITO_CDP.Скрипт не поднимает свой профиль (
launch_persistent_contextне используется): он подключается к существующему браузеру, берётbrowser.contexts[0], открывает свою вкладку и в конце закрывает только её. Чужие вкладки, контекст и сам браузер не трогаются — это рабочий Chrome владельца. -
ssh-доступ на прод (
ssh selectelбез пароля) — для заливки в БД. Прямого подключения к прод-Postgres с локалки нет: туннель:35432ведёт в мёртвую копию на Beget. -
Playwright в текущем интерпретаторе:
pip install playwright python -m playwright install chromiumВ uv-зависимости проекта playwright не добавлять: прод-сайдкар пинит playwright 1.60 под camoufox, и подъём версии сломает его.
Больше ничего ставить не нужно: только stdlib + playwright + импорт
scraper_kit(путьpackages/scraper-kit/srcскрипт добавляет вsys.pathсам, от__file__).
Запуск (PowerShell)
cd D:\prjct\gendesign\tradein-mvp\scripts\local-avito-msk
# 0) сухой прогон: ничего не шлём на прод, карточки пишем в runs\cards-<batch>.csv
python .\collect.py --dry-run --measure 5
# 1) обязательный первый прогон — замер (дефолт, 100 загрузок страниц)
python .\collect.py
# 2) полный проход — только явно
python .\collect.py --full --batch-id msk-serp-20260908
# 3) продолжить прерванный прогон по сохранённому плану коридоров
python .\collect.py --full --resume --batch-id msk-serp-20260908
Без аргументов скрипт работает в режиме --measure 100 и полный проход не начинает.
Ключи: --delay (пауза между загрузками, дефолт 8.0 с ±20 % джиттера — сознательно
совпадает с прод-расписаниями request_delay_sec 7–10 с), --batch-size (карточек в
одной заливке, дефолт 1000), --target-count (целевой размер коридора, дефолт 1500),
--base-url, --batch-id, --out-dir, --ssh-host/--container/--db-user/--db-name.
Как режется выдача
Потолок пагинации Авито — 30 страниц по 60 = 1800 объявлений на запрос. Любой
запрос с count > 1800 целиком не добирается, поэтому строится план ценовых коридоров:
- читаем счётчик «N объявлений» со страницы 1 (
page-title/count); count > --target-count→ делим коридор пополам по геометрической середине (sqrt(lo*hi)): цены логнормальны, арифметическая середина диапазона 1 млн … 100 млн даёт вырожденно-пустую верхнюю половину;- верхняя граница открытого коридора подбирается удвоением от 8 млн ₽;
- предохранители: глубина рекурсии ≤ 12 и минимальная ширина коридора (отношение
границ ≤ 1.05). Если коридор уже узкий, а
countвсё ещё > 1800 — он помечаетсяtruncated: trueв плане, а в лог и вmsk_raw.batches.notesпишется, сколько объявлений заведомо не добрано; - гео-параметры
radius/geoCoordsне используются: сервером они не применяются (#3043).
План лежит в runs/plan-<batch_id>.json и обновляется после каждой страницы — отсюда
работает --resume.
Стоп на первом признаке блока
Проверки в фиксированном порядке, первое срабатывание = немедленный стоп (никаких ретраев и никакого «продолжим со следующего коридора»):
| # | Признак | Причина в notes |
|---|---|---|
| 1 | HTTP 403 / 439 | platform |
| 2 | HTTP 429 | ratelimit |
| 3 | _is_firewall_page(html) — «доступ ограничен», «проблема с ip», firewall-container |
firewall |
| 4 | startpow / «доступ ограничен: проверка безопасности» в первых 4 КБ |
challenge |
| 5 | 0 карточек при ненулевом счётчике (DOM-drift или тихий блок) | empty_page |
При стопе: недоотправленный батч дозаливается, у батча проставляются finished_at и
notes, скрипт выходит с кодом 2.
Что делать при стопе. Не перезапускать сразу и не крутить ретраи. platform /
firewall / challenge — техаккаунт или IP помечены: пауза на несколько часов,
проверить вручную в браузере, что выдача открывается и аккаунт жив, при повторе
увеличить --delay. ratelimit — темп слишком высокий: --delay 15 и выше.
empty_page — сначала посмотреть сохранённую страницу в браузере: если выдача
рисуется, значит уехал DOM и чинить надо парсер в scraper-kit, а не скрипт.
После разбора — --resume с тем же --batch-id, уже собранное не потеряется.
Куда пишем
Схема msk_raw на проде (создана заранее):
msk_raw.batches(batch_id PK, kind, query, started_at, finished_at, rows_sent, rows_new, notes, uploaded_at)msk_raw.avito_cards(id, source_id, observed_at, batch_id → batches, kind, url, price, payload, UNIQUE(source_id,batch_id,kind))msk_raw.avito_latest— вьюDISTINCT ON (source_id) … ORDER BY source_id, observed_at DESC
Форма заливки: поток в
ssh <host> "docker exec -i tradein-postgres psql -U tradein -d tradein -v ON_ERROR_STOP=1 -f -".
Внутри одной транзакции: INSERT батча (ON CONFLICT DO NOTHING — строка обязана
существовать до карточек из-за FK), CREATE TEMP TABLE _stg (LIKE msk_raw.avito_cards INCLUDING DEFAULTS), \copy _stg (...) FROM STDIN WITH (FORMAT csv), затем
INSERT … SELECT в avito_cards с ON CONFLICT (source_id,batch_id,kind) DO NOTHING
и UPDATE batches SET rows_sent/rows_new (rows_new = разница count(*) по batch_id
до и после вставки).
Одиночный psql -c через ssh ломается на квотинге скобок и кавычек — поэтому только поток.
payload = lot.model_dump(mode="json"). source_id в БД bigint, а у ScrapedLot —
строка: приводится к int, нечисловые пропускаются со счётчиком (он попадает в
notes как skipped_non_numeric=N).
Как проверить залитое
ssh selectel "docker exec -i tradein-postgres psql -U tradein -d tradein -c \"SELECT count(*) FROM msk_raw.avito_latest;\""
ssh selectel "docker exec -i tradein-postgres psql -U tradein -d tradein -c \"SELECT batch_id, rows_sent, rows_new, started_at, finished_at, notes FROM msk_raw.batches ORDER BY uploaded_at DESC LIMIT 5;\""
Про фильтр городов
Парсер конструируется как AvitoScraper(SimpleNamespace(avito_serp_ekb_only=False), target_city_slug="moskva"). avito_serp_ekb_only=False обязателен: с True
_parse_html выбрасывает всё, у чего в URL нет /ekaterinburg/, включая все
подмосковные слаги — из московской выдачи не осталось бы ничего.