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

מדריך שטח להתמודדות עם שגיאות רשת ב-Web Scraping

8 במאי 20268 דק׳ קריאה
איור מופשט של גלי רשת דיגיטליים בצבעי כחול וכתום המייצגים שגיאות וחיבורים

שגיאות רשת הן לא באג, הן פיצ'ר

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

הגישה הנכונה ל-web scraping בסקייל היא לא לנסות למנוע שגיאות רשת, אלא לצפות להן ולטפל בהן בחן. כל בקשה שיוצאת מהסקריפר שלך עוברת שרשרת ארוכה: המכונה שלך -> הרשת המקומית -> ספק הפרוקסי -> האינטרנט -> שרת היעד. תקלה יכולה להתרחש בכל אחת מהחוליות האלה. המשימה שלנו היא להבין איפה התקלה התרחשה, והאם כדאי לנסות שוב, ואם כן, איך.

האנטומיה של בקשת רשת כושלת: מי האשם?

לפני שכותבים לולאת `for` שמריצה `retry`, צריך להבין מה משמעות השגיאה שקיבלנו. כל שגיאה מספרת סיפור אחר על נקודת הכשל. רוב הבעיות מתנקזות לחמש קטגוריות עיקריות.

DNS Resolution Failure

הסיפור: המערכת שלך (או שרת הפרוקסי) ניסתה להמיר את שם הדומיין (למשל, `example.com`) לכתובת IP ונכשלה. זה כמו לנסות להתקשר למישהו בלי לדעת את מספר הטלפון שלו.

החשודים: לרוב זו בעיה זמנית בשרת ה-DNS של הפרוקסי או ברשת. לפעמים זו פשוט טעות הקלדה בדומיין. זו שגיאה ששווה לנסות שוב אחרי שנייה או שתיים, אולי עם פרוקסי אחר.

Connection Timeout

הסיפור: שלחנו בקשה ליצירת קשר (TCP handshake) לשרת היעד, אבל הוא לא ענה בזמן. הדלת נעולה ואף אחד לא פותח.

החשודים: שרת היעד עמוס מאוד, לא זמין, או שחומת אש (firewall) חוסמת את ה-IP הספציפי שלך. זו הסיבה שחובה להגדיר `connect timeout` נמוך יחסית, כמו 5 שניות. אין טעם לחכות נצח למישהו שלא מתכוון לענות.

Read Timeout

הסיפור: הצלחנו להתחבר לשרת, שלחנו את בקשת ה-HTTP שלנו, ועכשיו אנחנו מחכים לתשובה. והיא פשוט לא מגיעה.

החשודים: לרוב זה שרת היעד. אולי יש לו שאילתת SQL איטית, או שהוא מייצר דף מורכב. זה יכול להיות גם פרוקסי איטי שמהווה צוואר בקבוק. כאן נכנס לתמונה `read timeout` ארוך יותר, למשל 30 שניות. אנחנו רוצים לתת לשרת הזדמנות לעבוד, אבל לא לחכות לנצח.

SSL Handshake Error

הסיפור: ניסינו ליצור חיבור HTTPS מאובטח, אבל תהליך אימות התעודות (certificates) נכשל. זה כמו להראות תעודת זהות מזויפת.

החשודים: כמעט תמיד זה הפרוקסי. במיוחד פרוקסים זולים או לא אמינים שמנסים לעשות Man-in-the-Middle בצורה כושלת. כשאתה רואה רצף של שגיאות SSL מפרוקסי מסוים, הגיע הזמן להסיר אותו מהמאגר שלך. זו לא שגיאה שכדאי לנסות שוב עם אותו פרוקסי.

Connection Reset By Peer

הסיפור: החיבור נוצר בהצלחה, ואז, בפתאומיות, הצד השני סגר לנו את הקו בפנים. בלי הודעה מוקדמת, בלי סטטוס קוד HTTP.

החשודים: זהו הסימן המובהק ביותר למערכת הגנה אקטיבית כמו WAF (Web Application Firewall). המערכת זיהתה דפוס התנהגות חשוד (למשל, יותר מדי בקשות מהר מדי) והחליטה לנתק אותך. זה לא סתם עומס, זו חסימה מכוונת. אם אתה נתקל בזה, זה הזמן לבדוק את ה-fingerprint של הבקשות שלך ולשקול טכניקות מתקדמות יותר, כמו אלה שדרושות כדי לעקוף הגנות כמו Cloudflare.

Timeouts: החוק הראשון של Scraping יציב

רוב הספריות מגיעות עם הגדרות timeout גרועות, או ללא הגדרות כלל. להריץ סקריפר בלי timeouts מוגדרים זה כמו לנהוג בלי בלמים. במוקדם או במאוחר, תתקע על בקשה אחת שתוקעת את כל התהליך.

