← Назад в базу знаний

Массовый парсинг Wildberries через IPv6 прокси: как обходить rate-limit без капчи

Массовый парсинг Wildberries через IPv6 прокси: как обходить rate-limit без капчи

Почему Wildberries вообще упирается в лимиты

Wildberries отдаёт каталог через публичный JSON API на `card.wb.ru` и `static-basket-01.wb.ru`. Формально это открытые endpoint'ы, авторизация не нужна. На практике при превышении порога запросов с одного IP начинается деградация: сначала растёт `latency`, потом прилетает `429`, потом `403` с залипшим баннером на 10-30 минут.

Порог не публикуется и плавает. По наблюдениям за 2024-2025 годы, с одного IPv4-адреса комфортно проходит 60-90 запросов в минуту к `/catalog`. Дальше — рандом. Иногда 200 запросов пролетают, иногда на 70-м уже `429`. Это не баг, а защита от скрейпинга, построенная на скользящем окне и репутации подсети.

Ключевой момент: Wildberries смотрит не только на частоту, но и на **разнообразие** запросов. Если с одного IP идут запросы только к `/catalog` без переходов на карточки товаров, без загрузки картинок — это палево. Нормальный пользователь так не делает.

Чем IPv6 меняет расклад

IPv4-адресов у провайдеров мало, они дорогие и их выдают блоками /29 или /28. IPv6-префикс /64 — это 18 квинтиллионов адресов. Один сервер с выделенным /64 может выставить миллиарды исходящих IP без NAT.

Для парсинга это означает: можно раскидать нагрузку по тысячам адресов, каждый из которых для Wildberries выглядит как отдельный пользователь. Rate-limit привязан к IP, а не к подсети — иначе они бы забанили половину мобильных операторов.

Проверить, что у вас реально работает IPv6:

```bash

curl -6 -s https://ifconfig.co

должен вернуть адрес вида 2a01:4f8:...

curl -6 -s -o /dev/null -w "%{http_code} %{time_total}\n" https://card.wb.ru/cards/v1/detail?appType=1\&curr=rub\&dest=-1257786\&nm=12345678

```

Если второй curl отдаёт `000` или таймаутит — у вас нет IPv6-маршрута, и вся затея мертва. Провайдеры вроде Hetzner, OVH, Vultr дают /64 бесплатно к любой VPS. Российские хостеры часто IPv6 не дают вообще.

Архитектура пула адресов

Схема простая: берём /64, разбиваем на адреса, каждый запрос уходит с нового source IP. Ядро Linux умеет это из коробки, если правильно настроить маршрутизацию.

```bash

ip -6 route add local 2a01:4f8:1c1c:1234::/64 dev lo

sysctl -w net.ipv6.ip_nonlocal_bind=1

```

Первая команда говорит ядру, что весь префикс локальный — можно биндиться на любой адрес из него. Вторая разрешает bind на адреса, которых физически нет на интерфейсе. Без неё `curl --interface` упадёт с `Cannot assign requested address`.

Дальше в Python выбираем случайный адрес из префикса:

```python

import random, ipaddress, httpx

PREFIX = ipaddress.IPv6Network("2a01:4f8:1c1c:1234::/64")

def rand_ip():

return str(PREFIX.network_address + random.randint(1, 2**64 - 2))

async def fetch(nm: int):

ip = rand_ip()

transport = httpx.AsyncHTTPTransport(local_address=ip)

async with httpx.AsyncClient(transport=transport, timeout=10.0) as client:

r = await client.get(

"https://card.wb.ru/cards/v1/detail",

params={"appType": 1, "curr": "rub", "dest": -1257786, "nm": nm},

)

return r.status_code, r.json() if r.status_code == 200 else None

```

`local_address` в httpx работает через bind на конкретный source IP. Это чище, чем мутить с `curl --interface`, и не форкает процесс на каждый запрос.

Сколько адресов реально нужно

Не гонитесь за миллионом. Wildberries не проверяет, что IP уникален глобально — важно, чтобы он не повторялся в пределах скользящего окна. Окно примерно 60 секунд.

Считаем: если вы хотите 500 запросов в секунду и не повторять IP в течение минуты, нужно 30 000 адресов. Это /112. Из /64 можно нарезать 65 536 таких блоков.

На практике 500 rps к Wildberries — это уже агрессивно. Они начнут замечать паттерн на уровне ASN: тысячи IP из одного /64, идущие строго в `card.wb.ru`. Поэтому пул адресов нужно разбавлять — часть трафика гнать через IPv4, часть через мобильные прокси, часть через residential.

Поведенческий профиль

Просто менять IP недостаточно. Wildberries смотрит на заголовки. Дефолтный `httpx` отправляет `User-Agent: python-httpx/0.27.0`, `Accept: */*`, `Accept-Encoding: gzip, deflate`. Это палево с первого запроса.

Нужен профиль мобильного клиента. Приложение Wildberries на Android шлёт примерно такое:

```python

HEADERS = {

"User-Agent": "Wildberries/6.2.1000 (Android 13; Pixel 6)",

"Accept": "application/json",

"Accept-Language": "ru-RU,ru;q=0.9",

"Accept-Encoding": "gzip",

"X-Request-Id": None,

"Device-Id": None,

}

```

`X-Request-Id` и `Device-Id` генерируются на каждый запрос. Формат `Device-Id` — hex-строка из 32 символов. Если их не отправлять, часть запросов отваливается с `400`, часть проходит. Если отправлять одинаковые — банят быстрее.

Ещё момент: порядок query-параметров. Wildberries не сортирует их, но мобильное приложение всегда шлёт в одном порядке. `httpx` с `params=` сортирует по алфавиту. Придётся собирать URL вручную.

