Библиотека httpx стала стандартом для асинхронных HTTP-запросов в Python, но при работе с прокси у неё есть нюансы, которые ломают парсеры на проде. Разберём, как настроить асинхронную ротацию прокси, включить HTTP/2 и почему ваш скрипт падает каждые 50-100 запросов.
Почему httpx, а не requests или aiohttp
requests синхронный и не умеет HTTP/2 из коробки. aiohttp асинхронный, но настройка прокси там менее дружелюбная, а поддержка HTTP/2 отсутствует вовсе. httpx закрывает оба вопроса: синхронный и асинхронный клиент с единым API, нативная поддержка HTTP/2 через расширение h2, и гибкая работа с прокси на уровне транспорта.
Для задач парсинга маркетплейсов (Wildberries, Ozon, Avito), сбора данных с API соцсетей или мониторинга цен конкурентов асинхронность критична — она даёт кратный прирост скорости по сравнению с последовательными запросами. Но без грамотной ротации прокси и управления соединениями асинхронность превращается в источник новых проблем: исчерпание пула соединений, зависшие корутины, утечки файловых дескрипторов.
Второй важный момент — HTTP/2 умеет мультиплексировать несколько запросов в одном TCP-соединении, что снижает нагрузку на целевой сервер и помогает обходить некоторые наивные антибот-системы, которые смотрят на количество TCP-хендшейков с одного IP.
Базовая настройка прокси в httpx
С версии httpx 0.26+ синтаксис настройки прокси изменился — вместо словаря proxies используется параметр proxy. Это частый источник путаницы, если вы смотрите старые туториалы.
import httpx
import asyncio
PROXY_URL = "http://user:[email protected]:8000"
async def fetch(url: str):
async with httpx.AsyncClient(proxy=PROXY_URL, timeout=15.0) as client:
response = await client.get(url)
return response.status_code, response.text[:200]
asyncio.run(fetch("https://httpbin.org/ip"))
Если нужно разделить прокси для HTTP и HTTPS трафика или настроить разные прокси для разных доменов, используйте httpx.Client(mounts=...) — транспорты монтируются по шаблону URL:
mounts = {
"http://": httpx.AsyncHTTPTransport(proxy="http://user:[email protected]:8000"),
"https://": httpx.AsyncHTTPTransport(proxy="http://user:[email protected]:8000"),
}
async with httpx.AsyncClient(mounts=mounts) as client:
response = await client.get("https://example.com")
Для аутентификации по логину и паролю формат http://user:pass@host:port работает стабильно для HTTP-прокси. Для SOCKS5 понадобится дополнительно установить httpx[socks], иначе получите ImportError в рантайме, а не на этапе написания кода — это ловушка, в которую попадают многие.
Асинхронная ротация прокси: рабочий код
Главная ошибка в ротации прокси — создавать новый AsyncClient на каждый запрос. Это убивает производительность, потому что клиент держит пул соединений, и его пересоздание сводит на нет все преимущества keep-alive соединений. Правильный подход — создать пул клиентов заранее, по одному на прокси, и распределять задачи между ними.
import httpx
import asyncio
import itertools
import random
PROXIES = [
"http://user:[email protected]:8000",
"http://user:[email protected]:8000",
"http://user:[email protected]:8000",
]
class ProxyPool:
def __init__(self, proxies: list[str]):
self.clients = [
httpx.AsyncClient(proxy=p, timeout=httpx.Timeout(15.0, connect=5.0))
for p in proxies
]
self._cycle = itertools.cycle(self.clients)
def get_client(self) -> httpx.AsyncClient:
return next(self._cycle) # round-robin
# для случайного выбора: random.choice(self.clients)
async def close_all(self):
for client in self.clients:
await client.aclose()
async def fetch_with_retry(pool: ProxyPool, url: str, retries: int = 3):
for attempt in range(retries):
client = pool.get_client()
try:
response = await client.get(url)
if response.status_code == 200:
return response.text
except (httpx.ConnectTimeout, httpx.ProxyError, httpx.ReadTimeout):
await asyncio.sleep(0.5 * (attempt + 1))
continue
return None
async def main():
pool = ProxyPool(PROXIES)
urls = [f"https://example.com/page/{i}" for i in range(100)]
sem = asyncio.Semaphore(20) # ограничиваем параллелизм
async def bound_fetch(url):
async with sem:
return await fetch_with_retry(pool, url)
results = await asyncio.gather(*(bound_fetch(u) for u in urls))
await pool.close_all()
return results
asyncio.run(main())
Этот паттерн решает три проблемы сразу: переиспользует TCP-соединения через keep-alive, распределяет нагрузку по прокси round-robin способом и ограничивает параллелизм через семафор, чтобы не положить целевой сервер и не спровоцировать бан по rate-limit.
Для продакшен-парсеров, которые работают с Wildberries, Ozon или Instagram API, важно, чтобы пул прокси был достаточно большим и разнообразным по географии — иначе ротация внутри одной подсети не спасёт от блокировки. Здесь хорошо подходят резидентные прокси, так как они имеют IP реальных пользователей и реже попадают под фильтры антибот-систем, которые анализируют ASN и репутацию диапазона.
HTTP/2 в httpx: зачем и как включить
По умолчанию httpx использует HTTP/1.1. Чтобы включить HTTP/2, нужно установить расширение и явно передать флаг:
# pip install httpx[http2]
async with httpx.AsyncClient(http2=True, proxy=PROXY_URL) as client:
response = await client.get("https://example.com")
print(response.http_version) # ожидаем "HTTP/2"
Важный нюанс: не все прокси поддерживают туннелирование HTTP/2 корректно. Часть дешёвых дата-центровых прокси работает только как HTTP/1.1 прокси даже если конечный сервер поддерживает HTTP/2 — в таком случае httpx тихо откатится на HTTP/1.1 через CONNECT-туннель, и вы не всегда заметите это без явной проверки response.http_version.
HTTP/2 даёт реальный выигрыш в трёх сценариях: когда вы делаете много запросов к одному домену (мультиплексирование в одном соединении снижает задержки), когда важно снизить заметность запросов для антибот-защиты (меньше TCP-хендшейков с одного IP выглядит естественнее), и когда целевой сайт сам требует HTTP/2 и возвращает ошибки или урезанный контент клиентам на HTTP/1.1 — это иногда встречается у сайтов на Cloudflare.
Для задач, где важна скорость и стабильность HTTP/2-соединений через прокси, лучше подходят выделенные прокси дата-центров — они быстрее резидентных и обычно корректно поддерживают туннелирование современных протоколов.
5 типичных ошибок с прокси в httpx
1. Пересоздание клиента на каждый запрос
Код вида async with httpx.AsyncClient(...) as client: await client.get(url) внутри цикла — это антипаттерн. Каждое создание клиента — это новый TCP-коннект, новый TLS-хендшейк, полное игнорирование connection pooling. На 1000 запросов разница в скорости между правильным и неправильным подходом может достигать 5-10 раз.
2. Отсутствие таймаутов или слишком большие таймауты по умолчанию
По умолчанию httpx ставит таймаут 5 секунд на всё сразу — connect, read, write, pool. При работе через нестабильные прокси этого мало для connect, но слишком много для read, если прокси просто "висит". Разделяйте таймауты явно:
timeout = httpx.Timeout(
connect=5.0,
read=10.0,
write=10.0,
pool=5.0
)
client = httpx.AsyncClient(proxy=PROXY_URL, timeout=timeout)
3. Игнорирование лимитов пула соединений
При большом количестве параллельных задач (gather на 500+ корутин без семафора) httpx начинает выбрасывать PoolTimeout, потому что лимит соединений в пуле (по умолчанию 100) исчерпан. Настраивайте лимиты явно через httpx.Limits(max_connections=200, max_keepalive_connections=50) и всегда используйте семафор для контроля параллелизма.
4. Отключение verify без понимания последствий
Многие копируют verify=False из Stack Overflow, чтобы избавиться от SSL-ошибок при работе через прокси с MITM-сертификатами. Это открывает дорогу атакам "человек посередине" и может привести к утечке данных через скомпрометированный прокси. Если прокси требует специальный сертификат — добавляйте его явно через verify="/path/to/cert.pem", а не отключайте проверку целиком.
5. Не закрытые клиенты и утечка файловых дескрипторов
Если вы создаёте пул клиентов вручную (как в примере выше), не забывайте вызывать await client.aclose() для каждого из них при завершении работы. Долго работающие скрипты без явного закрытия соединений упираются в лимит открытых файловых дескрипторов операционной системы (обычно 1024 на Linux по умолчанию), и парсер падает с загадочной ошибкой Too many open files через несколько часов работы.
Какой тип прокси выбрать для парсинга
Выбор прокси напрямую влияет на то, сколько ошибок из списка выше вы вообще увидите — нестабильные прокси провоцируют таймауты и обрывы соединений, даже если код написан идеально.
| Тип прокси | Скорость | Риск блокировки | Когда использовать |
|---|---|---|---|
| Дата-центр | Высокая | Высокий на защищённых сайтах | Open API, слабая антибот-защита |
| Резидентные | Средняя | Низкий | Wildberries, Ozon, соцсети |
| Мобильные | Средняя-низкая | Минимальный | Instagram, TikTok API, жёсткая защита |
Для асинхронного парсинга с высокой скоростью запросов к API с умеренной защитой дата-центровые прокси остаются экономически выгодным вариантом — главное, равномерно распределять нагрузку по пулу, как показано в коде выше.
Чек-лист перед продакшеном
- Клиенты httpx создаются один раз и переиспользуются, а не на каждый запрос
- Таймауты разделены на connect/read/write/pool, а не используется дефолт
- Параллелизм ограничен через
asyncio.Semaphore - Лимиты пула соединений настроены под реальную нагрузку
- Есть retry-логика с обработкой ProxyError, ConnectTimeout, ReadTimeout
- Все клиенты явно закрываются через
aclose()при завершении - verify=False не используется без крайней необходимости
- HTTP/2 включён осознанно и проверен через response.http_version
Заключение
httpx даёт мощный инструментарий для асинхронной работы с прокси и HTTP/2, но требует аккуратной настройки пула клиентов, таймаутов и лимитов соединений. Большинство падений парсеров в продакшене связано не с библиотекой, а с архитектурными ошибками: пересозданием клиентов, отсутствием ограничения параллелизма и неправильной обработкой исключений прокси.
Если вы строите парсер для маркетплейсов или API соцсетей, которые активно отслеживают подозрительную активность, стабильность инфраструктуры прокси важна не меньше, чем код. Для таких задач стоит присмотреться к резидентным прокси — они снижают долю ошибок ProxyError и ConnectTimeout из-за более высокой репутации IP-адресов по сравнению с дешёвыми дата-центровыми пулами.