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

Reddit через IPv6 прокси: парсинг сабреддитов и обход лимитов

Reddit через IPv6 прокси: парсинг сабреддитов и обход лимитов

Почему Reddit душит парсеров

Reddit отдаёт публичный JSON по адресу `https://www.reddit.com/r//new.json`. Выглядит как подарок. На деле — ловушка для наивных. Без авторизации лимит примерно 10 запросов в минуту на IP. С OAuth-токеном — 100 запросов в минуту на client_id. Превысил — получил `429 Too Many Requests` и бан IP на 10-15 минут. Иногда дольше.

Проблема в том, что лимит считается по IP-адресу. Один сервер — один IP — один счётчик. Параллельный парсинг сотни сабреддитов с одной машины упирается в стену за минуту. Прокси-пулы на IPv4 дорогие: датацентр-адреса Reddit режет агрессивно, резиденциальные стоят от \$3 за гигабайт. IPv6 меняет расклад.

У IPv6 адресное пространство /64 — это 18 квинтиллионов адресов. Один провайдер выдаёт клиенту /64 или /48. Каждый адрес из подсети — отдельный счётчик лимитов для Reddit. Даже если Reddit склеивает по /64 (а он не всегда это делает), у вас сотни /64 в /48. На практике ротация IPv6 даёт в 10-50 раз больше «бесплатных» запросов, чем один IPv4.

Как Reddit на самом деле считает лимиты

Заголовки ответа говорят больше, чем документация. Смотрите на `X-Ratelimit-Used`, `X-Ratelimit-Remaining`, `X-Ratelimit-Reset`. Reddit использует эти заголовки для OAuth-запросов. Для анонимных JSON-эндпоинтов они тоже приходят, но с другими значениями.

```bash

curl -sI -A "Mozilla/5.0 (X11; Linux x86_64) parser/1.0" \

"https://www.reddit.com/r/python/new.json?limit=100" | grep -i ratelimit

```

Ответ примерно такой:

```

x-ratelimit-used: 1

x-ratelimit-remaining: 9.0

x-ratelimit-reset: 60

```

`remaining: 9.0` — это не «9 запросов осталось навсегда», это скользящее окно. Сброс через 60 секунд. Если гнать быстрее — `remaining` уйдёт в ноль и придёт 429 с `Retry-After: 600`. Вот тут и начинается боль: 600 секунд простоя на одном IP.

Есть нюанс с User-Agent. Reddit требует уникальный UA с контактом разработчика. Дефолтный `python-requests/2.31.0` ловит 429 почти мгновенно. UA вида `myapp/1.0 (by /u/username)` живёт дольше. Но и он не спасает от лимита по IP.

Схема с IPv6-ротацией

Идея простая: каждый воркер ходит через свой IPv6-адрес из выданной подсети. Reddit видит разные IP — разные счётчики. Через прокси-провайдера с пулом IPv6 (например, lexic.ml даёт /64-подсети на аккаунт) вы получаете сотни адресов в минуту без затрат на резиденциальные IPv4.

Архитектура такая. Пул из N воркеров. Каждый берёт задачу из очереди (сабреддит + тип листинга: new/hot/top). Прокси-менеджер выдаёт свободный IPv6 из пула. Воркер делает запрос, парсит JSON, кладёт результат в БД, возвращает IP в пул через cooldown (например, 65 секунд — чтобы окно лимита точно сбросилось).

Cooldown критичен. Если вернуть IP в пул сразу, второй запрос с того же адреса попадёт в то же окно и сожжёт остаток лимита. 65 секунд — безопасный зазор с учётом сетевой задержки и рассинхрона часов.

Рабочий пример на Python

Берём `aiohttp` и `asyncio`. Синхронный `requests` с ротацией прокси работает, но упирается в GIL при большом числе воркеров.

```python

import asyncio

import aiohttp

import time

from collections import deque

PROXY_HOST = "gate.lexic.ml"

PROXY_PORT = 8080

POOL_SIZE = 50

COOLDOWN = 65

class IPv6Pool:

def __init__(self, size):

self.free = asyncio.Queue()

self.cooldown = COOLDOWN

for i in range(size):

self.free.put_nowait(f"user-session-{i}")

async def acquire(self):

session_id = await self.free.get()

return session_id

async def release(self, session_id):

await asyncio.sleep(self.cooldown)

await self.free.put(session_id)

async def fetch_subreddit(session, pool, sub, listing="new"):

sid = await pool.acquire()

proxy = f"http://{sid}:{sid}@{PROXY_HOST}:{PROXY_PORT}"

url = f"https://www.reddit.com/r/{sub}/{listing}.json?limit=100"

headers = {"User-Agent": "parser/1.0 (by /u/yourname)"}

try:

async with session.get(url, proxy=proxy, headers=headers, timeout=20) as resp:

if resp.status == 429:

retry = int(resp.headers.get("Retry-After", 600))

await asyncio.sleep(retry)

return None

data = await resp.json()

return data["data"]["children"]

except aiohttp.ClientError:

return None

finally:

asyncio.create_task(pool.release(sid))

async def main(subs):

pool = IPv6Pool(POOL_SIZE)

async with aiohttp.ClientSession() as session:

tasks = [fetch_subreddit(session, pool, s) for s in subs]

results = await asyncio.gather(*tasks)

return results

if __name__ == "__main__":

subs = [f"sub{i}" for i in range(1, 101)]

asyncio.run(main(subs))

```

