Эта статья была полезной?
Как парсить Avito
Технический специалист
Avito (avito.ru) — крупнейшая доска объявлений в России: телефоны, автомобили, недвижимость, одежда, услуги от частных продавцов и магазинов. Для мониторинга цен и анализа рынка всё нужное есть на открытых страницах: названия, цены и скидки, местоположение, состояние и характеристики товара, данные продавца.
Собрать эти данные сложнее, чем загрузить HTML и разобрать его CSS-селекторами:
- Avito защищён собственным файрволом. Если с одного адреса приходит слишком много запросов, сайт отвечает страницей «Доступ ограничен: проблема с IP». Обычный HTTP-клиент через неё не проходит: curl с полным набором браузерных заголовков получил HTTP 429 в 6 попытках из 6 с новых адресов.
- Файрвол может показать капчу. На всех запросах с 6 по 9 октября 2026 года это была GeeTest v4 — пазл со слайдером.
- Один URL — две разметки. Одна и та же страница выдачи приходит то только с обычной разметкой, то ещё и со встроенным JSON-состоянием, в котором больше полей.
- Очевидное поле цены содержит не ту цену. У товара со скидкой поле price в данных Avito содержит цену до скидки, а не ту, которую видит покупатель.
В этом руководстве разберём проект с открытым исходным кодом 2scraper/avito-scraper: как он открывает страницы, откуда берёт данные и что делает в каждом состоянии файрвола. Основное внимание уделим движку Playwright, который рекомендуют авторы репозитория.
Если не указано иное, все цифры ниже взяты из живых запусков репозитория 6 октября 2026 года через российский резидентный IP.
Что мы создадим
Парсер с тремя режимами:
- listing — страница категории или поиска с пагинацией через ?p=N. На каждую карточку — одна строка.
- item — страницы отдельных объявлений: по списку URL или прямо из результата режима listing.
- seller — профиль продавца и все его объявления.
Строка выдачи содержит: sku (id объявления на Avito), title, price, currency, original_price и discount_pct (если сайт их указывает), region_slug, city, location, summary_params, published_at, seller_name, seller_type, seller_id, seller_rating, seller_reviews, promoted, badges, delivery, page и position.
Режим item добавляет description, params (все характеристики), images, category_path, address, lat/lng, views_total, views_today, seller_since, seller_reply_time и is_active.
Чего парсер не собирает: всё, что доступно только после входа в аккаунт (в том числе номера телефонов), и замаскированный IMEI, который в некоторых категориях указан среди характеристик. Частных продавцов Avito обезличивает сам, поэтому у них seller_name пустой, а seller_type равен private. Названия компаний и магазинов в выгрузку попадают, поэтому если вы храните или обрабатываете результат, на него распространяется закон о персональных данных вашей страны — в России это 152-ФЗ.
Каждый запуск сохраняет рядом с данными файл метаданных. По нему видно, получен ли результат полностью или частично либо запуск был заблокирован.
Требования
- Python 3.9+
- Playwright (рекомендуется). В репозитории есть ещё движки pyppeteer и Selenium. Устанавливайте в каждое виртуальное окружение только один движок: их зависимости конфликтуют.
- beautifulsoup4 и requests
Из-за файрвола также понадобятся:
- Российский резидентный прокси с постоянным IP на всю сессию (подойдут резидентные прокси RuCaptcha). Браузер и сервис решения капчи должны работать с одного IP — почему, объясним ниже.
- API-ключ RuCaptcha (TWOCAPTCHA_KEY) для решения GeeTest на странице файрвола.
- Адрес подключения к RuCaptcha Browser API (AVITO_CDP_ENDPOINT): это удалённый браузер, к которому подключаются по протоколу Chrome DevTools (CDP). Авторы репозитория его рекомендуют, а последние замеры делают его практически обязательным: с 14 октября 2026 года решения капчи для локального браузера не принимались ни разу (подробности ниже).
В отличие от многих сайтов, с Avito не получится работать только бесплатными средствами. Обычные HTTP-запросы сайт отклоняет, а новый локальный браузер получает капчу в каждой новой сессии (5 из 5 в тестах). Приёмы разбора данных из статьи работают при любом способе подключения. Однако чтобы получить сами страницы, нужны браузер, российский IP-адрес и в большинстве случаев решённая капча.
Настройка проекта
Клонируйте репозиторий, затем установите основные зависимости и движок Playwright:
bash
git clone https://github.com/2scraper/avito-scraper.git
cd avito-scraper
python3 -m venv .venv
source .venv/bin/activate # Windows: .venv\Scripts\activate
pip install -r requirements.txt
pip install -r requirements-playwright.txt
playwright install chromium
cp .env.example .env
Парсер читает из .env ровно четыре переменные:
| Переменная | Что это |
|---|---|
| TWOCAPTCHA_KEY | API-ключ RuCaptcha для решения GeeTest на странице файрвола |
| AVITO_PROXY | Российский резидентный прокси: http://LOGIN:PASSWORD@HOST:PORT или HOST:PORT:LOGIN:PASSWORD, как его выгружает личный кабинет RuCaptcha |
| AVITO_CDP_ENDPOINT | Адрес подключения к Browser API по CDP (только для Playwright и pyppeteer) |
| AVITO_URL | Необязательный --url по умолчанию |
Храните учётные данные в .env, а не в командной строке: аргументы запуска видны любому процессу на машине через ps и попадают в историю shell и в логи CI. Пароль с символами @, : или / работает в обоих форматах прокси. Адрес шлюза без логина (с доступом по белому списку IP) парсер не принимает: идентификатор сессии, который закрепляет IP, передаётся именно в логине.
Если вы используете Browser API, адрес подключения и прокси создаются вместе, чтобы удалённый браузер и сервис решения капчи работали с одного IP. В репозитории есть скрипт, который делает это одной командой и записывает оба значения в .env, не выводя их на экран:
bash
python3 tools/browser_profile_client.py accounts
python3 tools/browser_profile_client.py connection --account-id <id> --mint-session --write-env
Прокси-сессия действует ограниченное число минут (параметр -sessTime- в логине, по умолчанию 120). Когда она истекает, адрес подключения на её основе перестаёт работать и отвечает 500 proxy_error. Тогда достаточно повторить ту же команду.
python3 env_config.py показывает, какие переменные заданы, не выводя их значений.
Быстрый старт
bash
# Категория, три страницы, JSON и CSV
python3 playwright_scraper.py --mode listing \
--url "https://www.avito.ru/moskva/telefony/mobilnye_telefony/apple-ASgBAgICAkS0wA3OqzmwwQ2I_Dc" \
--pages 3 --out out/iphones
# Поиск
python3 playwright_scraper.py --mode listing --url "https://www.avito.ru/moskva?q=iphone+15" --pages 2 --out out/search
# Страницы объявлений из результата предыдущего запуска
python3 playwright_scraper.py --mode item --from out/iphones.json --max-products 20 --out out/items
# Профиль продавца и его объявления
python3 playwright_scraper.py --mode seller --url "https://www.avito.ru/brands/<seller>" --out out/seller
С параметром --verbose парсер для каждой страницы выводит, с какой проверкой он столкнулся и что помогло её пройти.
Каждый запуск создаёт <out>.json, <out>.csv или оба файла, а также два файла метаданных:
- <out>.meta.json описывает данные, сохранённые рядом с ним.
- <out>.latest_attempt.meta.json создаётся при любом исходе: успехе, блокировке, ошибке подключения или сбое — с полями exit_code и error_class.
Второй файл важен для автоматизации. Запуск, который ничего не нашёл, не перезаписывает предыдущий удачный результат пустым [] (отключается флагом --allow-empty). Поэтому именно по файлу попытки планировщик узнаёт, что последний запуск не удался.
robots.txt Avito запрещает параметры сортировки и фильтров (?s=, ?f=, ?radius=, ?view=) и путь /items/phone. Парсер отказывается работать с такими URL ещё до первого запроса и называет нарушенное правило. Сужайте выборку с помощью пути в URL, как это делают ссылки самого сайта: город → категория → подкатегория. Поиск ?q= и пагинация ?p= разрешены; в тестах простой ?p=2 вернул 50 новых объявлений.
Как работает парсер
Репозиторий разделяет два вопроса: когда загружать страницу и что на ней написано.
| Файл | Роль |
|---|---|
| playwright_scraper.py, puppeteer_scraper.py, selenium_scraper.py | Драйверы движков: запустить браузер, открыть страницу, вернуть HTML |
| browser_bridge.py | Общий для всех движков цикл запуска и параметры командной строки |
| page_flow.py | Правила для каждого состояния страницы: ждать, решать капчу, менять IP или останавливаться |
| product_parser.py | Всё, что репозиторий знает о разметке Avito |
| captcha_solver.py | Запросы к API решения капчи |
| proxy_pool.py | Разбор прокси, сессии с постоянным IP, смена IP |
| output_writer.py | Модель строки, запись JSON/CSV, метаданные запуска |
Порядок обработки одной страницы выдачи:
- Собрать URL страницы (?p=N, начиная со второй страницы).
- Открыть страницу и подождать (не дольше заданного времени), пока она загрузится. Для выдачи «готова» значит, что карточек больше одной ([data-marker="item"][data-item-id]): селектор может совпасть с одним посторонним элементом, похожим на карточку, ещё до того, как отрисуется сетка.
- Определить состояние документа: content, challenge, captcha, empty, blocked или unknown.
- Применить правило для этого состояния (см. следующий раздел).
- Разобрать карточки, убрать дубликаты по sku (Avito повторяет некоторые продвигаемые карточки на разных страницах), перейти к следующей странице.
- Остановиться на последней странице сайта, на странице без новых объявлений или при достижении --max-products.
Правила хранятся в page_flow.py как данные, поэтому три движка не могут разойтись в поведении:
python
STATE_POLICY = {
"content": {"retry": False, "solve": False, "wait": False, "rotate": False, "blocked": False},
"challenge": {"retry": False, "solve": False, "wait": True, "rotate": True, "blocked": True},
"captcha": {"retry": False, "solve": True, "wait": True, "rotate": True, "blocked": True},
"empty": {"retry": False, "solve": False, "wait": False, "rotate": False, "blocked": False},
"blocked": {"retry": False, "solve": False, "wait": False, "rotate": True, "blocked": True},
"unknown": {"retry": True, "solve": False, "wait": False, "rotate": False, "blocked": False},
}
Пустая выдача (empty) считается правильным ответом, а не ошибкой. Состояние blocked означает «попробовать с другого адреса», а не «повторить ещё раз».
Почему браузер, а не requests? Файрвол сразу отклоняет обычные HTTP-клиенты, а одно из его состояний — страница proof-of-work (проверка, при которой браузер должен выполнить вычислительную задачу), и пройти её может только браузер. Сами данные содержатся в HTML, который отдаёт сервер, поэтому после прохождения файрвола остаётся обычный разбор HTML и JSON.
Извлечение данных
Выдача: две разметки одного URL
Один и тот же URL выдачи приходит в одном из двух видов:
- Обычная разметка. Сетка есть только в DOM: 50 карточек на страницу, у каждой — микроразметка schema.org (itemprop="price", priceCurrency, name, url).
- Разметка плюс встроенный JSON. Те же карточки и блок <script type="mime/invalid" data-mfe-state>. В его catalog.items есть поля, которых нет в DOM: время публикации, старая цена товара со скидкой, тип продавца.
Парсер читает оба источника. Если JSON есть, данные берутся из него, а в каждой строке поле price_source указывает источник (state или dom):
python
def listing_state(html):
m = re.search(r"<script[^>]*data-mfe-state[^>]*>(.*?)</script>", html, re.S)
if not m:
return None
data = json.loads(m.group(1))
return data["loaderData"]["data"]["catalog"] # catalog["items"] — список объявлений
Из DOM данные берутся по атрибутам карточки и микроразметке:
python
for card in soup.select('[data-marker="item"][data-item-id]'):
sku = card["data-item-id"]
title = card.select_one('[data-marker="item-title"]') or card.select_one('[itemprop="name"]')
price = card.select_one('[itemprop="price"]') # атрибут `content`
location = card.select_one('[data-marker="item-location"]')
params = card.select_one('[data-marker="item-specific-params"]')
rating = card.select_one('[data-marker="seller-rating/score"]')
Карточка, которой нет в JSON, всё равно берётся из DOM, так что неполный JSON-блок не приводит к потере строк.
Ловушка с ценой
У товара со скидкой поле верхнего уровня priceDetailed.value в JSON выдачи (и item.price на странице объявления) содержит старую цену — например, 69 989, тогда как на странице показано 62 990. Цена, которую видит покупатель, находится в компоненте price блока PriceStep, а старая цена отдельно указана в компоненте discount:
python
for step in item["iva"]["PriceStep"]:
component = step["componentData"]["component"]
if component == "price":
price = step["payload"]["priceDetailed"]["value"] # показанная цена
elif component == "discount":
original_price = step["payload"]["old_string"] # например, "69 989 ₽"
discount_pct = step["payload"]["percent"]
В DOM микроразметка itemprop="price" уже содержит показанную цену. Все три источника — JSON, микроразметка DOM и страница объявления — совпали по показанной цене на 50 карточках из 50. Парсер никогда не вычисляет original_price: поле заполняется только значением, которое указал сам сайт, поэтому в строках из обычной разметки оно остаётся пустым.
Карточки, которые не являются результатами
На странице выдачи есть два вида карточек с той же разметкой, что и у настоящих результатов:
- Поиск без совпадений всё равно возвращает около пятидесяти карточек под заголовком «Похожие объявления рядом и в других городах». Парсер обрезает страницу по этому заголовку. Такой поиск завершается пустым результатом (код выхода 4), а не результатом, который выглядит полным, но относится к другому запросу.
- Карусель рекомендаций «Подобрали для вас» может находиться прямо внутри сетки результатов, и её карточки имеют ту же разметку data-marker="item". Парсер отбрасывает все карточки внутри [data-marker="itemsCarousel"]. В JSON-состоянии эти карточки лежат отдельным списком, так что фильтр нужен только при разборе DOM.
python
cards = [c for c in cards if c.find_parent(attrs={"data-marker": "itemsCarousel"}) is None]
Страницы объявлений
Страница объявления хранит данные в объекте начального состояния:
javascript
window.__staticRouterHydrationData = JSON.parse("…")
Это строковый литерал JavaScript, внутри которого находится JSON. Поэтому парсер декодирует его дважды: сначала литерал, затем JSON внутри него. После этого он берёт buyerItem из loaderData:
python
k = html.find("__staticRouterHydrationData")
q = html.find('JSON.parse("', k)
literal, _ = json.JSONDecoder().raw_decode(html, q + len("JSON.parse("))
data = json.loads(literal)
buyer_item = next(v["buyerItem"] for v in data["loaderData"].values()
if isinstance(v, dict) and "buyerItem" in v)
Цену, которую видит покупатель, на странице объявления содержит item.formattedPrice.value, а не item.price.
Профили продавцов
Страница продавца (/brands/<slug>) разбирается по DOM: имя, рейтинг, число отзывов, срок на сайте («На Авито с …»), подписчики и счётчик активных объявлений. Сам профиль показывает только первые 15 объявлений. Чтобы получить остальные, парсер открывает ленту продавца (/brands/<slug>/all?sellerId=…), нажимает кнопку «Показать все» и прокручивает страницу; каждая прокрутка подгружает ещё 15. 9 октября 2026 года так было собрано 353 объявления из 353 за 22 прокрутки, у всех строк есть цена. Если в профиле магазина нет sellerId, парсер берёт его со страницы /brands/<slug>/items (проверено 10 октября на магазине с 7331 объявлением).
Если собрано меньше объявлений, чем показывает счётчик профиля, запуск помечается как частичный (код выхода 6), а не полный: сравните listings_on_page с active_ads.
Оговорка про robots.txt. Парсер никогда не запрашивает пути, запрещённые в robots.txt Avito, но при прокрутке ленты скрипт самой страницы обращается к /web/1/profile/items, а /web/ для роботов закрыт. Это осознанное решение: другого способа получить объявления продавца сверх первых 15 нет. Чтобы оставаться только на странице профиля, используйте --max-products 15.
Работа с антибот-защитой Avito
Защита Avito — собственный файрвол со страницей «Доступ ограничен: проблема с IP». В тексте этой страницы сказано, почему она появляется: с этого адреса идёт очень много запросов. То есть файрвол принимает решение по частоте запросов с конкретного IP.
Репозиторий различает три состояния этой страницы:
| Что вы видите | HTTP | Что помогает |
|---|---|---|
| Страница proof-of-work, которая вызывает /web/3/firewallPow/… и сама перезагружается | 439 | Браузер проходит её сам, обычно за несколько секунд. Парсер ждёт до 25 с и ничего не нажимает и не перезагружает страницу: иначе проверка начнётся заново |
| Страница с капчей: «Продолжить» → пазл GeeTest v4 со слайдером | 429 | Решение, отправленное через собственную форму страницы |
| «подождите немного и обновите страницу» — капчи на странице нет | 429 | Другой IP-адрес. Эту страницу сайт показывает после отклонённого решения |
Третье состояние — не капча, и отправлять его на решение бессмысленно. Парсер помечает его как blocked и, если может, переходит на новый IP-адрес. Если ничего не делать, страница сама не исчезла в течение 140 с ни в одном из 3 тестов.
Признаки проверяются в порядке убывания надёжности:
python
WALL_TITLE = "Доступ ограничен"
WAIT_TEXT = "подождите немного и обновите страницу"
def page_state(html):
if WALL_TITLE not in title_of(html):
return "content" if has_listing_cards(html) else ...
if re.search(r"<form\b[^>]*\bjs-firewall-form\b", html):
return "captcha" # на странице есть форма капчи файрвола
if WAIT_TEXT in html:
return "blocked" # решение отклонено: нужен новый IP
return "challenge" # proof-of-work: браузер пройдёт его сам
Стоит обратить внимание на две детали:
- Ищите элемент формы, а не подстроку. Скрипт страницы файрвола упоминает .js-firewall-form в querySelector независимо от того, есть ли форма на странице.
- Не ищите в HTML слово «captcha». Browser API добавляет свои скрипты …/captcha/… на каждую загружаемую страницу, и такая проверка считала бы заблокированной любую из них.
Поскольку файрвол реагирует на частоту запросов с одного адреса, парсер не разрешает --concurrency больше 1 и по умолчанию выдерживает паузу 2 с между страницами (--delay).
Использование прокси
Поддержка прокси уже есть в репозитории. AVITO_PROXY — это один URL прокси:
text
http://user:pass@host:port
Для шлюза резидентных прокси RuCaptcha регион и сессия с постоянным IP задаются прямо в логине:
text
http://{login}-zone-custom-region-RU-session-{id}-sessTime-120:{password}@<хост-шлюза>:<порт>
Как используется прокси, зависит от того, где работает браузер:
-
Локальный браузер: через прокси идёт весь трафик браузера. Учётные данные Playwright получает в собственных полях proxy, а не в аргументе --proxy-server=, поэтому они не попадают в командную строку браузера:
pythonlaunch["proxy"] = {"server": "http://host:port", "username": user, "password": password} browser = pw.chromium.launch(**launch)pyppeteer передаёт их через page.authenticate. Chrome под Selenium вообще не умеет передавать пароль прокси, поэтому парсер направляет его в proxy_forwarder.py — локальный прокси внутри процесса парсера, который сам добавляет учётные данные. Без -session- в логине шлюз менял бы IP при каждом запросе, поэтому парсер автоматически закрепляет одну сессию на весь запуск.
-
Browser API: удалённый браузер работает со своим IP-адресом. AVITO_PROXY в этом случае нужен только для решения капчи и должен совпадать с сессией, для которой создан адрес подключения.
Если страница в локальном браузере так и остаётся заблокированной, парсер запускает новый браузер с новым IP-адресом (--proxy-block-retries, по умолчанию 1). Парсер никогда не переносит активную сессию на другой IP: если cookies, выданные файрволом одному адресу, начинают приходить с другого, это выглядит подозрительнее, чем любой из адресов по отдельности. Новые IP-адреса берутся из списка прокси --proxy-file или, для шлюза RuCaptcha, из новых сессий. --proxy-rotate per-page берёт новый IP-адрес для каждой страницы, но ценой перезапуска браузера.
Работа с капчей (GeeTest v4)
С 6 по 9 октября 2026 года файрвол на всех запросах показывал GeeTest v4 (слайдер), в том числе при запросе новой картинки на самой странице. Ниже описано, как её решает парсер. Параметры задачи описаны в документации по GeeTest.
1. Обнаружение
Страница файрвола содержит блоки для трёх типов капчи, и скрипт страницы показывает тот, который выбрал сервер. wall_variant() возвращает первый блок без display: none. Для GeeTest это #geetest_captcha, при этом на странице также должен быть идентификатор капчи.
2. Параметры
Для GeeTest v4 нужен один параметр — captcha_id. Он указан во встроенном скрипте страницы файрвола:
python
captcha_id = re.search(r"captchaId\s*=\s*['\"]([0-9a-f]{32})['\"]", html).group(1)
3. Отправка на решение — с IP браузера
От этой детали зависит результат. Если капча решена не с IP браузера, Avito отклоняет токен: в тесте без прокси браузера сайт ответил новой капчей. Поэтому задача отправляется как GeeTestTask с параметрами прокси — той же сессии с постоянным IP, через которую работает браузер. Задача создаётся методом createTask, а результат запрашивается методом getTaskResult:
python
task = {
"type": "GeeTestTask",
"websiteURL": page_url,
"version": 4,
"initParameters": {"captcha_id": captcha_id},
"proxyType": "http",
"proxyAddress": proxy_host,
"proxyPort": proxy_port,
"proxyLogin": proxy_login,
"proxyPassword": proxy_password,
}
task_id = requests.post("https://api.rucaptcha.com/createTask",
json={"clientKey": API_KEY, "task": task}).json()["taskId"]
while True:
time.sleep(5)
result = requests.post("https://api.rucaptcha.com/getTaskResult",
json={"clientKey": API_KEY, "taskId": task_id}).json()
if result.get("status") == "ready":
solution = result["solution"]
# {captcha_id, lot_number, pass_token, gen_time, captcha_output}
break
По умолчанию репозиторий не создаёт задачу без прокси (solve_geetest_v4 в этом случае возвращает ошибку): лучше сообщить, что страница заблокирована, чем платить за токен, который сайт отклонит. Результат парсер ждёт до 240 с — дольше, чем сервис может потратить, прежде чем признать сложный пазл нерешаемым. При меньшем таймауте парсер перестал бы ждать раньше, чем стала бы известна причина неудачи. На одну страницу за запуск покупается не больше одного решения: если страница осталась заблокированной, её загружают заново с нового IP, но второй токен для неё не покупают.
4. Передача решения через форму страницы
Вместо прямого запроса к адресу проверки Avito парсер записывает решение туда, куда его записал бы код самой страницы, и вызывает собственный обработчик отправки формы. Обработчик добавляет нужные сайту заголовки, отправляет запрос на /web/3/firewallCaptcha/verify и перезагружает страницу — так же, как при решении капчи человеком:
python
value = json.dumps({"captcha_id": captcha_id, "lot_number": solution["lot_number"],
"pass_token": solution["pass_token"], "gen_time": solution["gen_time"],
"captcha_output": solution["captcha_output"]})
page.evaluate("""value => {
const inp = document.querySelector('input[name=captcha-response]');
const form = document.querySelector('.js-firewall-form');
inp.value = value;
form.dispatchEvent(new Event('submit'));
}""", value)
Затем парсер проверяет, что пришло в ответ:
- Содержимое страницы (карточки, объявление, профиль или действительно пустая выдача): решение принято.
- Страница «подождите…»: решение отклонено, парсер переходит на новый IP-адрес.
- Новая капча: на этот раз решение не принято. Страница остаётся заблокированной, и запуск сообщает об этом.
Исчезновение страницы файрвола ещё не означает успеха: во время перезагрузки страница проходит через промежуточные документы. Пока идёт навигация, page.content() в Playwright может даже выбросить исключение «page is navigating», поэтому во время ожидания читайте HTML внутри try.
Что показывают замеры
Правильное решение не гарантирует, что Avito примет сессию. Результаты живых запусков репозитория:
| Условия | Результат |
|---|---|
| Browser API, GeeTestTask через IP самого браузера (6 октября) | 3 из 3 принято, 36–73 с, $0.00299 за решение |
| Локальный Chromium, тот же способ (6 октября) | 2 из 3 принято (одно — ERROR_CAPTCHA_UNSOLVABLE) |
| Решение без прокси браузера | 0 из 1 — сайт ответил новой капчей |
| Локальный Chromium, все решения с 14 по 20 октября | 0 из 15 принято — каждый раз возвращалась капча или страница «подождите…» |
| Browser API, 11 запусков во всех режимах (19 октября) | Файрвол не встретился ни разу, решения не покупались |
Почему решения для локального браузера перестали приниматься, не доказано. В контрольной проверке 14 октября GeeTestTask вернул решение даже для прокси с неверным паролем и для несуществующего хоста — то есть сервис решения, судя по всему, не использовал прокси браузера, а токен с другого IP — наиболее вероятная причина отказа. Практический вывод от этого не меняется: для Avito используйте Browser API. После того как профиль один раз прошёл файрвол, сохранённые cookies пропускают его и на следующих страницах.
Запасные варианты: картинка и hCaptcha
В коде страницы файрвола предусмотрены ещё два типа капчи: собственная картинка Avito и hCaptcha. В тестах ни одна из них не появлялась, но парсер обрабатывает обе на случай, если сервер начнёт их выдавать:
- Капча-картинка (#inner-captcha): изображение приходит как data: URL и решается задачей ImageToTextTask. Ответ записывается в #form-input, затем форма отправляется так же.
- hCaptcha (#h-captcha): ключ сайта (sitekey) берётся из атрибута data-sitekey на странице, капча решается задачей HCaptchaTask через IP-адрес браузера. Токен записывается в #h-captcha-response.
Поле transport_facts.solves[].variant в метаданных запуска показывает, какой тип встретился (geetest, image или hcaptcha). Если там окажется один из двух последних вариантов, значит, сайт изменил поведение и это стоит проверить.
Использование RuCaptcha Browser API (CDP)
Вместо запуска локального Chromium движки Playwright и pyppeteer могут подключаться к удалённому браузеру по CDP:
python
browser = pw.chromium.connect_over_cdp(cdp_endpoint)
context = browser.contexts[0] if browser.contexts else browser.new_context()
page = context.pages[0] if context.pages else context.new_page()
Если в .env задан AVITO_CDP_ENDPOINT, парсер подключается так автоматически. Команды из раздела «Быстрый старт» не меняются. Настройка и параметры сессии описаны в документации Browser API.
Для Avito авторы репозитория рекомендуют именно этот вариант:
- Профиль сохраняется между запусками. После того как профиль один раз прошёл файрвол, сохранённые cookies позволяют проходить его и при следующих запросах: 6 октября десяток страниц выдачи, объявлений и продавцов подряд пришли без повторной проверки, а 19 октября 11 запусков во всех режимах ни разу не встретили файрвол.
- Страницу proof-of-work удалённый браузер проходит сам.
- У удалённого браузера собственный отпечаток, поэтому парсер не задаёт user agent.
При подключении парсер также включает встроенное автоматическое решение капчи удалённого браузера (Captcha.setAutoSolve) и даёт ему 25 с. На GeeTest Avito оно не сработало (0 из 3, 6 октября), поэтому парсер переходит к описанной выше задаче GeeTestTask. Именно поэтому AVITO_PROXY должен совпадать с сессией, для которой создан адрес подключения.
Код учитывает несколько особенностей Browser API:
- Одно активное подключение на профиль. Второе подключение получает ответ 500 profile_locked, пока первое не завершится; парсер повторяет попытку 3 раза с интервалом 10 с. После корректного завершения профиль освобождается быстро, после принудительного прерывания процесса — заметно дольше. Драйвер всегда закрывает подключение в конце работы.
- Адрес подключения перестаёт работать вместе с прокси-сессией. При попытке подключения сервер возвращает 500 proxy_error; создайте адрес подключения заново командой browser_profile_client.py connection --mint-session --write-env.
- Профиль тоже может попасть под лимит. Сохраняйте паузу между страницами по умолчанию.
- Selenium не поддерживает Browser API. В Chromedriver некуда передать учётные данные для подключения, поэтому движок Selenium работает только с локальным Chrome.
Масштабирование парсера
Репозиторий даёт базовые механизмы, а управление запусками в рабочем проекте придётся выстроить самостоятельно:
-
Для параллельной работы нужно больше IP-адресов, а не больше потоков. --concurrency больше 1 не разрешён: параллельные запросы с одного адреса заставляют файрвол появляться чаще. Профиль Browser API к тому же допускает только одно подключение. Чтобы работать быстрее, запускайте отдельные процессы, каждый со своим IP-адресом или профилем.
-
Сохраняйте паузы. --delay по умолчанию — 2 с между страницами. В репозитории не указана «безопасная» частота запросов для Avito, поэтому измеряйте на собственном трафике, как часто появляется файрвол.
-
Переиспользуйте сессии. Для регулярного мониторинга выгоднее использовать профиль Browser API, который уже прошёл файрвол, чем создавать новую сессию для каждого запуска.
-
Отличайте таймаут от отказа. При таймауте навигации запрос повторяется с того же IP (--retries, --retry-delay). При нерабочем прокси (ERR_CONNECTION_RESET, ERR_PROXY_…) или заблокированной странице берётся новый IP-адрес (--proxy-block-retries).
-
Используйте коды выхода:
Код Значение 0 Полный результат 1 Сбой — ошибка в коде, а не на сайте или в подключении 2 Неверный запуск — не тот сайт или режим, URL запрещён в robots.txt или не читается файл --from 3 Заблокировано — страница файрвола не исчезла после всех попыток 4 Нет результатов — выдача прочитана до конца и пуста, в том числе поиск без совпадений 5 Контент не получен — таймаут, нерабочий прокси, profile_locked после 3 попыток, proxy_error, страница объявления без объявления или профиль продавца, который не удалось разобрать 6 Частичный результат — часть страниц получена, затем работа остановлена; в режиме seller — объявлений меньше, чем показывает счётчик профиля Коды 3 и 5 требуют разных действий: 5 — это не блокировка файрволом: нужно создать адрес подключения заново, повторить запуск или проверить URL.
-
Проверяйте полноту. Поле status в <out>.meta.json принимает значения complete, partial или failed. В transport_facts перечислены все встреченные страницы файрвола, шаг, который помог их пройти, каждое решение капчи с указанием времени, стоимости и task_id, а также каждая смена IP.
-
Сравнивайте запуски корректно. diff_runs.py --old A.json --new B.json показывает добавленные, удалённые и изменённые объявления по sku. Неполные запуски, запуски в разных режимах, по другой выдаче, с другим числом страниц или другим --max-products он не сравнивает (код выхода 2; --force сравнивает всё равно): иначе разница выглядела бы как «исчезновение» объявлений. Если одна и та же карточка в двух запусках пришла в разных видах разметки, он не считает это изменением цены.
-
«Удалено» значит «больше нет на этих страницах», а не «снято с публикации». Avito постоянно меняет порядок выдачи: объявления поднимают и добавляют новые. Два запуска одной категории на 3 страницы 19 октября разошлись на 47 добавленных и 42 удалённых из 145 — объявления просто ушли дальше третьей страницы. Чтобы отличить одно от другого, запустите --mode item для «удалённых»: снятое объявление завершится с кодом 5.
Устранение проблем
| Проблема | Возможная причина | Решение |
|---|---|---|
| HTTP 429 у requests/curl | Файрвол Avito отклоняет обычные HTTP-клиенты | Используйте браузерный движок: через обычный HTTP данные не получить |
| Код 3, на странице кнопка «Продолжить» и пазл со слайдером | Капча GeeTest v4 | Задайте TWOCAPTCHA_KEY и AVITO_PROXY; парсер решит её через форму страницы |
| В логе решения "error": "no proxy for the solver" | Нет прокси для решения капчи | Задайте AVITO_PROXY. При работе через CDP это должна быть сессия, для которой создан адрес подключения |
| Капча решена, но страница файрвола вернулась или появилось «подождите немного и обновите страницу» | Avito отклонил решение для этой сессии/IP | В локальном браузере с 14 октября это ожидаемый результат: переходите на Browser API. При работе через CDP создайте адрес подключения и прокси заново |
| Прокси и адрес подключения не совпадают | Одно из значений создано заново, другое — нет | tools/browser_profile_client.py connection --mint-session --write-env |
| ERROR_CAPTCHA_UNSOLVABLE | Сервис не смог решить этот пазл | Запустите снова: с нового IP-адреса придёт новый пазл. См. коды ошибок |
| Код 5, 500 … proxy_error при подключении | Истекла прокси-сессия, для которой создан адрес подключения | Создайте адрес подключения заново |
| Код 5, 500 … profile_locked | Профиль ещё занят предыдущим подключением | Дождитесь освобождения; не подключайтесь к нему повторно в это время |
| Код 4 при поиске | Совпадений нет — Avito показал «Похожие объявления…» | Это ожидаемое поведение: ответ правильный |
| Код 2, robots.txt says Disallow | В URL есть ?s= / ?f= / ?radius= / ?view= | Сужайте выборку с помощью пути в URL: город → категория → подкатегория |
| Код 6 в режиме seller | Объявлений записано меньше, чем показывает счётчик профиля | Сравните listings_on_page с active_ads; причина остановки — в transport_facts.seller_feed |
| Пустые original_price, published_at, delivery | Строка получена из обычной разметки (price_source = dom) | Это ожидаемо; для таких объявлений запустите --mode item |
| city часто пустой | В обычной разметке город есть только в alt-тексте некоторых фото (7 карточек из 50) | Группируйте по region_slug: он есть в каждой строке |
| Пустой seller_name | Частный продавец: Avito скрывает имена таких продавцов | Это ожидаемо |
Пример результата
Строка выдачи (--mode listing) из запуска на 3 страницы 6 октября 2026 года. Она получена из обычной разметки, поэтому поля, которые есть только в JSON, равны null; имя продавца и его идентификатор в адресе заменены условными значениями:
json
{
"source": "avito.ru",
"scraped_at": "2026-10-06T11:43:07.634024+00:00",
"url": "https://www.avito.ru/moskva/telefony/iphone_17_256_gb_sim_esim_8212942358",
"sku": "8212942358",
"title": "iPhone 17, 256 ГБ, SIM + eSIM",
"image_url": "https://…img.avito.st/image/1/…",
"price": 64935.0,
"currency": "RUB",
"category": null,
"price_source": "dom",
"original_price": null,
"discount_pct": null,
"region_slug": "moskva",
"city": "Москва",
"location": "Багратионовская, 6–10 мин.",
"summary_params": "Новый",
"published_at": null,
"published_text": null,
"seller_name": "Продавец 1",
"seller_type": null,
"seller_id": "seller1",
"seller_url": "https://www.avito.ru/brands/seller1",
"seller_rating": 5.0,
"seller_reviews": 624,
"promoted": true,
"badges": ["Реквизиты проверены", "яПомогаю"],
"delivery": null,
"page": 1,
"position": 1
}
Строка объявления (--mode item, в сокращении) дополнительно содержит, например, все характеристики:
json
{
"sku": "7924452716",
"price": 39990.0,
"original_price": null,
"params": {
"Состояние": "Новое",
"Производитель": "Apple",
"Модель": "iPhone 15",
"Встроенная память": "128 ГБ",
"SIM-карты": "SIM + eSIM",
"Цвет": "Розовый"
},
"views_total": 4503,
"seller_type": "company",
"is_active": true
}
Метаданные запуска (<out>.meta.json, в сокращении):
json
{
"source": "avito.ru",
"mode": "listing",
"status": "complete",
"stop_reason": "completed",
"pages_requested": 3,
"pages_completed": 3,
"pages_failed": [],
"products": 147,
"transport_facts": {
"over_cdp": true,
"walls": [],
"solves": [],
"rotations": 0
}
}
Полный компактный пример
Ниже — основная логика репозитория, собранная в одном файле. Скрипт открывает выдачу в Playwright (через Browser API по CDP или локально через прокси), определяет состояние файрвола, решает капчу GeeTest v4 через IP-адрес браузера, если файрвол её показал, и разбирает карточки с учётом ловушки с ценой, карусели рекомендаций и блока «Похожие объявления». Пагинации, смены IP, обработки страниц объявлений и продавцов, а также файлов метаданных здесь нет: для этого используйте репозиторий. С учётом замеров выше запускайте пример с заданным AVITO_CDP_ENDPOINT: в локальном браузере шаг с капчей, скорее всего, не будет принят.
python
"""Минимальный парсер выдачи avito.ru — ядро 2scraper/avito-scraper в одном файле.
pip install playwright beautifulsoup4 requests && playwright install chromium
export AVITO_PROXY='http://user:pass@host:port' # российский резидентный, с постоянным IP
export TWOCAPTCHA_KEY='...' # нужен, только если Avito покажет GeeTest
python3 avito_minimal.py "https://www.avito.ru/moskva?q=iphone+15"
Чтобы работать через удалённый браузер вместо локального Chromium, задайте AVITO_CDP_ENDPOINT.
"""
import json
import os
import re
import sys
import time
from urllib.parse import urlparse
import requests
from bs4 import BeautifulSoup
from playwright.sync_api import sync_playwright
WALL_TITLE = "Доступ ограничен"
WAIT_WALL_TEXT = "подождите немного и обновите страницу"
CARD = '[data-marker="item"][data-item-id]'
# ------------------------------------------------------- состояние страницы
def page_state(html: str) -> str:
title = re.search(r"<title[^>]*>(.*?)</title>", html, re.S)
if not title or WALL_TITLE not in title.group(1):
return "content" if 'data-marker="item"' in html else "unknown"
if re.search(r"<form\b[^>]*\bjs-firewall-form\b", html):
return "captcha" # GeeTest v4 за кнопкой «Продолжить»
if WAIT_WALL_TEXT in html:
return "blocked" # приходит после отклонённого решения: нужен другой IP
return "challenge" # proof-of-work, браузер проходит его сам
# ---------------------------------------------------------- решение GeeTest
def solve_geetest(api_key: str, captcha_id: str, page_url: str, proxy: str) -> dict:
p = urlparse(proxy)
task = {"type": "GeeTestTask", "websiteURL": page_url, "version": 4,
"initParameters": {"captcha_id": captcha_id},
# Решать нужно с IP браузера, иначе Avito отклонит токен.
"proxyType": p.scheme, "proxyAddress": p.hostname, "proxyPort": p.port,
"proxyLogin": p.username, "proxyPassword": p.password}
r = requests.post("https://api.rucaptcha.com/createTask",
json={"clientKey": api_key, "task": task}, timeout=30).json()
if r.get("errorId"):
raise RuntimeError(r.get("errorCode"))
for _ in range(48): # до 240 с
time.sleep(5)
res = requests.post("https://api.rucaptcha.com/getTaskResult",
json={"clientKey": api_key, "taskId": r["taskId"]}, timeout=30).json()
if res.get("errorId"):
raise RuntimeError(res.get("errorCode"))
if res.get("status") == "ready":
return res["solution"]
raise TimeoutError("нет решения за 240 с")
SUBMIT_JS = """value => {
const inp = document.querySelector('input[name=captcha-response]');
const form = document.querySelector('.js-firewall-form');
if (!inp || !form) return false;
inp.value = value;
form.dispatchEvent(new Event('submit')); // обработчик страницы сам отправит /verify
return true;
}"""
def content(page) -> str:
try:
return page.content()
except Exception: # страница в процессе перезагрузки
return ""
def pass_wall(page, url: str, proxy: str) -> str:
html = content(page)
for _ in range(25): # proof-of-work: просто ждём
if page_state(html) != "challenge":
break
page.wait_for_timeout(1000)
html = content(page)
if page_state(html) != "captcha":
return html
key = os.environ.get("TWOCAPTCHA_KEY")
captcha_id = re.search(r"captchaId\s*=\s*['\"]([0-9a-f]{32})['\"]", html)
if not (key and proxy and captcha_id):
return html
sol = solve_geetest(key, captcha_id.group(1), url, proxy)
value = json.dumps({k: sol.get(k) for k in
("lot_number", "pass_token", "gen_time", "captcha_output")}
| {"captcha_id": captcha_id.group(1)})
page.evaluate(SUBMIT_JS, value)
for _ in range(20): # ждём перезагрузки
page.wait_for_timeout(1500)
html = content(page) or html
if page_state(html) in ("content", "blocked"):
break
return html
# ------------------------------------------------------------------ парсинг
def listing_state(html: str) -> dict:
m = re.search(r"<script[^>]*data-mfe-state[^>]*>(.*?)</script>", html, re.S)
if not m:
return {}
try:
catalog = json.loads(m.group(1))["loaderData"]["data"]["catalog"]
except (ValueError, KeyError, TypeError):
return {}
return {str(i["id"]): i for i in catalog.get("items", []) if i.get("type", "item") == "item"}
def prices(item: dict):
"""(показанная цена, старая цена). Поле `priceDetailed` верхнего уровня — цена ДО скидки;
цена, которую видит покупатель, лежит в компоненте "price" блока `PriceStep`."""
shown, old = (item.get("priceDetailed") or {}).get("value"), None
for step in (item.get("iva") or {}).get("PriceStep") or []:
component = (step.get("componentData") or {}).get("component")
if component == "price":
shown = step["payload"]["priceDetailed"]["value"]
elif component == "discount":
digits = re.sub(r"\D", "", step["payload"].get("old_string") or "") # "69 989 ₽"
old = float(digits) if digits else None
return shown, old
def parse_listing(html: str) -> list:
soup = BeautifulSoup(html, "html.parser")
state = listing_state(html)
# Поиск без совпадений всё равно отдаёт карточки — под заголовком «Похожие объявления…».
cut = [i for i in (html.find("extraTitle-"), html.find("Похожие объявления")) if i >= 0]
results = set(re.findall(r'data-item-id="(\d+)"', html[:min(cut)])) if cut else None
rows, seen = [], set()
for card in soup.select(CARD):
if results is not None and card["data-item-id"] not in results:
continue # «похожие» объявления, а не результаты
if card.find_parent(attrs={"data-marker": "itemsCarousel"}):
continue # «Подобрали для вас» — не результаты
sku = card["data-item-id"]
if sku in seen:
continue
seen.add(sku)
if sku in state:
item = state[sku]
price, old = prices(item)
rows.append({"sku": sku, "title": item.get("title"),
"price": price, "original_price": old, "price_source": "state",
"url": "https://www.avito.ru" + item.get("urlPath", "").split("?")[0]})
else:
title = card.select_one('[itemprop="name"]')
price = card.select_one('[itemprop="price"]')
link = card.select_one('a[itemprop="url"], a[data-marker="item-title"]')
rows.append({"sku": sku,
"title": title.get_text(" ", strip=True) if title else None,
"price": float(price["content"]) if price and price.get("content") else None,
"original_price": None, "price_source": "dom",
"url": "https://www.avito.ru" + link["href"].split("?")[0] if link else None})
return rows
# ---------------------------------------------------------------------- main
def main(url: str) -> int:
proxy = os.environ.get("AVITO_PROXY")
cdp = os.environ.get("AVITO_CDP_ENDPOINT")
with sync_playwright() as pw:
if cdp:
browser = pw.chromium.connect_over_cdp(cdp)
context = browser.contexts[0] if browser.contexts else browser.new_context()
else:
launch = {"headless": True}
if proxy:
p = urlparse(proxy)
launch["proxy"] = {"server": f"{p.scheme}://{p.hostname}:{p.port}",
"username": p.username, "password": p.password}
browser = pw.chromium.launch(**launch)
context = browser.new_context(locale="ru-RU")
page = context.pages[0] if context.pages else context.new_page()
page.goto(url, wait_until="domcontentloaded", timeout=60_000)
try:
page.wait_for_function(
"() => document.title.includes('Доступ ограничен') || "
"document.querySelectorAll('[data-marker=\"item\"][data-item-id]').length > 1",
timeout=20_000)
page.wait_for_load_state("load", timeout=20_000) # выдача продолжает догружаться после первых карточек
except Exception:
pass
html = pass_wall(page, url, proxy)
state = page_state(html)
browser.close()
if state != "content":
print(f"заблокировано: состояние страницы {state!r}", file=sys.stderr)
return 3
rows = parse_listing(html)
print(json.dumps(rows[:5], ensure_ascii=False, indent=2))
print(f"строк: {len(rows)}", file=sys.stderr)
return 0 if rows else 4
if __name__ == "__main__":
sys.exit(main(sys.argv[1] if len(sys.argv) > 1 else
"https://www.avito.ru/moskva?q=iphone+15"))
Если страница всё ещё заблокирована, пример завершается с кодом 3. В рабочем проекте в этот момент нужно запустить новый браузер с другим IP-адресом — это и делает функция fetch_with_rotation в репозитории.
Тестирование и ограничения
В репозитории есть набор офлайн-тестов, которым не нужны ни сеть, ни браузер:
bash
python3 smoke_test.py
python3 -m pytest -q
Тесты проверяют часть логики, не зависящую от сети, на фрагментах реальных страниц: распознавание всех трёх состояний файрвола, обе разметки выдачи, ловушку с ценой, разбор объявлений и продавцов, сборку задач для решения капчи, работу с прокси и запись результатов и метаданных. CI также прогоняет их для каждого движка в отдельном окружении и собирает Docker-образ. Ежедневный контрольный запуск парсит три страницы через Browser API с российского IP и считается успешным, только если запуск полный и строки прошли проверки данных.
Живые запуски, Москва, категория «Мобильные телефоны → Apple», 6 октября 2026 года:
| Запуск | Результат |
|---|---|
| Playwright, Browser API, --mode listing --pages 3 | 147 строк, 3 страницы, код 0 |
| Playwright, Browser API, --mode item, 5 объявлений | 5 из 5, код 0 |
| Playwright, Browser API, --mode seller | Профиль + 15 объявлений, код 0 |
| pyppeteer, Browser API, --pages 2 | 98 строк, код 0 |
| Selenium, локальный Chrome, --pages 2 | 99 строк, код 0 — на второй странице встретился файрвол: proof-of-work не прошёл за 25 с, с нового IP появилась GeeTest, решена за 92 с |
Поиск из «Быстрого старта» (?q=iphone+15 --pages 2), 10 октября 2026 года:
| Запуск | Результат |
|---|---|
| Playwright, Browser API | 100 строк, код 0 |
| Playwright, локальный Chromium через прокси | 100 строк, код 0 — proof-of-work пройден за 1 с, затем GeeTest: первый токен отклонён, токен с нового IP принят |
Ограничения, которые стоит учитывать:
- Локальный браузер получает капчу GeeTest в каждой новой сессии, а с 14 по 20 октября ни одно из 15 его решений не было принято. Используйте Browser API.
- Обработка капчи-картинки и hCaptcha написана по коду страницы файрвола, но в тестах эти капчи не появлялись.
- Загрузка ленты продавца опирается на запрос самой страницы к /web/1/profile/items — пути, закрытому в robots.txt (см. «Профили продавцов»).
- Пороги срабатывания файрвола нигде не опубликованы, и в репозитории они не указаны.
FAQ
Законно ли парсить Avito?
Парсер читает только публичные страницы: не входит в аккаунт, не открывает контакты и не пишет продавцам. Для каждого запрашиваемого URL он соблюдает robots.txt, кроме одного исключения с лентой продавца, описанного выше. В выгрузке есть имена продавцов, поэтому на неё распространяется закон о персональных данных — в России это 152-ФЗ. Проверьте также условия использования Avito. Это не юридическая консультация.
Можно ли парсить Avito без браузера?
Нет. Обычные HTTP-клиенты получили HTTP 429 в 6 попытках из 6, а страницу proof-of-work может пройти только браузер.
Какую капчу использует Avito?
На всех измеренных запросах файрвол показывал GeeTest v4. На странице также есть собственная капча-картинка Avito и hCaptcha: парсер умеет их решать, но в тестах они не появлялись.
Сколько стоит решение капчи?
В запусках репозитория решение GeeTest v4 стоило $0.00299, и парсер покупает не больше одного решения на страницу за запуск. С профилем Browser API, который уже прошёл файрвол, большинству запусков решения не нужны: 11 запусков 19 октября не купили ни одного.
Зачем нужен российский IP?
Файрвол принимает решение по IP и частоте запросов, а сервис решения капчи должен использовать тот же IP, что и браузер. Российский резидентный прокси с постоянной сессией держит обоих на одном адресе.
Заключение
Парсинг Avito сводится к двум вещам: брать данные там, где их на самом деле хранит страница, и считать файрвол обычной частью рабочего процесса.
Что касается данных: карточки выдачи берутся из встроенного JSON data-mfe-state, если он есть на странице, иначе — из микроразметки DOM. Данные страниц объявлений — из __staticRouterHydrationData. Всегда берите показанную цену, а не поле price верхнего уровня. Карусели рекомендаций и блоки «Похожие объявления» выглядят как результаты, но ими не являются.
Что касается доступа: через обычный HTTP данные Avito не получить. Нужен браузер с российским IP-адресом. Страницу proof-of-work браузер проходит сам, решение GeeTest v4 нужно отправлять через форму страницы, и оно засчитывается, только если получено с IP самого браузера. Страница «подождите…» — вовсе не капча, а сигнал сменить IP-адрес.
С учётом последних замеров используйте постоянный профиль Browser API даже для разовой выгрузки: решения для локального браузера перестали приниматься. Для регулярного мониторинга добавьте к нему метаданные запусков и diff_runs.py, чтобы временная блокировка или изменившийся порядок выдачи не выглядели в данных как исчезновение объявлений.
С чего начать: зарегистрируйтесь в RuCaptcha, чтобы получить API-ключ, и попробуйте RuCaptcha Browser API.
Исходный код
- Репозиторий: github.com/2scraper/avito-scraper (MIT)
- Все парсеры: github.com/2scraper
Документация
- Quick start: API-ключ, первый запрос, первый результат.
- GeeTest: параметры GeeTestTask, в том числе для версии 4.
- createTask и getTaskResult: два вызова, на которых строится любое решение.
- Коды ошибок и лимиты запросов.
- Документация Browser API: адрес подключения по CDP и параметры профиля.
- Proxy API: генерация списков резидентных прокси.
- 2captcha-python: официальный Python SDK.
Похожие руководства
- Как парсить SHEIN: решение сеток и кликов по иконкам на шлюзе проверки SHEIN.
- Как парсить Transfermarkt: AWS WAF и ловушки пагинации.
- Как парсить Google Play: рейтинги по странам и полная история отзывов.
- Как парсить Lidl: сетки товаров и цены за единицу.
- Как парсить YouTube: комментарии и метаданные через Innertube API.