Scraping sırasında HTTP 429 Too Many Requests nasıl düzeltilir

http 429 too many requests - How to Fix HTTP 429 Too Many Requests While Scraping

HTTP 429 Too Many Requests yavaşla demek, git demek değil. Sunucu size trafiğinizi hâlâ istediğini ama daha düşük bir hızda istediğini söylüyor ve genellikle tam olarak ne kadar bekleyeceğinizi de söylüyor. Bu yüzden çözüm neredeyse hiçbir zaman bir proxy ya da yeni bir user agent değil. Bir başlığı okumak, doğru biçimde beklemek ve aynı anda kaç isteğin havada olduğunu sınırlamak. Bu yazı üçünü de ele alıyor, ardından gerçek bir hız sınırını aynı durum kodunu takınan bir bot engelinden nasıl ayırt edeceğinizi gösteriyor; çünkü ikisi tam tersi tepkiyi gerektiriyor.

Neye ihtiyacınız var

  • Örnekler için Python 3.10 veya daha yenisi ve requests kütüphanesi. Mantık, herhangi bir HTTP istemcisine doğrudan taşınır.
  • Yeniden deneme kodunu yazmadan önce yanıt başlıklarına bakabilmeniz için bir terminal.
  • CapSkip çalışır durumda, yalnızca son bölüm için gerekli: ya loopback adresinde yerel modda ya da worker'larınızın erişebileceği bir makinede sunucu modunda. İkisi de şu bölümde ele alınıyor: bağlantı ayarları. O yüzden başlamadan önce birini seçin.

1. Adım: yeniden denemeden önce yanıtı okuyun

429 işlemesinin çoğu körlemesine yazılıyor, bu yüzden de çalışmıyor. Önce gerçek yanıta bakın. Durum koduyla birlikte, sınırın ne olduğunu ve ne zaman sıfırlanacağını söyleyen başlıklar geliyor ve farklı servisler farklı başlıklar kullanıyor.

# No install needed. Dump headers, throw the body away.
curl -sS -o /dev/null -D - "https://example.com/api/items?page=2"

# Look for these, in this order of usefulness:
#   Retry-After: 30           seconds, or an HTTP date
#   RateLimit-Reset: 1724500000
#   X-RateLimit-Remaining: 0
#   RateLimit-Limit: 100

Önemli olan Retry-After. Tam olarak bu durum için tanımlanmıştır ve iki biçimde gelir: beklenecek saniye sayısı ya da mutlak bir HTTP tarihi. İkisi de geçerli, ikisi de sahada karşınıza çıkıyor ve sayıyı varsayan kod, tarih gönderen sitelerde kırılıyor. MDN her iki biçimi de belgeliyor, ayrıca şunun için hazırladığı başvuru sayfası da 429 durum kodu iki dakikanızı ayırmaya değer.

Onu savunmacı biçimde ayrıştırın, varsa uyun ve yoksa kendi programınıza geri dönün:

# pip install requests
from email.utils import parsedate_to_datetime
from datetime import datetime, timezone

def retry_delay(response, fallback):
    """Seconds to wait, from Retry-After if the server sent one."""
    raw = response.headers.get("Retry-After")
    if not raw:
        return fallback
    try:
        return max(0.0, float(raw))          # the delay-seconds form
    except ValueError:
        pass
    try:
        when = parsedate_to_datetime(raw)    # the HTTP-date form
        return max(0.0, (when - datetime.now(timezone.utc)).total_seconds())
    except (TypeError, ValueError):
        return fallback

Geleni bir üst sınıra bağlayın. 3600 saniye isteyen bir sunucu size bir saat durmanızı söylüyor ve bir istek işleyicisinin içinde bu kadar uslu uslu uyuyan bir worker, üstündeki her şeye takılmış gibi görünecek. Başlık değeri ile kendi üst sınırınızın küçüğünü alın, sonra işi rafa kaldırıp kaldırmayacağınıza ayrıca karar verin.

2. Adım: üstel biçimde ve rastgele sapmayla geri çekilin