Прокси-провайдер обычно даёт sticky-сессии по логину: `user-session-N`. Каждая сессия прибита к своему IPv6 на время жизни. Смена сессии — новый адрес. Это дешевле, чем крутить SOCKS5-туннели вручную.

Reddit API против JSON-эндпоинтов

Многие лезут в OAuth сразу. Не всегда оправдано. Сравним.

| Параметр | Публичный JSON | OAuth API |

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

| Лимит без токена | ~10 req/min на IP | — |

| Лимит с токеном | — | 100 req/min на client_id |

| Авторизация | не нужна | OAuth2, client_id + secret |

| Данные | посты, комменты, листинги | всё то же + модерация, личные сообщения |

| Бан за превышение | IP на 10-15 мин | client_id навсегда |

| Скорость старта | 5 минут | регистрация приложения, 1-2 дня на модерацию |

OAuth даёт стабильные 100 req/min. Но client_id банится насовсем при злоупотреблении — восстановить сложно. Публичный JSON с IPv6-ротацией даёт больше суммарной пропускной способности при меньшем риске: сгорел IP — взял следующий. Для сбора публичных постов это выигрышная стратегия.

OAuth нужен, когда требуются приватные данные или запись (постинг, голосование). Для read-only парсинга сабреддитов публичный JSON + ротация — рабочий вариант.

Пагинация и курсоры

Reddit отдаёт по 100 постов на страницу максимум (параметр `limit`, hard cap 100). Дальше — `after` из `data.after`. Без курсора вы получите первые 100 постов и всё.

```python

async def paginate(session, pool, sub, pages=10):

after = None

all_posts = []

for _ in range(pages):

sid = await pool.acquire()

proxy = f"http://{sid}:{sid}@gate.lexic.ml:8080"

params = {"limit": 100}

if after:

params["after"] = after

url = f"https://www.reddit.com/r/{sub}/new.json"

async with session.get(url, params=params, proxy=proxy,

headers={"User-Agent": "parser/1.0"}) as r:

if r.status != 200:

break

data = await r.json()

posts = data["data"]["children"]

if not posts:

break

all_posts.extend(posts)

after = data["data"].get("after")

if not after:

break

await pool.release(sid)

return all_posts

```

Тонкость: `after` живёт ограниченное время. Если между страницами прошло больше пары минут (а с cooldown 65 секунд это норма), курсор может протухнуть. Reddit вернёт пустой `children` или 400. Решение — не разносить страницы одного сабреддита по разным IP с большими паузами, а брать 3-5 страниц подряд с одного адреса (укладываясь в лимит), потом менять IP.

Что ломается на практике

Первый грабли-кейс: сервер на Ubuntu 22.04, `aiohttp 3.9`, пул из 100 IPv6. Через час работы — 40% запросов с 403. Причина: провайдер выдал /64, а Reddit начал склеивать адреса по /64-префиксу. Все 100 адресов попали в один счётчик. Решение: запросить у провайдера /48 или несколько /64 и ротировать между подсетями, а не внутри одной.

Второй кейс: тот же парсер, но с `requests` в 20 потоках. Латентность выросла с 200 мс до 3-4 секунд. Причина — TCP-хендшейк на каждый запрос через новый прокси-туннель плюс DNS-резолв. С `aiohttp` и keep-alive соединениями на сессию латентность упала до 250-350 мс. Разница в 10 раз на объёме в 50k запросов — это часы против минут.

Третий кейс: парсинг r/all с `limit=100` и пагинацией на 50 страниц. На 30-й странице стабильно 429, хотя `remaining` показывал 5. Причина: r/all — самый нагруженный эндпоинт, у него отдельный, более жёсткий лимит (примерно 5 req/min на IP против 10 для конкретных сабреддитов). Решение: для r/all увеличить cooldown до 120 секунд и снизить `limit` до 50.

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

Без метрик парсер тихо деградирует. Минимум, что нужно собирать: доля 429/403 в минуту, средняя латентность, число активных IP в пуле, глубина очереди задач. Prometheus + Grafana закрывают это за вечер.

Ключевая метрика — `ratelimit_remaining` из заголовков. Если среднее по пулу падает ниже 3, пора расширять пул или снижать частоту. Второй индикатор — `Retry-After` в 429-ответах. Рост с 60 до 600 секунд означает, что Reddit ужесточил политику для вашей подсети.

Логирование IP в каждом запросе обязательно. Без него не поймёте, какой адрес сгорел и почему. Формат: `timestamp ip subreddit status latency ratelimit_remaining`. Парсится в ELK или Loki за минуты.

Юридическая сторона и ToS

Reddit ToS запрещает автоматизированный сбор без разрешения. На практике публичный JSON отдают открыто, а банят за агрессивность, не за сам факт парсинга. Границы размыты. Если собираетесь публиковать данные или коммерчески использовать — читайте Reddit API Terms и Data Use Agreement. Для внутренней аналитики и исследований прецеденты мягче.

Технически безопасная стратегия: не превышать 10 req/min на IP, ставить честный User-Agent с контактом, уважать `Retry-After`, не парсить приватные сабреддиты. Это снижает шанс попасть в чёрный список провайдера и получить жалобу.

IPv6-ротация — инструмент, не индульгенция. Она решает проблему лимитов, но не отменяет правила. Парсер, который ведёт себя как десять тысяч вежливых пользователей, живёт долго. Парсер, который долбит Reddit с 50k адресов в минуту, ловит бан подсети за сутки.

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