Parser di Scrapy mengalami timeout, kumpulan proxy habis jauh lebih cepat dari yang diharapkan, dan log dipenuhi dengan respons 407 dan 403 — apakah ini terlihat familiar? Dalam 90% kasus, masalahnya bukan pada proxy itu sendiri, tetapi pada cara penulisan DownloaderMiddleware. Mari kita bahas lima kesalahan paling umum dalam middleware yang mengubah trafik mahal menjadi permintaan sampah, dan tunjukkan cara memperbaikinya dengan kode.
Kesalahan 1: rotasi primitif tanpa mempertimbangkan status proxy
Konstruksi yang paling umum ditemukan dalam tutorial adalah random.choice(PROXY_LIST) di dalam process_request. Masalahnya adalah rotasi semacam ini tidak mengetahui proxy mana yang baru saja diblokir dan mana yang masih aktif. Akibatnya, parser terus mengirim permintaan melalui IP yang sudah diblokir, menerima 403/429, melakukan retry — dan memilih alamat yang sama lagi, karena pemilihannya acak dan tidak mengecualikan node "buruk".
Pendekatan yang benar adalah menjaga status setiap proxy: jumlah permintaan yang berhasil, jumlah kesalahan, waktu penggunaan terakhir. Berikut adalah versi kerja minimal:
import random
import time
class ProxyPool:
def __init__(self, proxies):
self.proxies = {p: {"fails": 0, "last_used": 0, "banned_until": 0} for p in proxies}
def get_proxy(self):
now = time.time()
available = [
p for p, state in self.proxies.items()
if state["banned_until"] < now
]
if not available:
# jika semua diblokir, reset yang paling "lama" diblokir
available = list(self.proxies.keys())
return random.choice(available)
def mark_fail(self, proxy, cooldown=300):
self.proxies[proxy]["fails"] += 1
self.proxies[proxy]["banned_until"] = time.time() + cooldown
def mark_success(self, proxy):
self.proxies[proxy]["fails"] = 0
self.proxies[proxy]["last_used"] = time.time()
Kumpulan ini mengecualikan IP yang diblokir selama waktu "pendinginan" (cooldown) dan mengembalikannya ke dalam rotasi nanti. Ini sudah mengurangi pengeluaran trafik secara signifikan, karena Anda tidak terus-menerus menyerang node yang sudah diblokir.
Kesalahan 2: penanganan retry dan status kode yang salah
Kesalahan tipikal kedua adalah menggunakan RetryMiddleware standar dari Scrapy tanpa modifikasi. Secara default, ia akan melakukan retry pada permintaan yang sama di proxy yang sama yang gagal, kecuali Anda secara eksplisit menyisipkan perubahan proxy di process_exception. Akibatnya, kita mendapatkan gambaran klasik: 3 retry, 3 pemblokiran, permintaan tetap gagal, dan trafik sudah terbuang.
Hal kedua adalah tidak semua status kode perlu di-retry dengan cara yang sama. 429 (Terlalu Banyak Permintaan) memerlukan jeda dan perubahan IP, 403 biasanya berarti pemblokiran proxy tertentu (perlu penggantian segera), sedangkan 5xx — sering kali merupakan masalah sementara di sisi server, retry bisa dilakukan pada proxy yang sama. Jika semuanya digabungkan, middleware akan terlalu agresif membakar proxy atau terlalu lama menunggu di tempat yang seharusnya segera mengganti IP.
class SmartRetryMiddleware:
def __init__(self, pool):
self.pool = pool
def process_response(self, request, response, spider):
proxy = request.meta.get("proxy")
if response.status in (403, 407):
if proxy:
self.pool.mark_fail(proxy, cooldown=600)
new_request = request.copy()
new_request.meta["proxy"] = self.pool.get_proxy()
new_request.dont_filter = True
return new_request
if response.status == 429:
if proxy:
self.pool.mark_fail(proxy, cooldown=120)
new_request = request.copy()
new_request.meta["proxy"] = self.pool.get_proxy()
new_request.dont_filter = True
return new_request
if proxy:
self.pool.mark_success(proxy)
return response
Di sini penting untuk membedakan cooldown berdasarkan jenis kesalahan: pemblokiran keras (403/407) — jeda lama, batas frekuensi (429) — jeda pendek. Ini menghemat puluhan persen trafik pada pengulangan yang panjang.
Kesalahan 3: tidak adanya sesi sticky untuk situs dengan otorisasi
Jika parser bekerja dengan situs yang memiliki login, keranjang belanja, paginasi dengan penyimpanan status, atau captcha dengan pemeriksaan berdasarkan IP — perubahan proxy pada setiap permintaan merusak sesi. Situs melihat bahwa permintaan №1 datang dari satu IP, sedangkan permintaan №2 (dalam sesi cookie yang sama) datang dari IP lain, dan ini segera memicu perlindungan dari bot, bahkan jika kedua IP "bersih".
Solusinya adalah mengaitkan satu proxy dengan satu sesi logis (misalnya, dengan akun tertentu atau dengan rangkaian permintaan dalam satu domain) untuk waktu tetap, bukan menggantinya pada setiap permintaan. Ini disebut sesi sticky.
class StickySessionMiddleware:
def __init__(self, pool, ttl=600):
self.pool = pool
self.ttl = ttl
self.sessions = {} # session_id -> (proxy, expires_at)
def process_request(self, request, spider):
session_id = request.meta.get("session_id")
if not session_id:
return
now = time.time()
session = self.sessions.get(session_id)
if session and session[1] > now:
request.meta["proxy"] = session[0]
else:
proxy = self.pool.get_proxy()
self.sessions[session_id] = (proxy, now + self.ttl)
request.meta["proxy"] = proxy
Untuk tugas yang memerlukan stabilitas IP sepanjang sesi — otorisasi, bekerja dengan akun pribadi, formulir multi-langkah — yang paling cocok adalah proxy residensial dengan dukungan sesi: mereka memungkinkan untuk mempertahankan IP keluar yang sama selama beberapa menit atau jam, dan kemudian menggantinya secara terkontrol, bukan secara acak pada setiap permintaan.
Kesalahan 4: pengiriman otorisasi proxy yang salah
Kesalahan keempat adalah teknis, tetapi muncul di hampir setiap proyek kedua. Pengembang mengirimkan nama pengguna dan kata sandi proxy langsung dalam URL seperti http://user:pass@ip:port melalui request.meta["proxy"]. Ini berfungsi dalam sebagian besar kasus, tetapi saat menggunakan proxy melalui tunnel HTTPS atau saat bekerja dengan beberapa penyedia, cara otorisasi ini tidak diproses dengan benar oleh HttpProxyMiddleware standar, dan permintaan gagal dengan 407 Proxy Authentication Required, meskipun kredensialnya benar.
Cara yang lebih andal adalah mengirimkan header Proxy-Authorization secara eksplisit, yang dikodekan dalam base64:
import base64
class ProxyAuthMiddleware:
def process_request(self, request, spider):
proxy = request.meta.get("proxy")
if not proxy:
return
# proxy tanpa kredensial dalam URL
request.meta["proxy"] = proxy
user = request.meta.get("proxy_user")
password = request.meta.get("proxy_pass")
if user and password:
credentials = f"{user}:{password}"
encoded = base64.b64encode(credentials.encode()).decode()
request.headers["Proxy-Authorization"] = f"Basic {encoded}"
Pendekatan ini lebih stabil saat diskalakan hingga ratusan permintaan paralel dan tidak tergantung pada bagaimana versi pustaka tertentu mengurai URL dengan kredensial yang tertanam. Ini sangat penting saat bekerja dengan proxy seluler, di mana otentikasi sering kali terikat pada daftar putih IP atau pemeriksaan header yang ketat.
Kesalahan 5: tidak ada pemantauan dan pencatatan pemblokiran
Kesalahan terakhir dan mungkin yang paling mahal akibatnya adalah tidak adanya pencatatan tentang proxy mana yang diblokir, seberapa sering, dan di domain mana. Tanpa data ini, tidak mungkin untuk memahami apa yang sebenarnya menghabiskan trafik: apakah kumpulan proxy telah habis di situs tertentu, atau apakah masalahnya ada pada parser itu sendiri (permintaan terlalu sering, tidak ada jeda, header yang mencurigakan).
Setidaknya, metrik yang perlu dicatat dalam middleware adalah:
- Jumlah permintaan untuk setiap proxy selama sesi parsing
- Jumlah dan kode kesalahan (403, 407, 429, 5xx) untuk setiap proxy
- Waktu hidup proxy hingga pemblokiran pertama
- Domain di mana pemblokiran terjadi paling sering
import logging
import json
logger = logging.getLogger("proxy_stats")
class ProxyStatsMiddleware:
def __init__(self):
self.stats = {}
def process_response(self, request, response, spider):
proxy = request.meta.get("proxy", "unknown")
domain = request.url.split("/")[2]
key = f"{proxy}|{domain}"
entry = self.stats.setdefault(key, {"requests": 0, "errors": 0})
entry["requests"] += 1
if response.status in (403, 407, 429):
entry["errors"] += 1
if entry["requests"] % 50 == 0:
logger.info(json.dumps(self.stats))
return response
Tanpa statistik semacam ini, setiap upaya untuk "mengoptimalkan" middleware menjadi tebak-tebakan. Dengan statistik ini, Anda dapat melihat dengan jelas: jika, misalnya, domain tertentu memblokir 80% dari kumpulan proxy dalam 10 permintaan pertama — masalahnya bukan pada proxy, tetapi pada pola permintaan (tidak ada rotasi User-Agent, frekuensi terlalu tinggi, tidak ada jeda antara permintaan).
Contoh kerja middleware secara keseluruhan
Mengumpulkan semuanya dalam settings.py — urutan middleware sangat penting, karena menentukan urutan penerapan pemeriksaan:
DOWNLOADER_MIDDLEWARES = {
"myproject.middlewares.ProxyAuthMiddleware": 350,
"myproject.middlewares.StickySessionMiddleware": 400,
"myproject.middlewares.SmartRetryMiddleware": 550,
"myproject.middlewares.ProxyStatsMiddleware": 900,
}
RETRY_ENABLED = False # menonaktifkan retry standar, menggunakan yang kustom
DOWNLOAD_TIMEOUT = 15
CONCURRENT_REQUESTS_PER_DOMAIN = 8
Perhatikan RETRY_ENABLED = False — ini sangat penting, jika tidak, mekanisme retry bawaan Scrapy akan bertentangan dengan logika penggantian proxy Anda, dan permintaan akan mulai diduplikasi atau di-retry dua kali. Juga penting untuk membatasi CONCURRENT_REQUESTS_PER_DOMAIN — paralelisme yang terlalu tinggi pada satu domain bahkan dengan rotasi proxy terlihat mencurigakan bagi sistem anti-bot.
Jenis proxy apa yang harus dipilih untuk Scrapy
Middleware menyelesaikan setengah masalah, setengah lainnya adalah pemilihan kumpulan proxy yang tepat untuk tugas tersebut. Berikut adalah perbandingan untuk skenario parsing yang tipikal.
| Jenis Proxy | Kapan digunakan | Kelebihan | Kekurangan |
|---|---|---|---|
| Proxy Data Center | Parsing massal halaman terbuka tanpa perlindungan anti-bot yang ketat | Kecepatan tinggi, biaya trafik rendah | Mudah terdeteksi, sering masuk daftar hitam |
| Proxy Residensial | Parsing marketplace, situs dengan perlindungan JS, otorisasi | IP nyata, persentase pemblokiran rendah, dukungan sesi sticky | Kecepatan lebih rendah dibandingkan dengan DC |
| Proxy Seluler | Parsing versi seluler situs dan API dengan perlindungan anti-bot yang ketat | Tingkat kepercayaan maksimum dari situs target | Biaya trafik tertinggi |
Aturan praktis: untuk parsing halaman statis tanpa perlindungan yang kuat, proxy data center murah cocok dipadukan dengan middleware yang tepat dari artikel ini. Jika situs menggunakan Cloudflare, PerimeterX, DataDome, atau sistem serupa — IP data center akan diblokir hampir segera, dan lebih menguntungkan untuk langsung beralih ke kumpulan residensial, menghemat waktu untuk debugging middleware.
Checklist sebelum meluncurkan parser ke produksi
- Middleware mengecualikan proxy yang diblokir pada cooldown, bukan memilihnya secara acak
- Logika retry membedakan jenis kesalahan (403/407 vs 429 vs 5xx) dengan cooldown yang berbeda
- Untuk tugas sesi, digunakan pengikatan sticky proxy, bukan perubahan pada setiap permintaan
- Otorisasi proxy dikirim melalui header Proxy-Authorization, bukan hanya melalui URL
- Statistik pemblokiran dicatat berdasarkan domain dan proxy untuk diagnosis masalah
- RetryMiddleware standar Scrapy dinonaktifkan untuk menghindari konflik dengan logika kustom
- Concurrency dibatasi pada nilai yang masuk akal, bukan diatur ke maksimum
Kesimpulan
Sebagian besar masalah dengan pengeluaran trafik proxy di Scrapy diselesaikan bukan dengan membeli lebih banyak kumpulan IP, tetapi dengan memperbaiki logika middleware: memisahkan kesalahan berdasarkan jenis, sesi sticky untuk skenario yang kompleks, otorisasi yang benar, dan pemantauan pemblokiran yang konstan. Kode dari artikel ini dapat digunakan sebagai dasar dan disesuaikan untuk proyek tertentu — strukturnya tetap berfungsi baik untuk parser kecil maupun untuk instalasi Scrapy-Cluster yang terdistribusi.
Jika middleware sudah diatur dengan benar, tetapi pemblokiran tetap terjadi terlalu sering — kemungkinan masalahnya ada pada kualitas kumpulan IP itu sendiri. Untuk parsing situs dengan perlindungan anti-bot yang canggih, sebaiknya mencoba proxy residensial dengan dukungan sesi — mereka secara signifikan mengurangi persentase pemicu perlindungan dibandingkan dengan alamat data center dengan logika middleware yang sama.