הנה כלל הזהב שלי, שעובד ב-95% מהמקרים:

  • Connect Timeout: 5 שניות.
  • Read Timeout: 30 שניות.

למה דווקא המספרים האלה? חיבור TCP אמור להיות כמעט מיידי. אם לוקח יותר מ-5 שניות רק כדי להגיד "שלום", משהו רקוב בדרך. זה יכול להיות פרוקסי מת, רשת עמוסה, או שרת יעד שלא מאזין. אין טעם לחכות. לעומת זאת, קריאת התוכן יכולה לקחת זמן. השרת אולי מריץ לוגיקה מורכבת בצד שלו. 30 שניות זה זמן סביר לתת לו לעבוד לפני שאנחנו מרימים ידיים.

בפייתון עם ספריית `requests`, זה נראה ככה:

import requests

try:
    response = requests.get(
        'https://example.com',
        timeout=(5, 30)  # (connect, read)
    )
except requests.exceptions.Timeout:
    print("Request timed out. Time to retry or rotate.")
except requests.exceptions.RequestException as e:
    print(f"An unexpected network error occurred: {e}")

הגדרת ה-tuple הזה ב-timeout היא כנראה השינוי הקטן עם ההשפעה הגדולה ביותר שאתה יכול לעשות כדי לייצב את הסקריפר שלך.

איך לבנות מנגנון Retries חכם (ולא לולאה טיפשה)

אוקיי, אז זיהינו שגיאה והחלטנו לנסות שוב. הנטייה הראשונית היא לעטוף הכל ב-`while True` עם `try-except`. זו טעות. retry אגרסיבי ומיידי רק יחמיר את המצב, יגרום לשרת היעד לחסום אותך מהר יותר, ויבזבז לך משאבים.

מנגנון retry חכם מבוסס על שני עקרונות: סוג השגיאה ו-Exponential Backoff.

  1. לא כל שגיאה נולדה שווה: שגיאות כמו `ConnectionTimeout` או `DNSFailure` הן לרוב זמניות. שווה לנסות שוב, אולי עם פרוקסי אחר. שגיאות כמו `SSLHandshakeError` מצביעות על בעיה יסודית בפרוקסי — לנסות שוב איתו זה בזבוז זמן. שגיאות HTTP כמו 404 (Not Found) או 403 (Forbidden) הן שגיאות קבועות מהשרת; לנסות שוב את אותה בקשה בדיוק לא ישנה את התוצאה.
  2. Exponential Backoff עם Jitter: אל תנסה שוב מיד. חכה שנייה. אם נכשלת שוב, חכה שתי שניות. אחר כך ארבע, שמונה, וכן הלאה, עד למגבלה מסוימת (למשל, 60 שניות). זה נותן למערכות בדרך (שרת היעד, הרשת) זמן להתאושש. הוספת "Jitter" (רכיב אקראי קטן לזמן ההמתנה) מונעת מצב שבו כל ה-workers שלך מנסים שוב באותו רגע בדיוק, מה שיוצר עומס מסונכרן.

הנה דוגמת קוד פשוטה שממחישה את העיקרון:

import time
import random

def fetch_with_smart_retry(url, max_retries=5):
    delay = 1.0  # initial delay in seconds
    for attempt in range(max_retries):
        try:
            print(f"Attempt {attempt + 1} for {url}")
            # This is where you'd make your actual request
            # For demonstration, we'll simulate failures
            if attempt < 3:
                raise ConnectionError("Simulated connection timeout")
            
            print("Success!")
            return "Page content"

        except (ConnectionError, TimeoutError) as e:
            print(f"Network error: {e}. Retrying in {delay:.2f} seconds...")
            time.sleep(delay + random.uniform(0, 0.5)) # delay with jitter
            delay *= 2 # exponential backoff
            if delay > 60:
                delay = 60 # cap the delay
    
    print("Failed after all retries.")
    return None

fetch_with_smart_retry("https://some-flaky-site.com")

הלוגיקה הזו הרבה יותר מתוחכמת מניסיון חוזר מיידי. היא נותנת למערכת לנשום ומגדילה דרמטית את סיכויי ההצלחה הכוללים. כשאתה בונה ארכיטקטורת scraping בסקייל, המנגנון הזה הוא לב המערכת.

תרחיש מהשטח: כשהפרוקסי בוגד בך

אני רוצה לשתף סיפור על פרויקט שבו גילינו בעיה חמקמקה. במשך ימים, כ-15% מהבקשות שלנו לאתר מסוים נכשלו עם `Connection Reset By Peer`. חשבנו מיד שהאתר חוסם אותנו. הגברנו את הרוטציה של ה-IPs, האטנו את הקצב, שינינו user-agents. שום דבר לא עזר. אחוז הכישלונות נשאר יציב.