Uyulacak bir Retry-After yoksa her denemede bekleme süresini ikiye katlayın ve rastgelelik ekleyin. İkiye katlama, zaten zorlanan bir servisi daha fazla dövmenizi engeller. Rastgelelik ise sınıra aynı saniyede çarpan yirmi worker'ınızın sonsuza kadar aynı saniyede yeniden denemesini engeller.

# pip install requests
import random, time, requests

def get_with_backoff(url, attempts=5, base=1.0, ceiling=60.0):
    for attempt in range(attempts):
        response = requests.get(url, timeout=30)
        if response.status_code != 429:
            return response
        # Full jitter: sleep somewhere in [0, base * 2 ** attempt].
        window = min(ceiling, base * (2 ** attempt))
        delay = retry_delay(response, random.uniform(0, window))
        time.sleep(min(delay, ceiling))
    raise RuntimeError(f"still rate limited after {attempts} attempts")

# Waits land near 0-1s, 0-2s, 0-4s, 0-8s, 0-16s unless the
# server named a delay, in which case that wins.

Tam sapma, yani pencerenin üstüne küçük bir titreme eklemek yerine sıfır ile pencere arasında rastgele bir değer seçmek, bir filoyu en hızlı biçimde eşzamanlılıktan çıkarır. Bunu kendiniz yazmak istemiyorsanız urllib3 bunu hazır sunuyor, ama işe yaraması için iki argümanın açıkça verilmesi gerekiyor:

# pip install requests
import requests
from requests.adapters import HTTPAdapter
from urllib3.util import Retry

retry = Retry(
    total=5,
    # status_forcelist defaults to none, so 429 is NOT retried
    # unless you list it here yourself. This is the usual bug.
    status_forcelist=[429, 500, 502, 503, 504],
    backoff_factor=1,        # 1 * 2 ** previous_retries seconds
    backoff_jitter=1.0,      # urllib3 2.x only
    allowed_methods=["GET", "HEAD"],
)

session = requests.Session()
session.mount("https://", HTTPAdapter(max_retries=retry))

Bu sınıf hakkında bilinmeye değer iki ayrıntı var. Retry-After’a kendisi zaten uyuyor, çünkü 429 onun RETRY_AFTER_STATUS_CODES kümesindeki üç durum kodundan biri, diğerleri 413 ve 503; respect_retry_after_header ise varsayılan olarak true. Ama status_forcelist varsayılan olarak boş, bu yüzden yeni bir Retry nesnesi, siz 429'u orada saymadıkça onu yeniden denemiyor. İnsanlar tersini varsayıp sonra adaptörün neden hiçbir şey yapmadığını merak ediyor.

3. Adım: daha ısrarlı denemek yerine eşzamanlılığı sınırlayın

Yeniden deneme mantığı belirtiyi tedavi ediyor. 429'lar dalgalar hâlinde değil düzenli geliyorsa, izin verilenden fazlasını istiyorsunuz demektir ve çözüm daha az göndermektir. Bir semafor ve istekler arasında en az bir boşluk, herhangi bir geri çekilme eğrisinden daha fazla hız sınırı sorununu çözer.

# Standard library only.
import asyncio

# Six in flight is a sane starting point for an unknown API.
gate = asyncio.Semaphore(6)
MIN_GAP = 0.2          # seconds between starts, per worker

async def fetch(client, url):
    async with gate:
        response = await client.get(url)
        await asyncio.sleep(MIN_GAP)
        return response

# Tune down on the first 429, and stay there for a while.
# Tuning back up too eagerly just rediscovers the limit.

429 sonrası ilk içgüdü, aynı yükü daha fazla IP'ye yaymaktır. Bazı hedeflerde işe yarıyor ama bu, kendi ödünleri olan ayrı bir karar; onu şu yazıda ayrıca ele aldık: CAPTCHA proxy rotasyonu. Bunu bir kapasite kararı olarak yapın, bir başlığı okumaktan kaçmanın yolu olarak değil.

