Async Python Scraping met httpx, aiohttp en Proxies

Async verandert een trage scraper in een snelle — en een snelle scraper in een geblokkeerde, tenzij je proxies, semaforen en timeouts goed instelt. Hier is het volledige patroon voor httpx en aiohttp, met code.

Als je scraper het grootste deel van zijn tijd wacht op het netwerk, is async de grootste snelheidswinst die beschikbaar is — en proxies zorgen ervoor dat die snelheid je niet verbiedt. Deze gids behandelt async Python scraping met httpx en aiohttp plus proxies van begin tot eind: hoe elke bibliotheek een proxy instelt, hoe je gelijktijdigheid beperkt met een semafoor, hoe je timeouts instelt die daadwerkelijk afgaan, en hoe je IP's roteert zonder je sessies te verstoren. Alles met uitvoerbare code en tijdelijke inloggegevens die je kunt vervangen door je eigen.

Waarom async, en waar proxies passen

Standaard requests is blokkerend: elke oproep wacht op de reactie voordat de volgende begint. Haal 500 pagina's op en je betaalt 500 rondreizen achter elkaar. Async haalt ze gelijktijdig op in één event loop, zodat de totale tijd neigt naar de langzaamste enkele aanvraag in plaats van de som. Het probleem: een uitbarsting van gelijktijdige aanvragen vanaf één IP is precies het signaal waar anti-bot systemen op letten. De oplossing is niet om te vertragen tot een slakkengang — het is om het verkeer te verspreiden over een roterende proxy pool en bewust te beperken met een semafoor.

Stel een proxy in httpx (async)

httpx is de pragmatische standaard omdat één clientmodel zowel sync als async doet, en het ondersteunt HTTP/2. Let op de moderne API: het is een enkelvoudig proxy= argument op de client, niet de oude proxies= dict — een veelvoorkomende bron van "waarom wordt mijn proxy genegeerd" verwarring na een upgrade. Inloggegevens gaan direct in de proxy-URL.

import asyncio, httpx

PROXY = "http://USER:PASS@gate.quantumproxies.io:8000"

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)))

Eén client, hergebruikt voor elke aanvraag, is het punt — het houdt de verbindingspool warm zodat je herhaalde TLS-handshakes via de proxy overslaat. Een nieuwe client per aanvraag maken is de meest voorkomende async prestatiebug: het gooit verbindinghergebruik en cookiestatus weg bij elke oproep.

Stel een proxy in aiohttp (per aanvraag)

aiohttp is asyncio-native en biedt de fijnste controle over gelijktijdigheid, maar zijn proxy-conventie verschilt: de proxy wordt per aanvraag doorgegeven op session.get(), niet op de sessie. Dat is eigenlijk handig voor rotatie. Twee dingen bijten mensen hier — de standaard User-Agent is letterlijk Python/3.x aiohttp/3.x, een duidelijke aanwijzing die je moet overschrijven, en je moet de verbindingspool expliciet dimensioneren.

import aiohttp, asyncio

PROXY = "http://USER:PASS@gate.quantumproxies.io:8000"
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) beperkt het totale aantal open verbindingen en verbindingen naar een enkele host — een eerste lijn van beleefdheid die voorkomt dat één doelwit je hele pool absorbeert.

Diagram van een async scraping loop: asyncio.gather voedt een semafoor, dan een roterende proxy-gateway, dan parallelle doelwitophalingen
De semafoor beperkt; de roterende gateway verbergt. Je hebt beide nodig — async snelheid zonder een van beide is een blokkadegolf.

Beperk gelijktijdigheid met een semafoor

Onbeperkt gelijktijdige aanvragen afvuren is de snelste manier om een proxy pool te verbranden en snelheidslimieten te overschrijden. Een asyncio.Semaphore is de begrenzer: het beperkt hoeveel aanvragen er tegelijkertijd in behandeling zijn, ongeacht hoeveel taken je in de wachtrij zet. Stel een globale limiet in, en voor agressieve taken ook een per-domein limiet.

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))

Het juiste aantal hangt af van de tolerantie van het doelwit en de grootte van je pool, niet van hoe snel je machine kan gaan. Begin conservatief (10–20), houd je blokkeerpercentage in de gaten, en verhoog het alleen terwijl het succes hoog blijft. Op een enkel plakkerig IP, houd het in de enkele cijfers.

Timeouts: modelleer elke fase

Een geproxiede aanvraag faalt op meer plaatsen dan een directe — DNS-resolutie, verbinding maken met de proxy, tunnelopzet, verbinding maken met het doelwit, wachten op headers, en het lezen van de body zijn allemaal afzonderlijke vertragingen. Een enkele algemene timeout verbergt welke fase vastliep. aiohttp's ClientTimeout(total=, connect=, sock_read=) en httpx's Timeout() laten je ze afzonderlijk begrenzen. De enige regel zonder uitzonderingen: geef nooit een aanvraag zonder timeout, anders zal een dode exit een coroutine voor altijd laten vastlopen en stilletjes je event loop uithongeren.

Roteer proxies zonder sessies te breken

Er zijn twee rotatiestrategieën en het kiezen van de verkeerde corrumpeert je gegevens. Willekeurige rotatie per aanvraag is perfect voor stateless pagina-ophalingen. Maar het vernietigt elke flow die afhankelijk is van cookies, login of localisatie, omdat aanvraag twee op een ander IP terechtkomt dan aanvraag één. De schone splitsing: roteer op de logische-eenheidsgrens — één IP per crawlesegment of per account — en gebruik een roterende gateway die je automatisch een nieuwe exit geeft zodat je code nooit een lijst beheert.

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

