# Асинхронный парсинг на Python с использованием httpx, aiohttp и прокси

Асинхронность превращает медленный парсер в быстрый — и быстрый парсер в заблокированный, если вы неправильно настроите прокси, семафоры и тайм-ауты. Вот полный шаблон для httpx и aiohttp с кодом.

Если ваш парсер проводит большую часть времени ожидания в сети, асинхронность — это самое большое ускорение, которое можно получить, а прокси — это то, что предотвращает блокировку. Это руководство охватывает асинхронный парсинг на Python с использованием `httpx` и `aiohttp` плюс прокси от начала до конца: как каждая библиотека настраивает прокси, как ограничить параллелизм с помощью семафора, как установить тайм-ауты, которые действительно срабатывают, и как менять IP, не разрушая сессии. Все с исполняемым кодом и учетными данными-заглушками, которые вы заменяете на свои.

## Почему асинхронность и где место прокси

Стандартные `requests` блокируют: каждый вызов ждет ответа перед началом следующего. Получите 500 страниц, и вы заплатите за 500 поездок туда и обратно. Асинхронность получает их одновременно в одном цикле событий, так что общее время сокращается до самого медленного запроса вместо суммы. Загвоздка: всплеск одновременных запросов с одного IP — это именно та сигнатура, которую отслеживают антибот-системы. Решение — не замедляться до ползания, а распределять трафик по [пулу вращающихся прокси](https://quantumproxies.io/rotating-proxies) и намеренно ограничивать с помощью семафора.

## Настройка прокси в httpx (асинхронно)

`httpx` — это прагматичный выбор, потому что одна клиентская модель работает как синхронно, так и асинхронно, и поддерживает HTTP/2. Обратите внимание на современный API: это единственный аргумент `proxy=` на клиенте, а не старый словарь `proxies=` — распространенный источник путаницы "почему мой прокси игнорируется" после обновления. Учетные данные идут прямо в URL прокси.

```python
import asyncio, httpx

PROXY = "http://USER:PASS@residentialpremium.quantumproxies.io:8080"

async def fetch(client, url):
    r = await client.get(url, timeout=httpx.Timeout(20.0))
    return url, r.status_code, r.text

async def main(urls):
    async with httpx.AsyncClient(proxy=PROXY, http2=True) as client:
        tasks = [fetch(client, u) for u in urls]
        return await asyncio.gather(*tasks)

urls = ["https://httpbin.org/ip"] * 5
print(asyncio.run(main(urls)))
```

Один клиент, используемый повторно для каждого запроса, — это суть — он поддерживает пул соединений в теплом состоянии, чтобы вы избегали повторных рукопожатий TLS через прокси. Создание нового клиента для каждого запроса — самая распространенная ошибка производительности в асинхронности: это выбрасывает повторное использование соединений и состояние куки на каждом вызове.

## Настройка прокси в aiohttp (для каждого запроса)

`aiohttp` является нативным для asyncio и предоставляет наилучший контроль над параллелизмом, но его соглашение о прокси отличается: прокси передается для каждого запроса в `session.get()`, а не на сессии. Это действительно удобно для ротации. Две вещи, которые здесь кусают людей — стандартный User-Agent буквально `Python/3.x aiohttp/3.x`, явный признак, который вы должны переопределить, и вы должны явно задать размер пула соединений.

```python
import aiohttp, asyncio

PROXY = "http://USER:PASS@residentialpremium.quantumproxies.io:8080"
HEADERS = {"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
           "AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.0.0 Safari/537.36"}

async def fetch(session, url):
    timeout = aiohttp.ClientTimeout(total=30, connect=10, sock_read=20)
    async with session.get(url, proxy=PROXY, timeout=timeout) as resp:
        return url, resp.status, await resp.text()

async def main(urls):
    conn = aiohttp.TCPConnector(limit=20, limit_per_host=8)
    async with aiohttp.ClientSession(connector=conn, headers=HEADERS) as session:
        return await asyncio.gather(*(fetch(session, u) for u in urls))

print(asyncio.run(main(["https://httpbin.org/ip"] * 5)))
```

`TCPConnector(limit=20, limit_per_host=8)` ограничивает общее количество открытых соединений и соединений с любым одним хостом — это первая линия вежливости, которая предотвращает поглощение вашей целой пула одним целевым объектом.

![asyncio.gather ставит в очередь все загрузки, asyncio.Semaphore(15) позволяет выполняться одновременно не более чем 15, а вращающийся прокси-шлюз дает каждому запросу новый выходной IP, прежде чем параллельные загрузки дойдут до цели. Карточка: семафор ограничивает, шлюз маскирует, а без любого из них асинхронная скорость превращается в волну блокировок.](https://res.cloudinary.com/dmqliab0t/image/upload/f_auto,q_auto,w_1200/v1785335150/blog/generated/qp-b9-010-diag1.png)
*Семафор ограничивает; вращающийся шлюз маскирует. Вам нужны оба — асинхронная скорость без них — это волна блокировок.*

## Ограничение параллелизма с помощью семафора

Запуск неограниченного количества одновременных запросов — это самый быстрый способ сжечь пул прокси и нарушить лимиты скорости. `asyncio.Semaphore` — это ограничитель: он ограничивает количество запросов, находящихся в процессе выполнения одновременно, независимо от того, сколько задач вы поставили в очередь. Установите глобальный лимит, а для агрессивных задач — лимит на домен.

```python
sem = asyncio.Semaphore(15)  # never more than 15 requests in flight

async def guarded_fetch(client, url):
    async with sem:
        return await fetch(client, url)

async def main(urls):
    async with httpx.AsyncClient(proxy=PROXY) as client:
        return await asyncio.gather(*(guarded_fetch(client, u) for u in urls))
```

Правильное число зависит от терпимости цели и размера вашего пула, а не от того, насколько быстро может работать ваша машина. Начните консервативно (10–20), следите за уровнем блокировок, и повышайте его только тогда, когда успех остается высоким. На одном липком IP держите его в однозначных числах.

## Тайм-ауты: моделируйте каждую фазу

Запрос через прокси терпит неудачу в большем количестве мест, чем прямой — разрешение DNS, подключение к прокси, настройка туннеля, подключение к цели, ожидание заголовков и чтение тела — все это отдельные задержки. Один общий тайм-аут скрывает, какая фаза зависла. `ClientTimeout(total=, connect=, sock_read=)` в `aiohttp` и `Timeout()` в `httpx` позволяют ограничивать их по отдельности. Единственное правило без исключений: никогда не отправляйте запрос без тайм-аута, иначе один зависший выход повесит корутину навсегда и тихо истощит ваш цикл событий.

## Меняйте прокси, не нарушая сессии

Существуют две стратегии ротации, и неправильный выбор испортит ваши данные. Случайная ротация для каждого запроса идеально подходит для безгосударственных выборок страниц. Но она разрушает любой поток, который зависит от куки, входа в систему или локализации, потому что второй запрос попадает на другой IP, чем первый. Чистое разделение: меняйте на границе логической единицы — один IP на сегмент парсинга или на аккаунт — и используйте вращающийся шлюз, который автоматически предоставляет вам новый выход, чтобы ваш код никогда не управлял списком.

```python
# rotating gateway: one endpoint, new exit IP per request
ROT = "http://USER:PASS@residentialpremium.quantumproxies.io:8080"

# sticky session: same IP for a multi-step flow, tag the session id
STICKY = "http://USER-sid-a1b2-ttl-30m:PASS@residentialpremium.quantumproxies.io:8080"

async def crawl_segment(urls):
    async with httpx.AsyncClient(proxy=ROT) as client:  # rotates per call
        return await asyncio.gather(*(fetch(client, u) for u in urls))
```

Вращающийся резидентный шлюз — это прагматичный выбор для парсинга с высокой параллельностью: ротация для каждого запроса через 90M+ IP в 200+ странах, с липкими сессиями, когда корзина или вход требуют одного и того же выхода на несколько минут. Если вы взвешиваете резидентный против ISP или дата-центра для задачи, наше руководство по [какой тип прокси использовать](https://quantumproxies.io/blog/mobile-vs-residential-vs-isp-proxies-which-one-should-you-use-in-2026) описывает компромиссы.

[Получите вращающийся резидентный шлюз](https://quantumproxies.io/rotating-proxies)

## Повторные попытки, откат и джиттер

Мертвые выходы и временные блокировки — это нормально в масштабе, а не исключение. Но наивные повторные попытки ухудшают ситуацию: когда 50 асинхронных задач терпят неудачу в тот же момент и все повторяются немедленно, вы запускаете синхронизированный всплеск, который бьет по цели сильнее, чем первоначальный запуск. Добавьте экспоненциальный откат плюс рандомизированный джиттер, чтобы повторные попытки распределялись, ограничьте количество попыток и меняйте IP при неудаче, а не используйте сгоревший.

```python
import random
from httpx import HTTPError

async def robust_fetch(client, url, tries=3):
    for attempt in range(tries):
        try:
            r = await client.get(url, timeout=httpx.Timeout(20.0))
            if r.status_code < 400 and looks_real(r.text):
                return r
        except HTTPError:
            pass
        # exponential backoff + jitter before the next attempt
        await asyncio.sleep((2 ** attempt) + random.uniform(0, 1))
    return None
```

![Код обеих библиотек бок о бок: httpx задает прокси один раз в AsyncClient с http2 и httpx.Timeout(20.0), aiohttp передает proxy= в каждом session.get() с ClientTimeout(total=30, connect=10) и TCPConnector(limit=20, limit_per_host=8). Карточка подводит итог двум соглашениям о прокси.](https://res.cloudinary.com/dmqliab0t/image/upload/f_auto,q_auto,w_1200/v1785335149/blog/generated/qp-b9-010-diag2.png)
*Две библиотеки, два соглашения о прокси: httpx устанавливает прокси на клиенте, aiohttp для каждого запроса.*

## 200 — это не успех

Самая тонкая ошибка асинхронного парсинга — считать HTTP 200 завершенным. Антибот-системы возвращают 200 с CAPTCHA-страницей, уведомлением об отказе в доступе, пустым набором результатов или JS-вызовом — так что прокси, который оценивает "успех" только по коду состояния, тихо подсовывает вам заблокированные страницы. Проверьте *содержимое*: проверьте наличие известного элемента, минимальную длину или отсутствие маркеров вызова, прежде чем доверять ответу. Это `looks_real()` ворота в цикле повторных попыток выше.

## Когда прекратить ручное создание стека

Указанный выше шаблон — асинхронный клиент, семафор, тайм-ауты, ротация, проверка содержимого — справляется с большинством целей чисто. Но как только сайт добавляет Cloudflare, TLS-отпечатки или тяжелый рендеринг на стороне клиента, чистый асинхронный HTTP начинает проигрывать независимо от вашего прокси, потому что рукопожатие TLS на Python выглядит не так, как у Chrome. В этой точке [Scraper API](https://quantumproxies.io/scraper-api), который несет реальный отпечаток браузера, меняет IP и рендерит JavaScript по требованию, требует меньше кода и дает более высокий уровень успеха, чем поддержка всего этого вручную. Наш пост о [стоимости headless против HTTP](https://quantumproxies.io/blog/headless-browser-vs-http-requests-cost) описывает, где такая эскалация окупается.

## Часто задаваемые вопросы

### Как использовать прокси с aiohttp?

Передайте URL прокси для каждого запроса: `session.get(url, proxy="http://user:pass@host:port")`. В отличие от `requests`, aiohttp не принимает словарь прокси на сессии. Всегда переопределяйте стандартный User-Agent (`Python/3.x aiohttp/3.x` — это явный сигнал бота) и установите `ClientTimeout`, чтобы мертвый выход не мог повесить корутину.

### Что лучше для асинхронного парсинга: httpx или aiohttp?

Выбирайте `httpx` по умолчанию: один клиент работает синхронно и асинхронно, HTTP/2 встроен, а прокси — это единственный аргумент. Выбирайте `aiohttp`, когда вам нужен максимальный контроль над параллелизмом — явные ограничения пула соединений и прокси для каждого запроса подходят для больших, быстрых обходов. Оба хороши; стратегия прокси важнее, чем библиотека.

### Сколько одновременных запросов я должен запускать?

Не столько, сколько позволяет ваша машина — столько, сколько терпят цель и ваш пул. Начните с семафора из 10–20 в процессе выполнения, следите за уровнем блокировок и ошибок, и повышайте его только тогда, когда успех остается высоким. На одном липком IP оставайтесь в однозначных числах. Более широкая ротация IP позволяет вам безопасно запускать более высокую общую параллельность.

### Почему мой асинхронный парсер блокируется, когда синхронный не блокировался?

Потому что параллелизм концентрирует сигнал: множество одновременных запросов с одного IP — это классический бот-паттерн. Распределите нагрузку по вращающемуся пулу, ограничьте с помощью семафора, добавьте джиттерный откат на повторных попытках и проверьте содержимое ответа — 200 все еще может быть страницей вызова. Скорость без ротации — это то, что привело к блокировке.

Это полный асинхронный шаблон: выберите клиента, настройте прокси правильным образом для него, ограничьте параллелизм с помощью семафора, ограничьте каждую фазу тайм-аута, меняйте на логической границе и никогда не доверяйте голому 200. Сначала настройте слой прокси правильно, и большинство списка блокировок исчезнет до того, как вы его достигнете.

[Попробуйте QuantumProxies Scraper API](https://quantumproxies.io/scraper-api)