Когда IPv6 не спасает

Wildberries в 2024 году начал раскатывать проверку на уровне TLS-fingerprint. Если у вас `httpx` с дефолтным OpenSSL — JA3-хэш не совпадает с мобильным клиентом. Тут IPv6 не поможет, нужен `curl_cffi` с имперсонацией:

```python

from curl_cffi import requests

r = requests.get(

"https://card.wb.ru/cards/v1/detail",

params={"appType": 1, "curr": "rub", "dest": -1257786, "nm": 12345678},

impersonate="chrome120",

proxies={"https": f"http://[{rand_ip()}]:3128"},

)

```

`impersonate="chrome120"` подделывает TLS-отпечаток под Chrome 120. Это работает лучше, чем `httpx`, но требует прокси, потому что `curl_cffi` не умеет `local_address` напрямую. Приходится поднимать локальный прокси-демон, который биндится на нужный IPv6.

Через lexic.ml это решается на уровне пула — прокси сам выбирает исходящий адрес, клиенту не нужно возиться с `local_address` и `ip_nonlocal_bind`. Для парсинга WB это удобно: один endpoint, ротация адресов на стороне прокси.

Кейс: каталог на 200 000 SKU

Сервер на Hetzner CX41, /64 префикс, задача — собрать цены по 200 000 SKU раз в час. Первая попытка: 50 потоков, один IPv4. На 3-й минуте — `429` на всё, бан на 40 минут.

Причина: 50 потоков давали ~300 rps с одного IP. Wildberries душит такое за секунды.

Решение: раскидали по 5 000 IPv6 из /64, ограничили 20 rps на адрес. Итого 100 000 rps теоретически, на практике упёрлись в CPU сервера — 4 vCPU не тянут 5 000 одновременных TLS-хендшейков. Снизили до 800 адресов и 40 rps на адрес, получили 32 000 rps. 200 000 SKU собрались за 7 секунд чистого времени, с учётом ретраев — 12 секунд.

Через сутки начались проблемы: Wildberries забанил весь /64 по ASN. Пришлось добавить второй префикс с другого хостера и чередовать.

Кейс: 429 при малом rps

Другой случай: 10 rps, один IPv6, всё равно `429` каждые 5 минут. Логи чистые, заголовки правильные.

Причина оказалась в `dest`. Параметр `dest=-1257786` — это код региона Москвы. Если слать всегда один и тот же `dest` с одного IP, Wildberries видит аномалию: живой пользователь меняет регион, смотрит разные склады. Статичный `dest` на протяжении часов — маркер бота.

Решение: ротация `dest` по списку регионов. Коды лежат в открытом виде на `static-basket-01.wb.ru/vol0/part0/regions.json`. 87 регионов, меняем каждые 20-30 запросов. `429` пропали полностью.

Кейс: пустые ответы вместо данных

Собрали 50 000 карточек, из них 12 000 вернули `200` с пустым массивом `products`. Ни ошибки, ни капчи — просто ничего.

Причина: Wildberries отдаёт пустоту, когда IP попал в «серую» зону. Не забанен, но и данные не отдаёт. Это дешевле для них, чем явный `403` — меньше шума в логах, сложнее детектить.

Решение: считать долю пустых ответов на IP. Если больше 5% — адрес в карантин на 30 минут. Плюс проверка: `products` пустой, но `total` в ответе ненулевой — точно бан. Если и `total` пустой — SKU реально не существует.

```python

def is_shadow_banned(resp_json):

if not resp_json.get("data", {}).get("products"):

if resp_json.get("data", {}).get("total", 0) > 0:

return True

return False

```

Прокси-слой: что важно

Не все IPv6-прокси одинаковы. Ключевые параметры: размер пула, скорость ротации, поддержка sticky-сессий, TLS-имперсонация.

| Параметр | Минимум | Комфорт |

|---|---|---|

| Размер пула | /112 (65k) | /64 (миллиарды) |

| Ротация | по запросу | по запросу + sticky 60s |

| Протокол | HTTP/HTTPS | SOCKS5 + HTTP |

| Имперсонация | нет | Chrome 120, Firefox 121 |

| Гео | RU | RU + несколько ASN |

Sticky-сессии нужны, когда парсите карточку в несколько шагов: сначала `/detail`, потом `/sellers`, потом отзывы. Если IP меняется между запросами, Wildberries может отдать неконсистентные данные или словить аномалию.

Мониторинг и алерты

Без мониторинга парсер тихо деградирует: доля `429` растёт, вы этого не видите, данные приходят неполные. Минимальный набор метрик:

- `rate_429` по IP — сколько запросов вернули `429`

- `rate_empty` — доля пустых ответов

- `latency_p95` — если растёт, IP под нагрузкой

- `unique_ips_per_minute` — сколько разных адресов использовано

Алерт на `rate_429 > 2%` за 5 минут. Алерт на `rate_empty > 5%`. Алерт на `latency_p95 > 2000ms`.

Пишите метрики в Prometheus, дашборд в Grafana. Без этого вы узнаете о бане только когда данные перестанут приходить вообще.

Итог

IPv6 даёт масштаб, но не решает проблему целиком. Wildberries смотрит на паттерны: заголовки, `dest`, TLS-fingerprint, соотношение запросов к каталогу и карточкам. Менять только IP — недостаточно. Нужен поведенческий профиль, ротация параметров, мониторинг и готовность менять ASN, когда текущий попадёт под бан.

Порог в 60-90 rps на IPv4 и возможность держать 30-40 rps на IPv6-адрес — рабочая отправная точка. Дальше подстраивайте под свои задачи и следите за метриками.

✔️Купить прокси