Scrapy ile çalışan bir ayrıştırıcı zaman aşımına uğruyor, proxy havuzu beklenenden çok daha hızlı tükeniyor ve loglar 407 ve 403 yanıtlarıyla dolu — tanıdık bir manzara mı? %90 durumunda sorun, proxy'lerde değil, DownloaderMiddleware'in nasıl yazıldığıyla ilgilidir. Trafiği çöp isteklere dönüştüren middleware'deki en yaygın beş hatayı inceliyoruz ve bunları kod ile nasıl düzelteceğimizi gösteriyoruz.
Hata 1: Proxy durumunu dikkate almadan basit döngü
Eğitimlerde sıkça karşılaşılan en yaygın yapı, random.choice(PROXY_LIST)'in process_request içinde kullanılmasıdır. Sorun, bu tür bir döngünün hangi proxy'nin yasaklandığını ve hangisinin hala aktif olduğunu bilmemesidir. Sonuç olarak, ayrıştırıcı zaten yasaklı bir IP üzerinden istek göndermeye devam eder, 403/429 alır, tekrar dener — ve yine aynı adresi seçer, çünkü seçim rastgeledir ve "kötü" düğümleri hariç tutmaz.
Doğru yaklaşım, her proxy'nin durumunu izlemektir: başarılı istek sayısı, hata sayısı, son kullanım zamanı. İşte minimum çalışma örneği:
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:
# eğer hepsi yasaklıysa, en "eski" yasaklamayı sıfırlıyoruz
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()
Bu havuz, yasaklı IP'leri "soğuma" süresi boyunca hariç tutar ve daha sonra yeniden devreye alır. Bu, trafiği önemli ölçüde azaltır çünkü aynı yasaklı düğüme sürekli olarak istek göndermiyorsunuz.
Hata 2: Tekrar deneme ve durum kodlarının yanlış işlenmesi
İkinci tipik hata, standart RetryMiddleware'in Scrapy'den değiştirilmeden kullanılmasıdır. Varsayılan olarak, başarısız olan isteği aynı proxy üzerinden tekrar dener, eğer process_exception'da proxy değişikliği yapmadıysanız. Sonuç olarak klasik bir manzara oluşur: 3 tekrar deneme, 3 yasak, istek yine düşer ve trafik zaten harcanmıştır.
İkinci nokta — tüm durum kodlarının aynı şekilde tekrar denemeye tabi tutulmaması gerektiğidir. 429 (Too Many Requests) bir bekleme süresi ve IP değişikliği gerektirir, 403 genellikle belirli bir proxy'nin yasaklandığını gösterir (acil değiştirme gerekir), 5xx ise genellikle sunucu tarafında geçici bir sorundur, aynı proxy üzerinden tekrar denemek mümkündür. Her şeyi bir araya toplarsanız, middleware ya proxy'leri çok agresif bir şekilde yakar ya da hemen IP değiştirilmesi gereken yerlerde çok uzun süre bekler.
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
Burada hata türüne göre soğuma süresini ayırmak önemlidir: sert yasak (403/407) — uzun bir bekleme süresi, frekans limiti (429) — kısa bir bekleme süresi. Bu, uzun çalışmalarda trafiğin ondan fazla yüzdesini tasarruf ettirir.
Hata 3: Yetkilendirme gerektiren siteler için yapışkan oturumların olmaması
Eğer ayrıştırıcı, giriş, sepet, durumun korunmasıyla sayfalandırma veya IP üzerinden kontrol edilen captcha ile çalışan bir siteyle çalışıyorsa — her istekte proxy değiştirmek oturumu bozar. Site, isteğin 1. numarasının bir IP'den geldiğini, isteğin 2. numarasının (aynı çerez oturumu içinde) başka bir IP'den geldiğini görür ve bu, bot korumasını anında tetikler, her iki IP de "temiz" olsa bile.
Çözüm, bir proxy'yi belirli bir mantıksal oturumla (örneğin, belirli bir hesapla veya bir alan adı içindeki istek zinciriyle) sabit bir süre boyunca ilişkilendirmektir, her istekte değiştirmek yerine. Buna yapışkan oturum denir.
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
Oturum boyunca IP'nin kararlılığı gereken görevler için — yetkilendirme, kullanıcı paneli ile çalışma, çok aşamalı formlar — en iyi şekilde rezidans proxy'leri ile desteklenen oturumlar uygundur: bunlar, aynı çıkış IP'sini birkaç dakika veya saat boyunca tutmanıza ve ardından kontrol edilebilir bir şekilde değiştirmenize olanak tanır, her istekte rastgele değil.
Hata 4: Proxy yetkilendirmesinin yanlış iletilmesi
Dördüncü hata — teknik bir hata, ancak neredeyse her ikinci projede karşılaşılır. Geliştiriciler, proxy'nin kullanıcı adı ve şifresini http://user:pass@ip:port biçimindeki URL'de request.meta["proxy"] üzerinden iletir. Bu çoğu durumda çalışır, ancak HTTPS tüneli üzerinden proxy kullanırken veya bazı sağlayıcılarla çalışırken bu yetkilendirme yöntemi standart HttpProxyMiddleware tarafından yanlış işlenir ve istekler 407 Proxy Authentication Required hatasıyla düşer, oysa kimlik bilgileri doğrudur.
Daha güvenilir bir yöntem, Proxy-Authorization başlığını açıkça, base64 olarak kodlayarak iletmektir:
import base64
class ProxyAuthMiddleware:
def process_request(self, request, spider):
proxy = request.meta.get("proxy")
if not proxy:
return
# kimlik bilgileri olmayan proxy
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}"
Bu yaklaşım, yüzlerce eşzamanlı isteğe ölçeklenirken daha kararlı çalışır ve belirli bir kütüphane sürümünün gömülü kimlik bilgileriyle URL'yi nasıl ayrıştırdığına bağlı değildir. Özellikle mobil proxy'lerle çalışırken bu kritik öneme sahiptir, çünkü kimlik doğrulama genellikle IP beyaz listesini veya başlıkların sıkı kontrolünü gerektirir.
Hata 5: Yasakların izlenmesi ve loglanmaması
Son hata ve belki de sonuçları en pahalı olanı — hangi proxy'lerin yasaklandığı, ne sıklıkla ve hangi alanlarda yasaklandığını loglamanın olmamasıdır. Bu veriler olmadan, trafiği neyin yediğini anlamak mümkün değildir: proxy havuzunun belirli bir sitede tükenip tükenmediği veya sorunun ayrıştırıcının kendisinde (çok sık istekler, gecikme eksikliği, şüpheli başlıklar) olup olmadığı.
Middleware'de loglanması gereken minimum metrik seti:
- Her proxy için ayrıştırma oturumu boyunca yapılan istek sayısı
- Her proxy için hata sayısı ve kodları (403, 407, 429, 5xx)
- Proxy'nin ilk yasaklanmaya kadar geçen ömrü
- Yasakların en sık meydana geldiği alanlar
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
Bu tür bir istatistik olmadan, middleware'i "optimize etme" girişimleri tahmine dayanır. Bununla kesin olarak görebilirsiniz: örneğin, belirli bir alan, ilk 10 istekte proxy havuzunun %80'ini yasaklıyorsa — sorun proxy'de değil, istek desenindedir (User-Agent döngüsü yok, çok yüksek frekans, istekler arasında gecikme yok).
Middleware'in tam çalışma örneği
Her şeyi settings.py'de bir araya getiriyoruz — middleware sırası kritik öneme sahiptir, çünkü hangi sırayla kontrollerin uygulanacağını belirler:
DOWNLOADER_MIDDLEWARES = {
"myproject.middlewares.ProxyAuthMiddleware": 350,
"myproject.middlewares.StickySessionMiddleware": 400,
"myproject.middlewares.SmartRetryMiddleware": 550,
"myproject.middlewares.ProxyStatsMiddleware": 900,
}
RETRY_ENABLED = False # standart tekrar denemeyi kapatıyoruz, kendi tekrar denememizi kullanıyoruz
DOWNLOAD_TIMEOUT = 15
CONCURRENT_REQUESTS_PER_DOMAIN = 8
RETRY_ENABLED = False'a dikkat edin — bu kritik öneme sahiptir, aksi takdirde Scrapy'nin yerleşik tekrar deneme mekanizması, proxy değişim mantığınızla çelişecektir ve istekler çoğaltılacak veya iki kez tekrar denenecektir. Ayrıca, CONCURRENT_REQUESTS_PER_DOMAIN'u sınırlamak da önemlidir — bir alan için çok yüksek eşzamanlılık, proxy döngüsü ile bile, anti-bot sistemleri için şüpheli görünür.
Scrapy için hangi proxy türünü seçmelisiniz
Middleware, sorunun yarısını çözer, diğer yarısı ise görev için doğru proxy havuzunu seçmektir. Aşağıda tipik ayrıştırma senaryolarına göre bir karşılaştırma bulunmaktadır.
| Proxy Türü | Ne zaman kullanılmalı | Artıları | Eksileri |
|---|---|---|---|
| Veri merkezi proxy'leri | Sert anti-bot koruması olmadan açık sayfaların toplu ayrıştırılması | Yüksek hız, düşük trafik maliyeti | Kolayca tespit edilir, sık sık kara listeye alınır |
| Rezidans proxy'leri | Pazar yerleri, JS koruması olan siteler, yetkilendirme | Gerçek IP'ler, düşük yasak oranı, yapışkan oturum desteği | DC'ye göre daha düşük hız |
| Mobil proxy'ler | Mobil versiyon siteleri ve sert anti-bot koruması olan API'lerin ayrıştırılması | Hedef sitelerden maksimum güven | Trafik maliyeti en yüksek |
Pratik bir kural: güçlü bir koruma olmadan statik sayfaların ayrıştırılması için ucuz veri merkezi proxy'leri, bu makaleden akıllı middleware ile bir arada kullanılabilir. Eğer site Cloudflare, PerimeterX, DataDome veya benzeri sistemler kullanıyorsa — veri merkezi IP'leri hemen yasaklanır ve burada zaman kaybetmeden rezidans havuzuna geçmek daha kârlıdır, middleware'i debug etmekten kurtulursunuz.
Ayrıştırıcıyı üretime almadan önce kontrol listesi
- Middleware, yasaklı proxy'leri soğuma süresinde hariç tutar, rastgele seçmez
- Tekrar deneme mantığı hata türlerini (403/407 vs 429 vs 5xx) farklı soğuma süreleriyle ayırır
- Oturum görevleri için sticky proxy bağlaması kullanılır, her istekte değiştirmez
- Proxy yetkilendirmesi, yalnızca URL üzerinden değil, Proxy-Authorization başlığıyla iletilir
- Yasakların istatistikleri, sorunların teşhisi için alanlar ve proxy'ler bazında tutulur
- Standart Scrapy RetryMiddleware devre dışı bırakılmıştır, böylece özel mantıkla çelişmez
- Eşzamanlılık makul değerlerle sınırlıdır, maksimuma ayarlanmamıştır
Sonuç
Scrapy'de proxy trafiği harcama sorunlarının çoğu, daha büyük bir IP havuzu satın almakla değil, middleware mantığını düzeltmekle çözülür: hata türlerini ayırmak, karmaşık senaryolar için yapışkan oturumlar, doğru yetkilendirme ve yasakların sürekli izlenmesi. Bu makaledeki kodu temel alarak alabilir ve belirli bir projeye uyarlayabilirsiniz — yapı, hem küçük ayrıştırıcılar hem de dağıtılmış Scrapy-Cluster kurulumları için çalışır.
Eğer middleware zaten doğru bir şekilde ayarlandıysa ve yasaklar yine de çok sık oluyorsa — muhtemelen sorun IP havuzunun kalitesindedir. Gelişmiş anti-bot koruması olan sitelerin ayrıştırılması için rezidans proxy'leri ile denemek — bunlar, aynı middleware mantığıyla karşılaştırıldığında, yasaklama oranını önemli ölçüde azaltır.