איפה רוב ה-scrapers נכשלים
בואו נשים את זה על השולחן. לבנות scraper שמוריד 50,000 מוצרים מאתר e-commerce זה פרויקט סופ"ש. אתה כותב כמה שורות ב-Scrapy, מריץ, ותוך כמה שעות יש לך CSV. כולם מרוצים. אבל כשלקוח מגיע ואומר "יש לי אתר עם 3 מיליון מוצרים, ואני צריך את הדאטה מעודכן ברמה יומית", רוב המערכות פשוט מתפרקות.
הגישה הנאיבית של "בוא נסרוק הכל מהתחלה כל יום" קורסת מכמה סיבות:
- זמן: סריקה מלאה של 3 מיליון דפים, אפילו בקצב אופטימי של 2 דפים בשנייה, לוקחת מעל 17 יום. זה לא "יומי".
- חסימות: רצף בקשות אגרסיבי כזה ידליק כל נורה אדומה. אתה תתחיל לראות שגיאות 429 ו-CAPTCHAs מהר מאוד. גם עם רשת פרוקסי טובה, זה קרב אבוד.
- משאבים: זה שורף רוחב פס, CPU וזיכרון בצד שלך ובצד של הלקוח. זה לא אלגנטי, וזה יקר.
- טריות מידע (Staleness): עד שתסיים לסרוק את המוצר ה-3 מיליון, המחיר של המוצר הראשון כבר לא רלוונטי. יצרת מאגר נתונים שהוא "נכון" נקודתית לרגע הסריקה, אבל אף פעם לא נכון כמכלול.
ראיתי את זה קורה יותר מדי פעמים. פרויקט מתחיל בהתלהבות, בונים crawler שמטפל בקטגוריה אחת, ואז מנסים להכיל אותו על כל האתר. אחרי שבוע של ריצה, הדאטהבייס מכיל 200,000 מוצרים, המערכת חוטפת חסימות IP, והלקוח שואל איפה שאר 2.8 מיליון המוצרים. זה הרגע שבו מבינים ש-scale הוא לא רק עניין של להריץ יותר workers. זו בעיה ארכיטקטונית.
ארכיטקטורת הבסיס: תור עדיפויות ו-Workers מבוזרים
במקום לחשוב על "סריקה מלאה", אנחנו צריכים לחשוב על מערכת חיה ונושמת שמגיבה לשינויים. הלב של מערכת כזו הוא תור משימות (Message Queue) חכם, כמו RabbitMQ או Redis Streams, שמנהל את כל ה-URLs שצריך לסרוק.
הטריק הוא שזה לא תור רגיל (FIFO). זהו תור עדיפויות (Priority Queue). כל משימה בתור מקבלת עדיפות, למשל מ-1 (הכי גבוה) עד 5 (הכי נמוך).
- עדיפות 1: מוצרים חדשים שגילינו הרגע. אנחנו רוצים את המידע עליהם ASAP.
- עדיפות 2: מוצרים שזוהו ככאלה שמשתנים בתדירות גבוהה (למשל, מבצעים יומיים).
- עדיפות 3: סריקה מחודשת של מוצרים שראינו לאחרונה (למשל, ב-24 שעות האחרונות) כדי לבדוק עדכונים.
- עדיפות 4: מוצרים ותיקים שלא השתנו הרבה זמן, לסריקת רענון שבועית.
- עדיפות 5: דפי קטגוריה, כדי לגלות מוצרים חדשים.
מול התור הזה עובד צי של workers. כל worker הוא תהליך עצמאי שלוקח משימה מהתור לפי העדיפות הגבוהה ביותר, מבצע את ה-scraping, ומדווח חזרה. זה מאפשר לנו לטפל קודם במה שחשוב, ולהבטיח שהמידע הקריטי ביותר תמיד יהיה טרי. זוהי דוגמה קלאסית של ארכיטקטורת web scraping מודרנית שנועדה להתמודד עם סקייל.
{
"url": "https://example.com/products/super-widget-pro",
"priority": 1,
"task_type": "product_discovery",
"metadata": {
"category": "electronics",
"discovered_at": "2025-10-26T10:00:00Z"
}
}
Crawling חכם: גילוי קטגוריות ומוצרים חדשים
איך מוצרים מגיעים לתור מלכתחילה? התשובה היא תהליך גילוי (Discovery) דו-שלבי.
שלב 1: סריקת קטגוריות
בתדירות נמוכה יחסית (למשל, פעם ביום), worker ייעודי סורק את דפי הקטגוריות והתת-קטגוריות. הוא לא נכנס לדפי המוצר עצמם. המטרה היחידה שלו היא לאסוף את כל ה-URLs של המוצרים המופיעים בדפים אלה. את רשימת ה-URLs הזו הוא משווה מול מאגר ה-URLs המוכרים לנו בדאטהבייס. כל URL שלא קיים אצלנו הוא מוצר חדש. ה-URLs החדשים האלה נזרקים לתור המשימות עם עדיפות 1.
שלב 2: Sitemap
במקביל, תהליך נוסף בודק את קובץ ה-`sitemap.xml` של האתר. אתרים גדולים מעדכנים את המפה הזו באופן קבוע, ולפעמים זו הדרך המהירה ביותר לגלות מוצרים חדשים או דפים שהוסרו. גם כאן, כל URL חדש שמתגלה נכנס לתור בעדיפות גבוהה.
השילוב של שתי השיטות מבטיח שאנחנו תמיד יודעים על מוצרים חדשים תוך שעות ספורות מהופעתם באתר, בלי צורך לסרוק את כל מיליוני המוצרים הקיימים כדי למצוא אותם.
לב הבעיה: Scraping אינקרמנטלי וזיהוי שינויים
אז גילינו את כל המוצרים, וסרקנו אותם פעם אחת. מה עכשיו? המטרה היא לא לסרוק שוב את כל המידע, אלא רק לזהות מה השתנה. כאן נכנס לתמונה ה-hashing.
עבור כל מוצר, אנחנו שומרים בדאטהבייס לא רק את המידע (מחיר, תיאור, זמינות), אלא גם "טביעת אצבע" (hash) של המידע הקריטי. בכל פעם ש-worker סורק דף מוצר, הוא מחשב טביעת אצבע חדשה על סמך המידע שמצא, ומשווה אותה לזו השמורה אצלנו.
איך בונים טביעת אצבע כזו? פשוט מאוד. משרשרים את שדות המפתח שמעניינים אותנו ומריצים עליהם פונקציית hash כמו MD5 או SHA256.
import hashlib
def generate_product_hash(price: str, stock_status: str, description: str) -> str:
"""Generates a hash based on key product fields."""
# Normalize and concatenate the critical data points
# The order is important!
data_string = f"{price.strip()}|{stock_status.strip()}|{description.strip()}"
# Create an MD5 hash of the UTF-8 encoded string
return hashlib.md5(data_string.encode('utf-8')).hexdigest()
# Example usage:
price = "99.90"
stock = "In Stock"
description = "A very cool widget."
product_hash = generate_product_hash(price, stock, description)
# Result: 'a8b12e45f...'
כשה-worker מבקר בדף:
- הוא מוריד את התוכן ומחלץ את המחיר, הזמינות והתיאור.
- הוא מחשב hash חדש עם המידע הזה.
- הוא משווה את ה-hash החדש ל-hash הישן ששמור בדאטהבייס.
אם ה-hashes זהים, סימן ששום דבר מהותי לא השתנה. אנחנו רק מעדכנים חותמת זמן `last_checked_at` וממשיכים הלאה. לא צריך לכתוב שום דבר לדאטהבייס הראשי. אם ה-hashes שונים, זהו! מצאנו שינוי. רק אז אנחנו שומרים את המידע המלא, מעדכנים את ה-hash החדש, ואולי שולחים התראה. גישה זו מפחיתה את עומס הכתיבה על הדאטהבייס ב-90-95% ומאפשרת לנו להתמקד רק בדלתא - בשינויים.
ניהול מצב ו-Freshness: איך לדעת מה לסרוק ומתי
עכשיו אפשר לחבר הכל יחד. יש לנו מאגר נתונים של מיליוני מוצרים, ולכל מוצר יש שדות ניהוליים:
- `product_url`
- `last_scraped_at` (מתי סרקנו את המידע המלא)
- `last_checked_at` (מתי בדקנו שינוי hash)
- `content_hash`
- `change_frequency_score` (כמה פעמים זיהינו שינוי)
תהליך מרכזי (Scheduler), שרץ פעם בכמה שעות, סורק את הדאטהבייס הזה ומחליט מה צריך להיכנס לתור הסריקה:
- מוצרים שמעולם לא נבדקו (`last_checked_at` is null)? הכנס לתור בעדיפות 2.
- מוצרים שה-`change_frequency_score` שלהם גבוה ולא נבדקו ב-12 השעות האחרונות? הכנס לתור בעדיפות 3.
- כל שאר המוצרים שלא נבדקו ב-48 השעות האחרונות? הכנס לתור בעדיפות 4.
המערכת הזו לומדת את עצמה. מוצרים שמשתנים הרבה ייבדקו בתדירות גבוהה יותר. מוצרים סטטיים ייבדקו לעתים רחוקות, מה שחוסך לנו משאבים יקרים. המטרה היא להגיע למצב ש-80% מהקטלוג נבדק לפחות פעם ב-24 שעות, וה-20% הדינמיים ביותר נבדקים כל 4-6 שעות.
אתגרים תפעוליים: Proxies, חסימות ו-Rate Limiting
גם עם הארכיטקטורה הכי חכמה בעולם, בסופו של דבר אנחנו שולחים בקשות HTTP לשרת שלא בהכרח רוצה שנסרוק אותו. בסקייל של מיליוני בקשות ביום, חסימות הן לא אפשרות, הן ודאות.
הגנה על ה-workers היא קריטית. זה אומר להשתמש ברשת פרוקסים איכותית. עבור אתרי e-commerce גדולים, לרוב אין ברירה אלא להשתמש בפתרון של residential proxies המאפשרים לבצע רוטציה בין מיליוני כתובות IP של משתמשים אמיתיים. זה מקשה מאוד על מערכות הגנה לזהות את תעבורת ה-scraping שלנו כתבנית חשודה.
בנוסף, ה-worker עצמו חייב להיות חכם. הוא צריך לדעת לזהות תגובות שמעידות על חסימה. כשמקבלים תגובת HTTP 429 (Too Many Requests), הדרך הנכונה היא לא לנסות שוב מיד. ה-worker צריך להחזיר את המשימה לתור (עם backoff אקספוננציאלי) ולנסות שוב מאוחר יותר, רצוי עם IP אחר. הבנה עמוקה של איך לטפל בשגיאות 429 ו-rate limiting היא מה שמבדיל בין מערכת שעובדת לסירוגין למערכת שעובדת 24/7.
ולבסוף, חשוב לזכור לכבד את האתר הנסרק. אל תפציצו אותו עם אלפי בקשות במקביל מ-IP בודד. פזרו את העומס, השתמשו ב-workers רבים שכל אחד מהם עובד בקצב מתון, והגדירו הגבלות קצב גלובליות ופר-דומיין. המטרה היא לקבל את הדאטה, לא להפיל את האתר.
שאלות נפוצות
הטיפול בוריאציות הוא קריטי. הגישה הטובה ביותר היא להתייחס לכל וריאציה כאל ישות נפרדת עם SKU ייחודי, אך לקשר אותן תחת מוצר-אב. במקום ליצור hash על המוצר כולו, יוצרים hash נפרד לכל וריאציה על בסיס המחיר, המלאי וה-SKU שלה. זה מאפשר לזהות שינוי במחיר של מידה L בכחול, בלי לסרוק מחדש את כל 15 הוריאציות האחרות. מבחינת גילוי, ה-scraper צריך לחפש את ה-JSON data של הוריאציות שמוטמע לרוב ב-JavaScript של הדף.
יעד ריאלי לקטלוג ענק הוא דיפרנציאלי. במקום לשאוף לטריות של שעה לכל הקטלוג, מגדירים רמות שירות (SLAs) שונות. למשל: 10% מהמוצרים הפופולריים ביותר (Top Movers) יתעדכנו כל שעה. 40% מהקטלוג יתעדכן כל 24 שעות. 50% הנותרים (מוצרי זנב ארוך) יתעדכנו פעם בשבוע. המפתח הוא לזהות את המוצרים הדינמיים והחשובים ביותר ולהשקיע בהם את רוב משאבי הסריקה, תוך שימוש במודל ה-hashing לזיהוי שינויים יעיל.
זיהוי הסרות הוא חלק חשוב מתחזוקת הקטלוג. ה-worker חייב לטפל בסטטוס קוד 404 או 410 כאירוע לכל דבר. כאשר מתקבל 404 עבור URL של מוצר קיים, אין למחוק אותו מיד. ייתכן שזו תקלה זמנית. במקום זאת, מסמנים את המוצר כ"חשוד להסרה" ומכניסים אותו לתור לבדיקה חוזרת בעוד מספר שעות. אם גם בבדיקה השנייה והשלישית (מ-IPs שונים) מתקבל 404, רק אז מסמנים את המוצר כלא פעיל (soft delete) במערכת.
עדיף להריץ הרבה workers קטנים וחסרי-מצב (stateless). ארכיטקטורה המבוססת על עשרות או מאות workers קטנים, למשל על קונטיינרים כמו Docker, היא גמישה ועמידה יותר. אם worker אחד נתקע, נחסם או קורס, ההשפעה על המערכת כולה מינורית. מנהל התור פשוט יקצה את המשימה שלו ל-worker פנוי אחר. גישה זו גם מאפשרת סקייל-אאוט קל: אם צריך להגביר את קצב הסריקה, פשוט מוסיפים עוד קונטיינרים. לעומת זאת, worker גדול ויחיד מהווה נקודת כשל בודדת (SPOF).
רינדור JavaScript הוא מכפיל עלות משמעותי. שימוש בכלים כמו Playwright או Puppeteer צורך הרבה יותר CPU וזיכרון מאשר בקשות HTTP פשוטות עם Scrapy. בסקייל של מיליוני מוצרים, יש להימנע מרינדור מלא בכל דף. הגישה הנכונה היא לבצע ניתוח מקדים (reverse engineering) של האתר, לזהות את קריאות ה-API הפנימיות (XHR) שהדפדפן מבצע כדי לטעון את נתוני המוצר, ולחקות את הקריאות האלה ישירות. זה מאפשר לקבל את המידע כ-JSON נקי, וחוסך 95% ממשאבי העיבוד.