# sticky session: same IP for a multi-step flow, tag the session id
STICKY = "http://USER-session-a1b2:PASS@gate.quantumproxies.io:8000"

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))

Een roterende residentiële gateway is de pragmatische standaard voor scraping met hoge gelijktijdigheid: rotatie per aanvraag over 90M+ IP's in 200+ landen, met plakkerige sessies wanneer een winkelwagen of login dezelfde exit nodig heeft voor een paar minuten. Als je residentieel tegen ISP of datacenter afweegt voor de klus, legt onze gids over welk proxytype te gebruiken de afwegingen uit.

Krijg een roterende residentiële gateway

Retries, backoff en jitter

Dode exits en tijdelijke blokkades zijn normaal op schaal, niet uitzonderlijk. Maar naïeve retries maken het erger: wanneer 50 async taken op hetzelfde moment falen en allemaal onmiddellijk opnieuw proberen, vuur je een gesynchroniseerde uitbarsting af die het doelwit harder raakt dan de oorspronkelijke run. Voeg exponentiële backoff plus willekeurige jitter toe zodat retries zich verspreiden, beperk de pogingen, en roteer het IP bij falen in plaats van de verbrande opnieuw te gebruiken.

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
Zij-aan-zij vergelijking van httpx en aiohttp proxy-conventies, timeouts en gelijktijdigheidscontroles voor async scraping
Twee bibliotheken, twee proxy-conventies: httpx stelt de proxy in op de client, aiohttp per aanvraag.

Een 200 is geen succes

De subtielste async scraping bug is HTTP 200 als voltooid beschouwen. Anti-bot systemen geven 200 terug met een CAPTCHA-pagina, een toegang-geweigerd melding, een lege resultaatset of een JS-uitdaging — dus een proxy die "succes" scoort op statuscode alleen voedt je stilletjes geblokkeerde pagina's. Valideer de inhoud: controleer op een bekend element, een minimale lengte, of de afwezigheid van uitdagingmarkeringen voordat je een reactie vertrouwt. Dat is de looks_real() poort in de retry loop hierboven.

Wanneer stoppen met het handmatig bouwen van de stack

Het bovenstaande patroon — async client, semafoor, timeouts, rotatie, inhoudsvalidatie — behandelt de meeste doelen netjes. Maar zodra een site Cloudflare, TLS-vingerafdrukken of zware client-side rendering toevoegt, begint raw async HTTP te verliezen ongeacht je proxy, omdat een Python TLS-handshake niets lijkt op die van Chrome. Op dat punt is een Scraper API die een echte browservingerafdruk draagt, IP's roteert en JavaScript op aanvraag rendert minder code en een hogere succesratio dan alles handmatig onderhouden. Onze post over headless vs HTTP kosten behandelt waar die escalatie loont.

Veelgestelde vragen

Hoe gebruik ik een proxy met aiohttp?

Geef de proxy-URL per aanvraag door: session.get(url, proxy="http://user:pass@host:port"). In tegenstelling tot requests neemt aiohttp geen proxies dict op de sessie. Overschrijf altijd de standaard User-Agent (Python/3.x aiohttp/3.x is een voor de hand liggend botsignaal) en stel een ClientTimeout in zodat een dode exit de coroutine niet kan laten vastlopen.

Is httpx of aiohttp beter voor async scraping?

Kies httpx als standaard: één client werkt sync en async, HTTP/2 is ingebouwd, en de proxy is een enkel argument. Kies aiohttp wanneer je maximale controle over gelijktijdigheid wilt — expliciete verbindingspoolbeperkingen en per-aanvraag proxies passen bij grote, snelle crawls. Beide zijn prima; de proxystrategie is belangrijker dan de bibliotheek.

Hoeveel gelijktijdige aanvragen moet ik uitvoeren?

Niet zoveel als je machine toestaat — zoveel als het doelwit en je pool verdragen. Begin met een semafoor van 10–20 in behandeling, houd het blokkeer- en foutenpercentage in de gaten, en verhoog het alleen terwijl het succes hoog blijft. Op één plakkerig IP, blijf in de enkele cijfers. Brede IP-rotatie laat je hogere totale gelijktijdigheid veilig uitvoeren.

Waarom wordt mijn async scraper geblokkeerd terwijl de sync dat niet deed?

Omdat gelijktijdigheid het signaal concentreert: veel gelijktijdige aanvragen vanaf één IP is een klassiek botpatroon. Verspreid de belasting over een roterende pool, beperk met een semafoor, voeg jittered backoff toe bij retries, en valideer de inhoud van de reactie — een 200 kan nog steeds een uitdagingpagina zijn. Snelheid zonder rotatie is wat je geblokkeerd heeft gekregen.

Dat is het volledige async patroon: kies een client, stel de proxy op de juiste manier in, beperk gelijktijdigheid met een semafoor, begrens elke timeoutfase, roteer op de logische grens, en vertrouw nooit op een kale 200. Krijg de proxylaag eerst goed en het grootste deel van de blokkeerlijst verdwijnt voordat je deze raakt.

Probeer de QuantumProxies Scraper API