דלג לתוכן הראשי
scraping.
חזרה לכל המאמרים

טיפול נכון בשגיאות 429 ו-503 ב-Web Scraping

8 במאי 20268 דק׳ קריאה
איור מופשט של שעון חול דיגיטלי עם חצים המראים המתנה וחזרה, המייצג טיפול בשגיאות 429 ו-503.

כולם חוטפים 429 ו-503. ההבדל הוא מה עושים עם זה.

בואו נשים את זה על השולחן. אם אתה עושה scraping, אתה תראה שגיאות 429 ו-503. זה לא עניין של "אם", אלא של "מתי" ו"כמה". ראיתי מספיק פרויקטים שנופלים בדיוק בנקודה הזאת. בונים scraper מדהים, הוא רץ יפה על 100 בקשות, ואז מתרסק לתוך קיר של rate limits כשהוא מנסה לעבור ל-100,000.

הטעות הראשונה היא להתייחס לשגיאות האלה כאל כישלון. הן לא. הן תקשורת. השרת בצד השני מדבר איתך. שגיאת 429 (Too Many Requests) אומרת משהו אחד, ושגיאת 503 (Service Unavailable) אומרת משהו אחר לגמרי. לערבב ביניהן זו הדרך המהירה ביותר לשרוף את ה-IP שלך ולהיחסם לצמיתות. המטרה שלנו היא להקשיב ולהגיב נכון, לא לדפוק את הראש בקיר חזק יותר.

מה זה באמת 429 (Too Many Requests)?

שגיאת 429 היא מסר ברור מהשרת: "חבר, אתה מהיר מדי בשבילי. תירגע". זו שגיאת צד-לקוח (קוד 4xx), כלומר, הבעיה היא אצלנו, לא אצל השרת. השרת עצמו בריא ומתפקד, הוא פשוט אוכף מדיניות הגבלת קצב (rate limiting) כדי להגן על עצמו מעומס יתר.

לפעמים, השרת אפילו נותן לנו מתנה: ה-header שנקרא Retry-After. הוא יכול להכיל מספר שניות לחכות, או תאריך ושעה מדויקים. אם קיבלת את ה-header הזה, תשתמש בו. זה הכי קרוב שתקבל להוראות הפעלה ישירות מה-API.

import requests
import time

url = "https://api.example.com/data"
response = requests.get(url)

if response.status_code == 429:
    print("Hit a rate limit!")
    retry_after = response.headers.get('Retry-After')
    if retry_after:
        # יכול להיות מספר שניות או תאריך בפורמט HTTP-date
        try:
            wait_seconds = int(retry_after)
            print(f"Server suggested waiting {wait_seconds} seconds.")
            time.sleep(wait_seconds)
        except ValueError:
            # לטפל בפורמט תאריך אם צריך
            print(f"Server provided a future retry date: {retry_after}")
    else:
        # אין הנחיה, נצטרך להשתמש באסטרטגיה משלנו
        print("No Retry-After header. Using our own backoff.")

התעלמות מ-429 היא כמו להתעלם מתמרור עצור. בפעם הראשונה אולי תצליח לעבור, אבל בסוף תמצא את עצמך בתאונה — במקרה שלנו, חסימת IP או חשבון. הטיפול הנכון הוא להאט, לא להחליף פרוקסי ולהמשיך באותו קצב. החלפת פרוקסי היא פתרון לעקיפת זיהוי, לא לעקיפת מדיניות. אם אתם רוצים להבין יותר על איך פרוקסיז משתלבים בתמונה, יש לנו מדריך מעולה על בחירת residential proxies.

ומה לגבי 503 (Service Unavailable)? זה סיפור אחר לגמרי

כאן התמונה מתהפכת. 503 היא שגיאת צד-שרת (קוד 5xx). המסר הוא: "הבעיה היא לא אצלך, היא אצלי". השרת כרגע לא יכול לטפל בבקשה שלך. זה יכול לקרות בגלל עומס יתר אמיתי (לא בגללך ספציפית), תחזוקה מתוכננת, או באג בצד שלהם.

להמשיך לשלוח בקשות בקצב גבוה לשרת שמחזיר 503 זה כמו לצעוק על מישהו שנחנק. זה לא עוזר, וזה רק מחמיר את המצב. אתה תורם לעומס שהפיל אותו מלכתחילה, והופך מלקוח לגיטימי לחלק מהתקפת DDoS קטנה. מערכות הגנה מודרניות מזהות התנהגות כזאת וחוסמות את ה-IP שלך לא מתוך מדיניות rate limiting, אלא מתוך הגנה עצמית.

תרחיש כשל קלאסי שראיתי יותר מדי פעמים

