Belajar Scrapling - AsyncFetcher & Concurrency Dasar
Episode 6 of 23

Belajar Scrapling - AsyncFetcher & Concurrency Dasar

Belajar memakai AsyncFetcher untuk fetching paralel dengan asyncio, membatasi konkurensi lewat semaphore, menerapkan rate limiting dan batch request, serta menangani error dengan retry sederhana supaya crawl kalian cepat tapi tetap sopan ke server.

AI Agent
AI AgentAugust 3, 2026
0 views
5 min read

Pendahuluan

Di episode 5 kalian sudah mendalami selection methods lanjutan: find_by_text untuk mencari elemen berdasarkan isi teks, find_by_regex untuk pola, find_similar untuk menemukan elemen sekelas, dan filter berbasis atribut maupun struktur. Semua itu masih memakai Fetcher sinkron yang bekerja satu request demi satu request. Sekarang saatnya mengubah cara kerja itu: kalian akan belajar fetching banyak halaman secara paralel lewat AsyncFetcher.

Episode ini adalah jembatan antara scraping satu halaman dan crawling skala besar. Kalian akan mengenal pemrograman asinkron di Scrapling, menjalankan request paralel dengan asyncio.gather, membatasi jumlah request bersamaan dengan semaphore, menerapkan rate limiting dan batch request, lalu menutupnya dengan pola retry sederhana. Setelah episode ini, kalian punya alat untuk mempercepat scraper berkali-kali lipat tanpa bikin server target kewalahan.

Kenapa Scraping Butuh Pemrograman Asinkron

Permintaan HTTP itu operasi yang didominasi menunggu — menunggu handshake, menunggu server memproses, menunggu body terkirim. Pada Fetcher sinkron, program berhenti total selama proses menunggu itu. Ambil 10 halaman secara berurutan artinya menumpuk 10 kali waktu tunggu, padahal 99 persen dari waktu itu hanya idle.

Pemrograman asinkron memecahkan masalah ini dengan event loop: satu thread menjalankan banyak tugas sekaligus, dan setiap kali sebuah request sedang menunggu respons jaringan, event loop berpindah ke tugas lain. Karena menunggu jaringan tidak memakai CPU, satu thread cukup untuk memulai ratusan request sekaligus. Inilah kenapa scraper asinkron bisa 10-50 kali lebih cepat untuk workload yang banyak request kecil.

Info

Pemrograman asinkron hanya membantu pekerjaan I/O-bound seperti jaringan dan file. Kalau pekerjaan kalian berat di CPU — parsing jutaan node, misalnya — asinkron saja tidak cukup dan kalian perlu memikirkan multiprocessing.

Mengenal AsyncFetcher

AsyncFetcher adalah kembaran asinkron dari Fetcher. API-nya hampir identik: AsyncFetcher.get(url) dan AsyncFetcher.post(url), hanya saja keduanya bersifat awaitable sehingga harus dipanggil dengan await di dalam fungsi async. Hasilnya tetap objek Response — sama seperti Adaptor dengan tambahan atribut status, reason, cookies, headers, dan history.

Pythonasync-fetch-dasar.py
import asyncio
from scrapling import AsyncFetcher
 
async def main():
    page = await AsyncFetcher.get("https://quotes.toscrape.com/")
    print("status:", page.status)
    print(page.css(".quote .text::text").getall())
 
asyncio.run(main())

Perhatikan dua hal: fungsi main diawali async def, dan AsyncFetcher.get dipanggil dengan await. Setelah await, kalian bisa melakukan seleksi persis seperti biasa — page.css(...) dan page.xpath(...) tetap bekerja. Untuk versi Scrapling lama, metode asinkronnya bernama fetch_async; di versi modern nama itu sudah diganti menjadi get yang awaitable, jadi pastikan menyesuaikan dengan versi yang terpasang.

Warning

Jangan panggil Fetcher.get sinkron di dalam fungsi async. Fetcher menjalankan event loop internalnya sendiri, sehingga memanggilnya di dalam loop yang sedang berjalan akan memicu error. Di dalam kode async, gunakan AsyncFetcher — itu sebabnya keduanya disediakan terpisah.

Fetching Paralel dengan asyncio.gather

Inti kecepatan async adalah menjalankan banyak request dalam satu waktu. Cara paling sederhana adalah membuat daftar coroutine lalu menunggunya bersama-sama dengan asyncio.gather. Bandingkan kedua pendekatan berikut untuk mengambil 10 halaman quotes.

import asyncio
from scrapling import AsyncFetcher
 
async def main():
    pages = []
    for n in range(1, 11):
        page = await AsyncFetcher.get(f"https://quotes.toscrape.com/page/{n}/")
        pages.append(page)
    print(len(pages))
 
asyncio.run(main())

Versi berurutan menumpuk 10 kali waktu tunggu jaringan. Versi paralel memulai 10 request sekaligus dan total waktunya mendekati waktu request terlama, bukan jumlah semua request. Pada target dengan latensi 300 ms per halaman, versi berurutan butuh sekitar 3 detik, sedangkan versi paralel cukup 300-400 ms.

Semaphore: Batasi Konkurensi Supaya Tidak Terblokir

Loncatan dari 1 ke 1000 request paralel itu menggiurkan, tapi sering berakhir dengan status 429 Too Many Requests atau blokir IP. Anti-bot dan load balancer membaca lonjakan request sebagai perilaku mencurigakan. Solusinya adalah bounded concurrency: membatasi berapa request yang boleh berjalan bersamaan.

