Wikipedia через IPv6 прокси: массовый парсинг и обход ограничений
Содержание
- Почему Wikipedia вообще упирается в лимиты
- Анатомия лимитов Wikimedia
- Как IPv6 меняет расклад
- Реализация ротации через прокси
- !/bin/bash
- Python-клиент с пулом прокси и backoff
- Парсинг HTML-страниц и заголовок Accept-Encoding
- Кейс: обход бана /64-подсети
- Кейс: 429 на search API при большом srlimit
- Кейс: медленные ответы через IPv6-прокси
- Что НЕ надо делать
- Итог: сколько реально выжать
Почему Wikipedia вообще упирается в лимиты
Wikipedia отдаёт контент через MediaWiki API и обычные страницы. Для анонимных запросов действует rate limit: примерно 200 запросов в минуту с одного IP на API, и мягче — на HTML-страницы. Превысил — получаешь HTTP 429 с заголовком `Retry-After`. Дальше хуже: если долбить настойчиво, IP попадает в бан на несколько часов, а иногда и на сутки.
Проблема в том, что за одним IPv4-адресом часто сидят сотни людей через NAT. Один парсер-энтузиаст роняет скорость всем. Wikipedia это знает и режет по IP жёстко. IPv6 тут интереснее: адресное пространство огромное, и каждый запрос можно гнать со своего адреса. Но и здесь есть нюансы — Wikimedia банит целые /64-подсети, если видит паттерн.
Разберём, как строить парсер, который не падает на 429 и не превращается в DDOS-инструмент.
Анатомия лимитов Wikimedia
У Wikimedia Foundation есть публичная документация по API etiquette. Ключевые цифры: 200 запросов в минуту для анонимов, 5000 в час для зарегистрированных ботов с нормальным User-Agent. Заголовок `User-Agent` обязателен — без него MediaWiki отдаёт 403 с текстом про policy. Это не шутка, реально блокируют.
Вот что возвращает сервер при превышении:
```
HTTP/2 429
retry-after: 60
x-ratelimit-limit: 200
x-ratelimit-remaining: 0
x-ratelimit-reset: 1718030400
```
Заголовки `x-ratelimit-*` появились относительно недавно, но они есть. Игнорировать `retry-after` — прямой путь к бану. Сервер запоминает, что ты долбил после 429.
Есть ещё отдельный лимит на тяжёлые запросы: `action=query&list=search` с большими `srlimit`, рендеринг PDF, доступ к истории правок. Они считаются дороже. Если парсишь категории с тысячами страниц — готовься к тому, что каждый десятый запрос будет 429 даже при соблюдении общего лимита.
Как IPv6 меняет расклад
IPv6-прокси даёт тебе пул адресов. Не один, а тысячи. Каждый запрос — с нового адреса. С точки зрения MediaWiki это выглядит как трафик от разных пользователей. Rate limit считается по IP, значит, распределяя запросы по /64-подсетям, ты обходишь порог в 200/мин.
Но Wikimedia не наивна. У них есть механизм проверки: если с одной /64-подсети приходит аномально много запросов, они могут забанить всю подсеть. Это называется "range block". Для /64 это 18 квинтиллионов адресов — бан одного /64 не так страшен, если у тебя пул из сотен подсетей.
Практика показывает: ротация по /64 с интервалом 1-2 запроса на адрес работает стабильно. Ротация по каждому запросу внутри одной /64 — быстрее ловишь range block. Провайдеры вроде lexic.ml выдают пул /64-подсетей именно с расчётом на такое использование — это техническое требование, а не маркетинг.
Реализация ротации через прокси
Простейший вариант — curl с прокси и рандомизацией адреса. Пул прокси-серверов с IPv6, каждый запрос идёт через случайный:
```bash
!/bin/bash
PROXIES=(
"http://user:pass@[2a01:4f8:1c1c:1::1]:3128"
"http://user:pass@[2a01:4f8:1c1c:2::1]:3128"
"http://user:pass@[2a01:4f8:1c1c:3::1]:3128"
)
URL="https://en.wikipedia.org/w/api.php?action=query&list=random&rnlimit=10&format=json"
for i in \$(seq 1 50); do
PROXY=\${PROXIES[\$((RANDOM % \${#PROXIES[@]}))]}
curl -s --proxy "\$PROXY" \
-H "User-Agent: WikiParser/1.0 (contact@example.com)" \
"\$URL" -o "out_\$i.json"
sleep 0.3
done
```
Работает, но грубо. Нет контроля над лимитами, нет обработки 429, нет ротации по подсетям. Для серьёзного парсинга нужен Python с пулом сессий и счётчиками.
Python-клиент с пулом прокси и backoff
Ниже — рабочий каркас. Он держит пул прокси, считает запросы на каждый адрес, уважает `retry-after` и ротирует при приближении к лимиту.
```python
import time
import random
import requests
from urllib.parse import urlparse
from collections import defaultdict
PROXIES = [
"http://user:pass@[2a01:4f8:1c1c:1::1]:3128",
"http://user:pass@[2a01:4f8:1c1c:2::1]:3128",
"http://user:pass@[2a01:4f8:1c1c:3::1]:3128",
"http://user:pass@[2a01:4f8:1c1c:4::1]:3128",
]
API = "https://en.wikipedia.org/w/api.php"
HEADERS = {"User-Agent": "WikiParser/1.0 (contact@example.com)"}
LIMIT_PER_PROXY = 150
WINDOW = 60
usage = defaultdict(list)
blocked_until = {}
def pick_proxy():
now = time.time()
candidates = []
for p in PROXIES:
if blocked_until.get(p, 0) > now:
continue
recent = [t for t in usage[p] if now - t < WINDOW]
usage[p] = recent
if len(recent) < LIMIT_PER_PROXY:
candidates.append(p)
if not candidates:
wait = min(blocked_until.values()) - now if blocked_until else 1
time.sleep(max(wait, 1))
return pick_proxy()
return random.choice(candidates)
def fetch(params, retries=3):
for attempt in range(retries):
proxy = pick_proxy()
try:
r = requests.get(
API, params=params, headers=HEADERS,
proxies={"http": proxy, "https": proxy},
timeout=15,
)
usage[proxy].append(time.time())
if r.status_code == 429:
retry_after = int(r.headers.get("retry-after", "60"))
blocked_until[proxy] = time.time() + retry_after
continue
r.raise_for_status()
return r.json()
except requests.RequestException:
blocked_until[proxy] = time.time() + 30
raise RuntimeError("all retries exhausted")
def iter_category(category, limit=500):
cont = {}
fetched = 0
while fetched < limit:
params = {
"action": "query", "list": "categorymembers",
"cmtitle": f"Category:{category}", "cmlimit": "50",
"format": "json", **cont,
}
data = fetch(params)
for m in data["query"]["categorymembers"]:
yield m["title"]
fetched += 1
if fetched >= limit:
return
if "continue" not in data:
return
cont = data["continue"]
```
Что здесь важно. Счётчик `usage` считает запросы за окно в 60 секунд по каждому прокси отдельно. `blocked_until` — это честный backoff по `retry-after`. Если все прокси забанены — ждём. Никакого героизма, никакого «продолжим долбить».
Порог `LIMIT_PER_PROXY = 150` — не 200. Специально оставляем запас: MediaWiki считает запросы не идеально ровно, и 150 даёт буфер на всплески. По опыту, на 200 стабильно ловишь 429 раз в 5-10 минут.
Парсинг HTML-страниц и заголовок Accept-Encoding
API — это полдела. Часто нужны сами страницы: `/wiki/Python_(programming_language)`. Здесь лимит мягче, но HTML-ответы весят больше. Если тянешь 10 МБ страниц в минуту — упрёшься в полосу, а не в rate limit.
Полезно просить сжатие. `Accept-Encoding: gzip, br` и парсинг через `brotli` дают экономию в 4-5 раз на типичной статье. Wikipedia отдаёт brotli, если попросить.
```python
import brotli
r = requests.get(
"https://en.wikipedia.org/wiki/Python_(programming_language)",
headers={**HEADERS, "Accept-Encoding": "gzip, br"},
proxies={"https": pick_proxy()}, timeout=20,
)
html = brotli.decompress(r.content).decode("utf-8")
```
Дальше — `BeautifulSoup` или `lxml`. Совет: не парси визуальную часть, если нужны данные. У Wikipedia есть `action=parse&prop=wikitext` — отдаёт сырой викитекст, разбирать его проще и надёжнее, чем DOM с классами вида `mw-parser-output`.
Кейс: обход бана /64-подсети
Пример: парсер на 8 прокси из одной /64-подсети. Через 40 минут работы — все 8 получают 429 одновременно, `retry-after: 3600`. Причина: Wikimedia зафиксировала 200+ запросов/мин с диапазона и наложила range block на /64.
Решение: пул пересобран из 8 разных /64 (по одному адресу на подсеть). Каждый прокси теперь — отдельная подсеть. Ротация между ними. Лимит снижен до 120 запросов/мин на прокси. После этого — 6 часов непрерывного парсинга без единого 429. Скорость: около 950 страниц в минуту суммарно.
Технические детали: запросы шли с `User-Agent: WikiParser/1.0 (contact@example.com)`, что соответствует policy. Без нормального UA бан пришёл бы раньше — на этапе 403.
Кейс: 429 на search API при большом srlimit
Пример: запрос `action=query&list=search&srsearch=...&srlimit=500`. Сервер отвечает 429 даже при 50 запросах/мин с прокси. Причина: search — дорогая операция, у неё отдельный лимит, примерно 50 запросов/мин на IP независимо от общего.
Решение: `srlimit` снижен до 50, пагинация через `sroffset`. Каждый запрос — с нового прокси, но с интервалом не меньше 1.2 секунды на адрес. Плюс кэш результатов в SQLite по хэшу `(query, offset)` — повторные запросы не идут в сеть. Итог: 50 страниц результатов за 6 запросов вместо одного, зато без бана.
Кейс: медленные ответы через IPv6-прокси
Пример: среднее время ответа через IPv6-прокси — 800 мс, через прямое IPv4-подключение — 180 мс. Причина: прокси-сервер в Европе, а клиент в Азии, плюс MTU 1280 на туннеле вызывает фрагментацию.
Диагностика: `ping6 -M do -s 1400` показал, что пакеты больше 1280 байт теряются. Классическая проблема PMTUD на IPv6-туннелях. Решение: на прокси-сервере выставлен `MTU 1420` на интерфейсе, `tcp_mss_clamp` в nftables:
```
nft add rule inet filter forward tcp flags syn tcp option maxseg size set rt mtu
```
После этого время ответа упало до 240 мс. Разница с прямым подключением осталась, но стала приемлемой. Для парсинга 1000 страниц это экономит около 9 минут.
Что НЕ надо делать
Не запускай парсер без `User-Agent`. MediaWiki отдаст 403 с текстом про policy, и это самый мягкий исход. С нормальным UA хотя бы понятно, кто ты.
Не игнорируй `retry-after`. Если сервер сказал 60 секунд — жди 60. Обход через смену прокси в момент 429 работает, но если делать это агрессивно, Wikimedia начнёт банить подсети превентивно.
Не парси `Special:Export` массово. Это тяжёлый endpoint, и его отдельно мониторят. Для больших выгрузок есть официальные dumps — `dumps.wikimedia.org`, они обновляются раз в месяц и качаются по HTTP без лимитов. Если тебе нужен весь корпус — качай дамп, а не долби API.
Не забывай про robots.txt. Wikipedia разрешает парсинг, но с уважением к лимитам. Если твой парсер роняет доступ для других — прилетит жалоба, и Wikimedia может принять меры вплоть до блокировки по ASN.
Итог: сколько реально выжать
С пулом из 10 /64-подсетей, лимитом 150 запросов/мин на прокси и соблюдением `retry-after` стабильно получается 1200-1400 запросов в минуту. Это примерно 20 запросов в секунду. Для 100 000 статей — около 80 минут работы. Без бана, с нормальным UA, с кэшем.
Если нужно быстрее — только дампы. API не резиновый, и уважение к лимитам здесь не вежливость, а техническая необходимость. Один раз перегнул — и следующие сутки парсишь с дампа, потому что все подсети в бане.