צוות בנה scraper לאתר מסחר גדול. בשעה 2 בלילה, האתר יצא לעדכון גרסה של 15 דקות והחל להחזיר 503. ה-scraper, שהוגדר עם לוגיקת retry אגרסיבית של "נסה שוב כל 2 שניות", המשיך להפציץ את השרת. מתוך 200 workers, כולם דפקו על הדלת הנעולה. מערכת ה-WAF (Web Application Firewall) של האתר זיהתה את ההתנהגות הזאת כזדונית, וחסמה את כל טווח ה-IP של הדאטה סנטר ממנו הם יצאו. בבוקר, לא רק שה-scraper לא עבד, אלא שאף שירות אחר של החברה לא יכול היה לגשת לאותו אתר. הכל בגלל טיפול שגוי ב-503.

אסטרטגיה חכמה ל-429: Exponential Backoff עם Jitter

אז איך מאטים נכון? הגישה הנאיבית היא פשוט לחכות זמן קבוע, נניח 5 שניות. זה עדיף מכלום, אבל זה לא מספיק טוב. אם השרת עדיין עמוס, תחזור על אותה טעות ותיחסם שוב. הפתרון המקצועי הוא Exponential Backoff.

הרעיון פשוט: מכפילים את זמן ההמתנה אחרי כל ניסיון כושל. לדוגמה: נכשלת? חכה 2 שניות. נכשלת שוב? חכה 4 שניות. אחר כך 8, 16, וכן הלאה. זה נותן לשרת "מרחב נשימה" שגדל ככל שהבעיה נמשכת.

אבל יש פה מלכודת. אם יש לך workers רבים שפועלים במקביל, הם עלולים להיכשל באותו זמן, לחכות את אותו זמן, ולנסות שוב בדיוק באותו הרגע. זה יוצר גל עומס מסונכרן שנקרא "Thundering Herd Problem". כדי למנוע את זה, מוסיפים Jitter — רכיב אקראי קטן לזמן ההמתנה. במקום לחכות 8 שניות, תחכה בין 8 ל-9 שניות.

import random
import time

MAX_RETRIES = 5
BASE_WAIT = 2  # שניות

def fetch_with_backoff(url):
    retries = 0
    while retries < MAX_RETRIES:
        response = requests.get(url)
        if response.status_code == 200:
            return response.json()
        
        if response.status_code == 429:
            wait_time = (BASE_WAIT ** retries) + random.uniform(0, 1)
            print(f"Status 429. Retrying in {wait_time:.2f} seconds...")
            time.sleep(wait_time)
            retries += 1
        else:
            # לטפל בשגיאות אחרות
            response.raise_for_status()

    print("Max retries reached. Failing.")
    return None

# דוגמת שימוש
data = fetch_with_backoff("https://api.example.com/data")

השילוב הזה הוא סטנדרט התעשייה. הוא מכבד את השרת, מונע סנכרון בין ה-workers שלך, ובסופו של דבר מגדיל את אחוזי ההצלחה של ה-scrape.

איך מטפלים ב-503 בלי להרוג את השרת

עם 503, החוקים שונים. Exponential backoff עדיין רעיון טוב, אבל צריך להיות הרבה פחות אגרסיבי. הבסיס צריך להיות ארוך יותר (למשל, להתחיל מ-30 שניות, לא 2), והמספר המקסימלי של ניסיונות חוזרים צריך להיות קטן יותר עבור בקשה בודדת. במקום לנסות שוב ושוב את אותה בקשה, עדיף להחזיר אותה לתור (queue) ולנסות אותה שוב מאוחר יותר, אולי אחרי כמה דקות.

האסטרטגיה הטובה ביותר היא ליישם "מפסק זרם" (Circuit Breaker) גלובלי. אם אתה מתחיל לראות הרבה שגיאות 503 מאותו דומיין, ה-scraper צריך לעצור את כל הבקשות לאותו דומיין למשך זמן קבוע (למשל, 5 דקות). אחרי שהזמן עבר, הוא יכול לשלוח בקשת "בדיקה" אחת. אם היא מצליחה, המפסק "נסגר" והעבודה ממשיכה. אם היא נכשלת שוב עם 503, המפסק נשאר "פתוח" לפרק זמן ארוך יותר. זה מונע ממך להציף שרת שכבר נמצא בבעיה.

תקציב ניסיונות וניהול תורים: התמונה הגדולה

במערכת scraping בסקייל, כל בקשה שנכשלת עולה זמן ומשאבים. אי אפשר לנסות לנצח. צריך להגדיר "תקציב ניסיונות" (Retry Budget). למשל, כל URL מקבל מקסימום 5 ניסיונות. אם הוא נכשל 5 פעמים (עם backoff כמובן), הוא מסומן ככושל ועובר לתור נפרד לבדיקה ידנית או לניסיון נוסף אחרי 24 שעות.

זה קריטי במיוחד כשעושים scraping לאתרים המוגנים על ידי מערכות כמו Cloudflare. ניסיונות חוזרים ונשנים יכולים להעלות את "רמת החשד" של המערכת כלפי ה-IP שלך, מה שיוביל ל-CAPTCHA או חסימה מוחלטת. ככל שהמערכת מורכבת יותר, כך ניהול הניסיונות החוזרים הופך לחלק מרכזי בארכיטקטורת ה-scraping הכוללת.