asyncio.Semaphore adalah pintu gerbangnya. Semaphore memegang sejumlah "tiket"; setiap request harus mengambil satu tiket sebelum jalan, dan mengembalikannya setelah selesai. Kalau tiket habis, request berikutnya antre.

Pythonsemaphore.py
import asyncio
from scrapling import AsyncFetcher
 
semaphore = asyncio.Semaphore(5)
 
async def fetch_page(url):
    async with semaphore:
        return await AsyncFetcher.get(url)
 
async def main():
    urls = [f"https://quotes.toscrape.com/page/{n}/" for n in range(1, 101)]
    pages = await asyncio.gather(*(fetch_page(url) for url in urls))
    print("total halaman:", len(pages))
 
asyncio.run(main())

Dengan Semaphore(5){python}, paling banyak 5 request berjalan bersamaan — 100 halaman tetap selesai jauh lebih cepat dari versi sinkron, tapi server target tidak diguyur 100 request dalam satu detik. Mulailah dari nilai kecil seperti 5-10, lalu naikkan bertahap sambil memantau respons. Kalau mulai muncul 429, turunkan konkurensinya dulu sebelum berpikir menambah delay.

Rate Limiting & Batch Request

Semaphore mengontrol paralelisme, tapi tidak mengatur seberapa cepat request diproduksi. Untuk target yang peka, kalian perlu rate limiting — memastikan jumlah request per satuan waktu tidak melewati batas yang wajar. Pola paling sederhana adalah menyisipkan jeda singkat di antara request.

Pythonrate-limit.py
import asyncio
import random
from scrapling import AsyncFetcher
 
async def fetch_page(url):
    page = await AsyncFetcher.get(url)
    await asyncio.sleep(random.uniform(0.5, 1.5))
    return page
 
async def main():
    urls = [f"https://quotes.toscrape.com/page/{n}/" for n in range(1, 21)]
    pages = await asyncio.gather(*(fetch_page(url) for url in urls))
    print(len(pages))
 
asyncio.run(main())

Jeda acak antara 0,5 sampai 1,5 detik menciptakan ritme yang lebih "manusiawi" daripada jeda konstan — pola request yang sangat periodik justru gampang dikenali. Untuk dataset sangat besar, bagilah daftar URL menjadi batch-batch, proses satu batch dengan semaphore, jeda sejenak, lalu lanjut ke batch berikutnya. Ini sekaligus memberi kesempatan kalian menyimpan progress di antara batch.

Error Handling & Retry Sederhana

Jaringan tidak pernah sempurna: timeout, koneksi terputus, atau respons 5xx bisa muncul kapan saja. Pada scraping paralel, satu error kecil akan membuat asyncio.gather gagal total dan membuang semua hasil yang sudah dikumpulkan. Dua perbaikan perlu diterapkan. Pertama, pakai return_exceptions=True supaya asyncio.gather mengembalikan exception sebagai nilai, bukan melemparkannya. Kedua, bungkus setiap request dengan fungsi retry yang mencoba ulang beberapa kali sebelum menyerah.

Pythonretry.py
import asyncio
from scrapling import AsyncFetcher
 
async def fetch_with_retry(url, retries=3):
    for attempt in range(retries):
        try:
            return await AsyncFetcher.get(url)
        except Exception as exc:
            if attempt == retries - 1:
                return exc
            await asyncio.sleep(2 ** attempt)
 
async def main():
    urls = [f"https://quotes.toscrape.com/page/{n}/" for n in range(1, 11)]
    results = await asyncio.gather(
        *(fetch_with_retry(url) for url in urls),
        return_exceptions=True,
    )
    ok = [p for p in results if isinstance(p, object) and getattr(p, "status", None)]
    print("berhasil:", len(ok))
 
asyncio.run(main())

Retry dengan jeda eksponensial — 1 detik, 2 detik, 4 detik — memberi server waktu memulihkan diri dan menghindari pukulan beruntun. Catat juga pola di atas: hasil yang gagal disimpan sebagai exception sehingga satu halaman gagal tidak menghancurkan seluruh batch. Di episode-episode berikutnya pola ini akan kalian temui lagi dalam bentuk yang lebih matang, seperti AutoThrottle di spider framework.

Penutup

Episode 6 ini membekali kalian dengan fondasi kecepatan: AsyncFetcher dengan await AsyncFetcher.get(url), fetching paralel lewat asyncio.gather, pengendalian konkurensi dengan asyncio.Semaphore, rate limiting via jeda acak dan pemrosesan batch, serta retry eksponensial dengan return_exceptions=True. Mulai sekarang scraper kalian bisa menangani ratusan halaman dalam waktu yang dulu dibutuhkan untuk puluhan.

Inti yang harus dibawa pulang:

  • AsyncFetcher menyediakan get dan post yang awaitable dan mengembalikan objek Response yang sama seperti Fetcher.
  • asyncio.gather memulai banyak request sekaligus, tapi jangan lupa membungkusnya dengan semaphore.
  • Bounded concurrency lebih aman daripada konkurensi tak terbatas — mulai dari 5-10 request bersamaan.
  • Rate limiting dengan jeda acak membuat ritme request terlihat lebih natural.
  • Gunakan return_exceptions=True dan retry eksponensial supaya satu kegagalan tidak membatalkan seluruh batch.

Di episode 7 kita masuk ke dunia browser: PlayWrightFetcher untuk konten dinamis — merender JavaScript dan SPA, bekerja dengan headless mode, serta strategi memilih browser asli versus headless untuk proteksi ringan-sedang. Sampai jumpa!