429 bir 403 değildir, doğrulama da ikisinden biri değildir

429 işlemesinin en pahalıya patladığı yer burası. Hız sınırlama ile bot tespiti, zaman zaman aynı durum kodunu paylaşan farklı sistemlerdir ve sizden tam tersi şeyleri isterler. Bir hız sınırlayıcıdan gelen HTTP 429 Too Many Requests bir zamanlama talimatıdır, aynı kod bir anti-bot kenarından gelirse bir rettir. Bot engeline karşı nazikçe geri çekilmek bir saatinizi çöpe atar. Gerçek bir hız sınırında ısrarla denemek IP'nizi yasaklatır.

Elinize ne geçtiGenellikle ne anlama gelirGerçekten ne işe yarar
Retry-After başlığıyla gelen 429Gerçek ve belgelenmiş bir hız sınırıTam o kadar bekleyin, sonra hızınızı düşürün
Başlıksız ve HTML gövdeli 429API değil, bir kenar ya da anti-bot katmanıSınır değil engel olarak ele alın
Anında gelen 403Parmak izi, TLS ya da IP itibarıİstemciyi düzeltin, beklemek hiçbir şeyi değiştirmez
Retry-After ile gelen 503Aşırı yüklü ya da bakımda429 ile aynı geri çekilme yolu
Doğrulama sayfası taşıyan 200Puanlandınız ve araya girildiDoğrulamayı çözün ve devam edin

Son satır insanları şaşırtıyor, çünkü hiçbir şey başarısız olmadı. İstek 200 döndü ve gövdede verileriniz yerine bir doğrulama sayfası var; bu yüzden yalnızca durum koduna bakan bir yeniden deneme döngüsü orayı keyifle sonsuza kadar dövecek. Yalnızca durum satırına değil, gövdede beklediğiniz işarete bakın. Bulduğunuz şey bir doğrulamaysa cevap geri çekilmek değil, onu çözmektir.

Çözüm döngünüzü aynı tırmığa bastırmayın

Bir CAPTCHA yanıtını bekleyen yoklama döngüsü de bir yeniden deneme döngüsüdür ve el yapımı olanlar yukarıdaki hataların aynısını yapıyor. CapSkip'te bunu sayaçlı bir hizmete kıyasla kolaylaştıran iki şey var. Kendi donanımınızda çalışıyor, dolayısıyla tükenecek bir çözüm kotası ve takılacak kendi hız sınırı yok. Ve SDK'lar sizin için zaten geri çekiliyor: yoklama saniyenin çeyreğinde başlıyor ve sabit bir aralık değil bir üst sınır olan pollingInterval'e kadar büyüyor.

# pip install capskip
from capskip import CapSkip, NetworkException, TimeoutException

# Local mode. In Server mode, host is the solver box's address.
solver = CapSkip(host="127.0.0.1", port=8080, pollingInterval=2)

try:
    result = solver.recaptcha(
        sitekey="YOUR_SITEKEY",
        url="https://example.com/page-with-recaptcha",
    )
    print(result["code"][:24])   # token, submit it with the form
except TimeoutException:
    # Polling ran past recaptchaTimeout, 300 seconds by default.
    print("gave up waiting, try again or lower the timeout")
except NetworkException:
    # The solver is not reachable on that host and port.
    print("check CapSkip is running and the mode you set")

pollingInterval'i düşürmek yanıtın daha erken gelmesini sağlar ve size hiçbir şeye mal olmaz; ücretli bir API'de böyle bir seçeneğiniz esasen yok. SDK yerine ham HTTP uçlarını yokluyorsanız, her CAPTCHA türü için önerilen bekleme süreleri şurada listeli: API dokümantasyonu. Bir çözüm hâlâ sürerken alacağınız bekleme yanıtı da orada.

Çözücüyü onun yerine bir sunucuda çalıştırmak

Hız sınırlama genelde tek bir betiğin değil bir filonun sorunudur ve bir filo tek bir loopback adresini paylaşmaz. Bağlantı ayarları iki durumu da kapsıyor:

ModDinlediği adresNe zaman kullanılır
Yerel127.0.0.1, yalnızca o cihazScraper'ınız ve çözücü aynı makinede çalışıyor
SunucuAğ adresiniz veya genel IP'nizWorker'lar, konteynerler, bir VPS ya da barındırılan bir platform API üzerinden bağlanıyor

SDK host'unu çözücünün bulunduğu makineye yönlendirin, kodunuzda başka hiçbir şey değişmez; böylece on worker tek bir örneği paylaşabilir. Çağrılar kendi ağınızın dışından geliyorsa statik bir genel IP öneriliyor. Ayrıntılar şurada: bağlantı ayarları. Sunucu modu hâlâ sizin donanımınız ve hâlâ sayaçsız: çözücünün nerede çalıştığını değiştiriyor, kime ait olduğunu değiştirmiyor.

FAQ

Retry-After’a her zaman uymalı mıyım?

Uyun, ama bir üst sınırla. Pencerenin ne zaman yeniden açılacağına dair alacağınız en güvenilir sinyal odur; onu yok saymak, sunucunun size zaten söylediğinden daha kötü tahmin etmek demektir. Bir saatlik bir değer ise başka bir karar: bir worker'ı uyur halde tutmak yerine işi rafa kaldırıp sonra dönün, çünkü üstteki her şey bunu takılma olarak okuyacak.

Bir hız sınırını bot engelinden nasıl ayırt ederim?

Durum koduyla birlikte ne geldiğine bakın. Gerçek bir sınır makine tarafından okunabilir: bir Retry-After ya da RateLimit başlığı, küçük bir JSON gövdesi ve beklediğinizde tutarlı davranış. Bir bot engeli ise bir HTML sayfası gönderir, zamanlama bilgisi vermez ve ne kadar beklerseniz bekleyin sıklıkla aynı yanıtı verir. İkincisinin istediği daha uzun bir uyku değil, farklı bir istemci.

Çözücü hiç 429 döndürür mü?

Hayır. CapSkip sizin denetlediğiniz donanımda, çözüm başına kota olmadan çalışıyor; dolayısıyla tükenecek bir faturalama penceresi ve takılacak bir üst sınır yok. Bir çözüm çağrısı başarısız olursa, daemon erişilemez olduğu için bir ağ hatası ya da yoklama üst sınırını aştığı için bir zaman aşımı alırsınız. İkisi de yerel bir şeye işaret ediyor, o yüzden host'u, portu ve ayarladığınız modu kontrol edin.

Worker'larım barındırılan bir platformda çalışıyor. Çözücü nereye gidiyor?

Platformun erişebileceği, size ait bir makineye; çözücü de sunucu modunda olacak. Barındırılan runner'lar ve yönetilen otomasyon platformları sizin loopback adresinizi görmez, bu yüzden API'yi ağ adresinize ya da genel IP'nize bağlayın ve her worker'ı ona yönlendirin. Tek bir örnek tüm filoya hizmet eder ve tünele gerek kalmaz.

En kısa hâli

HTTP 429 Too Many Requests bir zamanlama sorunudur, ona da öyle davranın. Retry-After'ı okuyun ve kendi seçtiğiniz bir üst sınıra kadar uyun. Başlık yoksa tam sapmalı üstel geri çekilmeye dönün. Sonra eşzamanlılığınızı düşürün, çünkü düzenli gelen 429'lar hiçbir yeniden deneme eğrisinin çözemediği bir kapasite sorunudur. Ve yeniden denemeye kalkışmadan önce gövdeye bakın; bir doğrulama sayfası son derece sağlıklı bir durum koduyla geliyor ve beklemeyi değil çözülmeyi istiyor. Bu kısım için yerelde çalışan bir sınırsız captcha çözücü tam oturuyor; bir worker havuzuna nasıl yerleştiğini ise şu yazı web scraping için CAPTCHA çözücü başlığı altında anlatıyor.