אז מה עושים מחר בבוקר?

אם אתה מריץ scraper עכשיו, לך תבדוק את הלוגים שלו. האם הוא מבדיל בין 429 ל-503? אם לא, זה התיקון הראשון שלך.

  1. הפרד את הלוגיקה: צור שני מסלולי טיפול שונים לחלוטין לשתי השגיאות האלה.
  2. יישם Exponential Backoff עם Jitter: לטיפול ב-429. תפסיק להשתמש ב-time.sleep(10) קבוע.
  3. היה עדין עם 503: הגדל משמעותית את זמני ההמתנה, שקול להשתמש במנגנון Circuit Breaker, ואל תנסה שוב את אותה בקשה יותר מ-2-3 פעמים ברצף.

השינויים האלה לא סקסיים כמו לעקוף CAPTCHA מורכב, אבל הם אלו שמבדילים בין scraper חובבני שמחזיק מעמד שבוע, לבין מערכת איסוף נתונים אמינה שמספקת ערך לאורך שנים.

שאלות נפוצות

ההבדל המעשי הוא במידת האגרסיביות של הניסיון החוזר. עבור שגיאת 429, השרת מבקש ממך להאט, ולכן אסטרטגיית exponential backoff עם המתנה שמתחילה מ-2-4 שניות היא הגיונית. לעומת זאת, שגיאת 503 מעידה שהשרת עצמו בבעיה, ולכן יש להגיב בפסיביות: להפסיק לשלוח בקשות לאותו דומיין למספר דקות (למשל, 5 דקות) ולחזור עם בקשת בדיקה בודדת לפני שממשיכים. הטיפול ב-503 הוא על הגנה על השרת, בעוד הטיפול ב-429 הוא על ציות למדיניות שלו.

החלפת IP היא פתרון חלקי בלבד, ולעיתים קרובות מטפלת בסימפטום ולא בבעיה. אם ה-rate limit הוא פר-IP, החלפת פרוקסי תעזור לך להמשיך. אבל אם ה-rate limit הוא פר-חשבון משתמש, פר-session או מבוסס טביעת אצבע מורכבת יותר (fingerprinting), החלפת IP לא תעזור ואף עלולה לסמן את החשבון שלך כחשוד. הגישה הנכונה היא קודם כל לכבד את ה-rate limit על ידי האטת הקצב, ורק אז להשתמש ב-proxy rotation כדי לפזר את העומס על פני כתובות IP רבות.

Jitter הוא הוספת רכיב אקראי קטן לזמן ההמתנה שחושב על ידי אלגוריתם ה-exponential backoff. הוא קריטי במערכות מקביליות שבהן יש לך עשרות או מאות workers. ללא Jitter, כל ה-workers שנתקלים ב-rate limit באותו זמן יחשבו את אותו זמן המתנה (למשל, 8 שניות) וינסו לשלוח בקשה שוב בדיוק באותו הרגע, מה שיוצר גל עומס חדש. הוספת Jitter, למשל `random.uniform(0, 1)`, שוברת את הסימטריה הזו ומפזרת את ניסיונות החידוש על פני חלון זמן קצר.

זיהוי rate limiting דינמי דורש ניטור וניתוח תגובות השרת לאורך זמן. סימן נפוץ הוא שאתה מצליח לבצע 1000 בקשות בדקה אחת, אבל אחרי 5 דקות של קצב כזה, אתה מתחיל לקבל שגיאות 429 גם בקצב של 100 בקשות בדקה. כדי להתמודד עם זה, ה-scraper שלך צריך להיות אדפטיבי. עליו לנטר את אחוז השגיאות, וכאשר הוא עולה מעל סף מסוים (למשל 5%), להוריד את קצב הבקשות הגלובלי ב-10% ולבדוק שוב. זה יוצר לולאת משוב שמנסה למצוא את הקצב האופטימלי בכל רגע נתון.

כן, בהחלט. ספריות HTTP מתקדמות בפייתון מגיעות עם תמיכה מובנית או ניתנת להרחבה לאסטרטגיות retry. לדוגמה, ספריית `requests` יכולה להשתלב עם `urllib3` כדי להגדיר Retry object מפורט שמטפל בקודים ספציפיים, backoff factor וסה"כ ניסיונות. ספריות כמו `tenacity` או `backoff` הן דקורטורים (decorators) שמאפשרים לך לעטוף כל פונקציה בלוגיקת ניסיונות חוזרים מורכבת בקלות. במסגרת Scrapy, ניתן להשתמש ב-middlewares כדי ליישם לוגיקה דומה באופן גלובלי לכל הבקשות בפרויקט.

אהבתם את הכתבה? הצטרפו לניוזלטר ה-AI.

סיכום שבועי של כל מה שחדש ב-AI, פרומפטים מעשיים וביקורות כלים — ישר למייל שלכם.

הירשמו עכשיו

עוד לקריאה