התחלנו לבודד משתנים. הרצנו את אותן הבקשות דרך ספק פרוקסי אחר, ובמקביל, ישירות מהשרת שלנו (לצורך בדיקה קצרה). התוצאות היו מדהימות: דרך הספק השני ומהשרת שלנו, אחוז ההצלחה היה קרוב ל-100%. הבעיה לא הייתה באתר היעד, אלא בספק ה-residential proxies הראשון שלנו.

התברר שהרשת הפנימית שלהם הייתה לא יציבה. חלק מה-exit nodes שלהם היו מנתקים חיבורים תחת עומס קל. השגיאה `Connection Reset` נראתה כאילו היא מגיעה משרת היעד, אבל מקורה היה באמצע הדרך. הלקח: תמיד תטיל ספק בכל חוליה בשרשרת. לפני שאתה מאשים את שרת היעד, ודא שהכלים שלך — ובמיוחד הפרוקסים שלך — נקיים מאשמה.

מה רוב המפתחים מפספסים

הטעות הכי גדולה שאני רואה היא התייחסות לכל שגיאות הרשת כמקשה אחת. מפתחים כותבים בלוק `try-except` כללי שתופס `Exception` ופשוט מנסה שוב, או גרוע מזה, רושם ללוג וממשיך הלאה. זו גישה שמובילה לנתונים חסרים, חסימות IP מיותרות, ובזבוז כסף על פרוקסים ושירותי CAPTCHA.

סקריפר מקצועי לא רק מבקש דפים, הוא מנהל דיאלוג עם הרשת. הוא מבין את הניואנסים בין timeout ל-connection reset. הוא יודע מתי להאט, מתי להחליף IP, ומתי לוותר על בקשה ספציפית. האבחנה הזו בין סוגי השגיאות היא מה שמבדיל בין סקריפט חובבני למערכת איסוף נתונים אמינה שמסוגלת לרוץ חודשים ללא התערבות.

שאלות נפוצות

ההבדל הוא קריטי לאבחון הבעיה. Connection Timeout קורה בשלב יצירת החיבור הראשוני (TCP handshake) ומעיד בדרך כלל על בעיה ברשת, חומת אש, או שרת יעד שלא זמין כלל. Read Timeout מתרחש לאחר שהחיבור נוצר בהצלחה, אך השרת לא שלח תגובה בזמן. זה בדרך כלל מצביע על עומס בצד השרת או שאילתה איטית. לכן, מומלץ להגדיר connect timeout קצר (כ-5 שניות) ו-read timeout ארוך יותר (כ-30 שניות).

כדאי לוותר על בקשה כאשר השגיאה שקיבלת מעידה על בעיה קבועה ולא זמנית. שגיאות HTTP כמו 404 (Not Found) או 401 (Unauthorized) לא ישתנו בניסיון חוזר עם אותם פרמטרים. גם שגיאת SSL Handshake המגיעה מפרוקסי ספציפי היא סימן שכדאי להסיר את הפרוקסי מהמאגר ולא לנסות שוב דרכו. ניסיון חוזר במקרים אלה רק מבזבז זמן ומשאבים.

זו שאלת המפתח, והתשובה היא בידוד משתנים. אם אתה רואה אחוז גבוה של שגיאות (מעל 5-10%) מסוג מסוים, נסה לשלוח מספר בקשות לאותו יעד דרך פרוקסי אחר או ישירות מהרשת שלך (אם אפשרי). אם השגיאות נעלמות, הבעיה היא בספק הפרוקסי הראשון. בנוסף, שגיאות כמו SSL Handshake או DNS Failure הן כמעט תמיד באשמת הפרוקסי. ניטור שיעורי ההצלחה פר פרוקסי הוא קריטי.

זהו אלגוריתם חכם לניסיונות חוזרים. במקום לנסות שוב מיד, אתה מכפיל את זמן ההמתנה אחרי כל כישלון (1, 2, 4, 8 שניות) עד למגבלה מסוימת. זה נותן למערכת בצד השני זמן להתאושש מהעומס. ה-'Jitter' הוא הוספה של רכיב אקראי קטן לזמן ההמתנה. זה מונע מצב שבו כל תהליכי הסקריפר שלך, שנכשלו יחד, מנסים שוב בדיוק באותו רגע ויוצרים ספייק נוסף של עומס.

בהחלט, שימוש בספריה ייעודית כמו `tenacity` או `retry` הוא פתרון מצוין ועדיף על המצאת הגלגל מחדש. ספריות אלו מספקות דרך נקייה ודקורטיבית להגדיר לוגיקת ניסיונות חוזרים, כולל exponential backoff, jitter, מספר ניסיונות מירבי, ואף הגדרת תנאים לאילו שגיאות ספציפיות כדאי לנסות שוב. לדוגמה, עם `tenacity` אפשר בקלות להגדיר ניסיון חוזר רק עבור `ConnectionError` אבל לא עבור `HTTPError` עם קוד 404.

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

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

הירשמו עכשיו

עוד לקריאה