למה רוב מערכות ה-error handling נכשלות
כולנו היינו שם. שלוש לפנות בוקר, וההתראה מה-scraper הראשי קופצת. עוד פעם. אתה פותח את הלוגים ומקבל בליל של שגיאות 503, חסימות 403, ופה ושם כמה timeouts. הנטייה הראשונית היא לעטוף הכל ב-try/except ענק, להוסיף retry, ולקוות לטוב. זו טעות.
הגישה הזו מתייחסת לכל השגיאות כשקולות, אבל הן לא. שגיאת 503 (Service Unavailable) היא סיפור שונה לחלוטין מחסימת 403 (Forbidden). הראשונה אומרת "אני עמוס, תחזור עוד מעט", השנייה צועקת "אני יודע מי אתה ואתה לא רצוי פה". להגיב לשניהם עם אותו retry פשוט שורף לך את הפרוקסיז ואת הזמן. כדי לבנות scraper עמיד (resilient), אנחנו צריכים להפסיק לטפל בסימפטומים ולהתחיל לאבחן את המחלה.
שלושת סוגי השגיאות העיקריים (ומה כל אחד אומר)
אחרי שנים של דיבאגינג, הגעתי למסקנה שכמעט 99% מהשגיאות בסקרייפינג נופלות לאחת משלוש קטגוריות. הבנה של הקטגוריה היא הצעד הראשון לפתרון אמיתי.
1. חסימה אקטיבית (Bot Detection)
זוהי התנגדות ישירה מהאתר. הוא זיהה שהתעבורה שלך אינה אנושית והוא חוסם אותך באופן פעיל. זה יכול להתבטא בכמה צורות:
- HTTP Status Codes: בעיקר 403 Forbidden, אבל גם 401 Unauthorized או 405 Method Not Allowed במקרים מסוימים.
- CAPTCHA: קוד סטטוס 200 OK, אבל גוף התשובה מכיל דף CAPTCHA במקום התוכן שרצית.
- חסימת IP: בקשות פשוט נתקעות ב-timeout או נכשלות ברמת ה-TCP.
- דפים מזויפים: קוד 200 OK, אבל עם נתונים שגויים או חלקיים, במטרה "להרעיל" את הדאטה שלך.
שגיאות אלו הן האויב הכי קשה. הן דורשות שינוי בטקטיקה: החלפת IP, שיפור טביעת האצבע של הדפדפן, או שימוש בכלים מתקדמים יותר. למי שנתקל בזה הרבה, מומלץ לקרוא על שימוש ב-Playwright במצב stealth כדי להיראות אנושי יותר.
2. בעיות זמינות וקצב (Server/Network Issues)
כאן האתר לא בהכרח מנסה לחסום אותך, הוא פשוט לא עומד בקצב או שיש לו בעיות פנימיות. זה קורה הרבה באתרים קטנים או בזמני עומס.
- Rate Limiting (429 Too Many Requests): זו הדרך המנומסת של השרת להגיד לך להאט. בדרך כלל מגיע עם header בשם `Retry-After` שמציין כמה שניות לחכות.
- שגיאות שרת (5xx): קודי 500, 502, 503, 504. אלה מצביעים על תקלה בצד השרת. לרוב הן זמניות.
- Timeouts: הבקשה לקחה יותר מדי זמן. יכול להיות בגלל רשת איטית, שרת איטי, או פרוקסי גרוע.
התגובה הנכונה כאן היא סבלנות. לא להפציץ את השרת, אלא ליישם מנגנון חכם של exponential backoff. ניסיון חוזר מיידי רק יחמיר את המצב. טיפול נכון ב-rate limiting הוא קריטי, ויש טכניקות ספציפיות להתמודדות עם שגיאות 429.
3. שינוי מבני באתר (Structural Change)
זו השגיאה השקטה והמסוכנת ביותר. ה-scraper שלך מקבל קוד 200 OK, הוא לא נחסם, הכל נראה תקין. אבל הנתונים שהוא מחלץ הם `null`. ריקים. הסיבה? מפתח בצד הלקוח שינה class של `div` או העביר כפתור ממקום למקום. הסלקטור שלך (`#main-product-price`) כבר לא תופס כלום.
השגיאה הזו לא תפעיל שום התראת רשת. ה-scraper ימשיך לרוץ בשמחה וימלא לך את הדאטהבייס בזבל. הדרך היחידה לתפוס אותה היא באמצעות ולידציה של הנתונים עצמם.
מאבחון אוטומטי למפת פעולה
אוקיי, אז יש לנו שלוש קטגוריות. איך הופכים את זה לקוד שעובד? הרעיון הוא לבנות "ממיין שגיאות" (error classifier) שמסתכל לא רק על קוד הסטטוס, אלא גם על תוכן התגובה.
def classify_error(status_code, response_text):
if status_code in [403, 401, 405]:
return "BOT_DETECTION"
if "captcha" in response_text.lower() or "are you a robot" in response_text.lower():
return "BOT_DETECTION"
if status_code == 429:
return "RATE_LIMIT"
if status_code >= 500:
return "SERVER_ERROR"
# This part happens *after* parsing
# if parsed_data is empty or invalid:
# return "STRUCTURAL_CHANGE"
return "SUCCESS"
אחרי שיש לנו סיווג, אנחנו יכולים למפות אותו לפעולות:
- BOT_DETECTION: החלף IP וטביעת אצבע (User-Agent, headers). נסה שוב מקסימום 2-3 פעמים. אם נכשל, סמן את ה-target כ"חסום" ושלח התראה.
- RATE_LIMIT / SERVER_ERROR: יישם exponential backoff. התחל עם המתנה של 5 שניות, ואז 10, 25, וכן הלאה. נסה 5-7 פעמים על פני כמה דקות. לרוב זה נפתר מעצמו.
- STRUCTURAL_CHANGE: אל תנסה שוב! זה לא יעזור. השהה את הסקרייפינג עבור ה-target הספציפי הזה, פתח כרטיס ב-Jira או שלח הודעה ל-Slack עם ה-URL והסלקטור שנכשל, והמתן לתיקון ידני.
הסיפור על ה-scraper שרץ על 99% שגיאות במשך שבוע
אחת הטעויות הכי גדולות שעשיתי בתחילת הדרך הייתה לסמוך על HTTP status codes. בניתי scraper לאתר מסחר גדול, והוא רץ יפה. המוניטורינג הראה 99.8% הצלחה (כלומר, קוד 200). הרגשתי מלך.
שבוע לאחר מכן, איש הדאטה שאל אותי למה כל המחירים של מוצרים חדשים הם אפס. בדיקה מהירה גילתה שהאתר עשה A/B test קטן. לחלק מהמשתמשים (כולל ה-IPs שלנו), הם הציגו מבנה עמוד מעט שונה, שבו ה-selector של המחיר לא היה קיים. ה-scraper שלי, במקום לקרוס, פשוט החזיר `None`, שהפך ל-0 בדאטהבייס. במשך שבוע שלם "אספנו" דאטה חסר ערך, וכל מערכות הניטור היו ירוקות. זה היה שיעור כואב בחשיבות של ולידציית דאטה כחלק אינטגרלי מה-error handling.
תעדוף: מה באמת דחוף ומה יכול לחכות
לא כל שגיאה דורשת התערבות מיידית. אם יש לך 100,000 targets, ואחד מהם נכשל בגלל שינוי מבני, זה לא דחוף. אם 40% מהבקשות שלך מתחילות לקבל CAPTCHA, זה מצב חירום.
הנה מודל תעדוף פשוט:
- רמת P0 (חירום): עלייה חדה וגלובלית בשיעור שגיאות מסוג BOT_DETECTION. למשל, קפיצה מ-5% ל-50% בתוך שעה. זה אומר שהאתר שינה את מערכות ההגנה שלו וכל המערכת בסכנה. זה דורש התערבות מיידית.
- רמת P1 (גבוה): עלייה גלובלית בשגיאות שרת (5xx). יכול להצביע על בעיה בפרוקסיז שלך או שהאתר נפל. שווה בדיקה.
- רמת P2 (בינוני): כישלון עקבי של target ספציפי או קבוצת targets קטנה. לרוב מעיד על שינוי מבני נקודתי. פתח כרטיס ותקן כשיהיה זמן.
- רמת P3 (נמוך): שגיאות זמניות (timeouts, 503 בודדים) שנפתרות אוטומטית על ידי retries. אין צורך בפעולה, רק לוודא שהן לא הופכות למגמה.
מתי המערכת הזאת עדיין לא מספיקה?
הגישה הזו מכסה את רוב המקרים, אבל יש מצבי קצה. מה קורה כשהאתר מחזיר 200 OK עם הודעת "שגיאה" בתוך ה-HTML? או כשהוא מזהה אותך כבוט ומחזיר לך בכוונה נתונים ישנים או שגויים? אלה "שגיאות רכות" שקשה מאוד לתפוס עם לוגיקה פשוטה. כאן נכנסים לתחום של ולידציית סכמה (schema validation), זיהוי אנומליות סטטיסטיות בדאטה, ובניית ארכיטקטורת סקרייפינג מורכבת יותר שיודעת להתמודד עם אי-ודאות. אבל הבסיס תמיד נשאר זהה: להבין את *סוג* הכישלון לפני שמנסים לפתור אותו.
שאלות נפוצות
הדרך הטובה ביותר להתמודד עם חסימת IP היא להשתמש במאגר של פרוקסיז (proxy pool) ולבצע רוטציה ביניהם. מומלץ להשתמש ב-residential proxies שמדמים משתמשים אמיתיים. בנוסף, חשוב להאט את קצב הבקשות פר IP כדי לא לעורר חשד. אם בקשה נכשלת, המערכת צריכה אוטומטית להחליף IP ולנסות שוב. כלים כמו Scrapy או Playwright תומכים באינטגרציה עם שירותי פרוקסי כדי להפוך את התהליך לאוטומטי.
זיהוי שינוי מבני דורש ולידציה של הנתונים שחולצו, לא רק של תגובת הרשת. יש להגדיר סכמה (schema) ברורה לנתונים המצופים, למשל באמצעות Pydantic בפייתון. אחרי כל חילוץ, ודא שהשדות החשובים (כמו מחיר או כותרת) אינם ריקים או `null`. אם יותר מ-10% מהשדות החיוניים חסרים, יש לסמן את התוצאה כלא-תקינה ולהפעיל התראה למפתח, גם אם קוד הסטטוס היה 200.
Retry פשוט מנסה שוב את הבקשה במרווחי זמן קבועים, למשל כל 5 שניות. Exponential backoff, לעומת זאת, מגדיל את זמן ההמתנה באופן מעריכי אחרי כל כישלון – למשל, המתנה של 2 שניות, אחר כך 4, 8, 16 וכן הלאה. גישה זו יעילה בהרבה נגד שגיאות זמניות כמו 429 או 503, כי היא נותנת לשרת זמן להתאושש ומפחיתה את הסיכוי שתמשיך להעמיס עליו ותיחסם לחלוטין.
לא, שימוש ב-Headless Browser לכל משימה הוא יקר ולא יעיל. Playwright או Puppeteer צורכים משאבי CPU וזיכרון רבים (פי 10 ויותר) בהשוואה לספריית HTTP פשוטה כמו aiohttp או requests. יש להשתמש בהם רק כשחייבים: באתרים המבוססים על JavaScript כבד (כמו React/Angular) שמרנדרים תוכן בצד הלקוח, או כשהאתר מפעיל אמצעי הגנה מתוחכמים הדורשים טביעת אצבע מלאה של דפדפן כדי לעקוף אותם.
התעדוף צריך להתבסס על היקף ההשפעה של השגיאה. שגיאה שמשפיעה על אחוז גבוה מה-targets (למשל, 50% מהבקשות נכשלות עם 403) היא בעדיפות עליונה, כי היא מסכנת את כל הפרויקט. לעומת זאת, שגיאה שמשפיעה על target יחיד או קבוצה קטנה (למשל, סלקטור ספציפי שנשבר) היא בעדיפות נמוכה יותר. כדאי לבנות דשבורד ניטור שמציג את שיעור השגיאות לפי סוג והיקף כדי לקבל החלטות מבוססות נתונים.
