איפה רוב ה-pipelines של scraping נשברים
בואו נהיה כנים. רוב ה-pipelines של scraping שבונים בהתחלה הם פשוט סקריפט אחד גדול. בין אם זה Scrapy, Playwright, או סקריפט Python מותאם אישית, התבנית זהה: התחל, תעשה scraping, תעבד את הנתונים, תכתוב לדאטהבייס, סיים. נשמע הגיוני, נכון? אבל זה מתכון לאסון.
ראיתי את זה קורה עשרות פעמים. אתה מריץ scraper על אתר e-commerce גדול. הוא רץ 6 שעות, אוסף מידע על 500,000 מוצרים. הכל נראה תקין. ואז, בשלב הכתיבה ל-PostgreSQL, קורה משהו. connection נקטע, שדה לא צפוי גורם ל-exception, הדיסק מתמלא. בום. כל העבודה של 6 השעות האחרונות ירדה לטמיון. אין לך דאטה, שרפת פרוקסיז, וצריך להתחיל הכל מההתחלה.
הבעיה היא הצימוד (coupling) ההדוק. תהליך ה-scraping, תהליך הניקוי, והכתיבה לדאטהבייס הם כולם חלק מאותו תהליך ארוך ושברירי. כל נקודת כשל מפילה את כל הדומינו. זה פשוט לא עובד בסקייל, ובטח שלא ב-2025.
השינוי התפיסתי: חשיבה באירועים, לא באצווֹת (Batches)
הפתרון הוא להפריד את הרכיבים האלה. במקום תהליך אחד מונוליטי, אנחנו בונים מערכת מבוזרת שמתקשרת באמצעות אירועים. מה זה אירוע בהקשר שלנו? זה יכול להיות כל דבר:
- "דף מוצר X נסרק בהצלחה."
- "מחיר של מוצר Y השתנה."
- "קטגוריה Z הוסיפה 10 מוצרים חדשים."
- "נתקלתי ב-CAPTCHA בדף W."
כל אירוע כזה הוא יחידת מידע קטנה, עצמאית ובלתי ניתנת לשינוי (immutable). תפקיד ה-scraper משתנה באופן דרמטי: במקום להיות אחראי על כל התהליך, הוא אחראי רק על דבר אחד — לייצר את האירועים האלה ולפרסם אותם למערכת מרכזית. מרגע שהאירוע פורסם, העבודה של ה-scraper הסתיימה. הוא יכול להמשיך לדף הבא.
האנטומיה של Pipeline מבוסס-אירועים
ארכיטקטורה כזו מורכבת בדרך כלל משלושה חלקים עיקריים:
- Producer (היצרן): זה ה-scraper שלנו. הוא מבקר בדפי אינטרנט, מפיק את הנתונים הגולמיים (למשל, ה-HTML המלא או JSON מה-API), ואורז אותם כהודעה (אירוע). לאחר מכן הוא שולח את ההודעה הזו ל-Broker.
- Broker (המתווך): זו מערכת תורים עמידה וסקיילבילית. הכלי המוביל בתחום הזה הוא ללא ספק Apache Kafka. קפקא מקבל את ההודעות מה-Producer ומאחסן אותן בבטחה בתוך נושאים (topics). הוא מבטיח שההודעות לא יאבדו, גם אם המערכות בצד השני נופלות.
- Consumers (הצרכנים): אלו תהליכים נפרדים לחלוטין שמאזינים לנושאים הרלוונטיים בקפקא. כל consumer יכול לבצע פעולה שונה על אותו אירוע, במקביל ובאופן בלתי תלוי.
היופי פה הוא ה-decoupling. ה-scraper לא יודע, ולא אכפת לו, מה קורה עם הנתונים אחרי שהוא מפרסם אותם. הוא לא צריך לחכות לכתיבה לדאטהבייס. זה מאפשר לו לעבוד בקצבים הרבה יותר גבוהים ומונע ממנו להיות צוואר בקבוק. בנינו מערכות שמטפלות ביותר מ-10,000 אירועי scraping בשנייה באמצעות הגישה הזו.
קפקא כמערכת העצבים המרכזית
למה דווקא קפקא? כי הוא נבנה בדיוק לזה. הוא לא סתם מערכת תורים; הוא פלטפורמת streaming. הוא מאחסן את האירועים בצורה עמידה (persistency) כך שניתן "להריץ אחורה" את ההיסטוריה ולעבד מחדש נתונים אם צריך. זה משנה את כללי המשחק.
הנה דוגמה פשוטה של scraper בפייתון (Producer) ששולח את ה-HTML הגולמי לקפקא:
from kafka import KafkaProducer
import json
import requests
producer = KafkaProducer(
bootstrap_servers=['kafka-broker:9092'],
value_serializer=lambda v: json.dumps(v).encode('utf-8')
)
def scrape_product(url):
try:
response = requests.get(url, timeout=10)
response.raise_for_status() # Raise an exception for bad status codes
event = {
'url': url,
'raw_html': response.text,
'status_code': response.status_code,
'timestamp': time.time()
}
producer.send('raw_product_pages', value=event)
producer.flush()
print(f"Successfully produced event for {url}")
except requests.exceptions.RequestException as e:
print(f"Failed to scrape {url}: {e}")
# Usage
scrape_product('http://example-ecommerce.com/product/123')
שימו לב: ה-scraper לא מנתח את ה-HTML. הוא לא מנקה שדות. הוא פשוט שולח את הנתון הגולמי ביותר. זה הופך אותו לפשוט, מהיר ואמין. כל הלוגיקה המורכבת עוברת ל-consumers.
הכוח האמיתי: צבא של Consumers
מרגע שהאירוע נמצא ב-topic בשם `raw_product_pages`, אנחנו יכולים לחבר אליו כמה צרכנים שנרצה, בלי לשנות שורה אחת בקוד של ה-scraper:
- Consumer 1 (DB Writer): מאזין ל-topic, מנתח את ה-HTML עם BeautifulSoup או lxml, מוציא את שם המוצר, המחיר והמפרט, וכותב אותם לטבלת `products` ב-PostgreSQL.
- Consumer 2 (Elasticsearch Indexer): מאזין לאותו topic, מנתח את הטקסט, ומאנדקס אותו ב-Elasticsearch כדי לאפשר חיפוש טקסט מלא.
- Consumer 3 (Price Alert): מאזין, בודק אם המחיר נמוך מהמחיר הקודם (ששמור במאגר אחר), ואם כן — שולח התראה ב-Slack.
- Consumer 4 (Cache Warmer): מאזין, וכאשר מוצר מתעדכן, הוא מבצע purge לגרסה הישנה של הדף ב-CDN או ב-Varnish.
כל אחד מהם הוא מיקרו-שירות עצמאי. אם ה-DB Writer נופל, שאר המערכות ממשיכות לעבוד. קפקא ישמור את ההודעות, וכשה-DB Writer יחזור לפעולה, הוא ימשיך מאותה נקודה שבה עצר. איבדנו 0% מהנתונים. זה הבדל של שמיים וארץ מהסקריפט המונוליטי. זו ארכיטקטורת scraping מודרנית.
זה לא תמיד פשוט: סכמות ועיבוד חד-פעמי
הגישה הזו לא חפה מאתגרים. הבעיה הגדולה ביותר היא "חוזה הנתונים". מה קורה אם ה-scraper מחליט לשנות את מבנה האירוע? למשל, לשנות את שם השדה `raw_html` ל-`html_content`. פתאום כל ה-consumers יישברו כי הם מצפים למבנה הישן.
הפתרון המקובל הוא שימוש ב-Schema Registry (כמו זה של Confluent). זהו שירות מרכזי שמנהל את הסכמות של הנתונים בכל topic. לפני שה-Producer שולח הודעה, הוא מוודא שהיא תואמת לסכמה הרשומה. ה-Consumer, לפני שהוא קורא הודעה, מושך את הסכמה המתאימה ויודע איך לפענח אותה. זה מונע שברים ומאפשר אבולוציה מנוהלת של מבנה הנתונים.
אתגר נוסף הוא להבטיח עיבוד "בדיוק פעם אחת" (exactly-once). אנחנו לא רוצים שרשת תקלה תגרום ל-consumer לעבד את אותו אירוע פעמיים וליצור רשומות כפולות בדאטהבייס. לקפקא יש מנגנונים מתקדמים לתמוך בזה, אבל זה דורש תכנון זהיר בצד ה-consumer.
דפוסים מתקדמים: מעקב אחר שינויים (CDC)
ברגע שיש לך pipeline כזה, אפשר לעשות דברים מתוחכמים יותר. במקום לסרוק את כל האתר כל לילה, אפשר לסרוק רק את דפי הקטגוריות. כאשר ה-scraper מזהה מוצר חדש או שינוי במחיר, הוא לא מפרסם את כל דף המוצר, אלא אירוע "דלתא" קטן: `{"event_type": "price_changed", "product_id": 123, "new_price": 99.99}`.
הגישה הזו, המכונה Change Data Capture (CDC), היא יעילה לאין שיעור. היא מפחיתה דרמטית את העומס על אתר היעד, מקטינה את הסיכוי לחסימות ושגיאות 429, ומצמצמת את נפח הנתונים שעובר במערכת. זה הופך את ה-scraping לכמעט כירורגי. במקום פטיש 5 קילו, אנחנו משתמשים באזמל מנתחים.
בניית pipelines מבוססי-אירועים היא השקעה. היא דורשת יותר תכנון מאשר כתיבת סקריפט פשוט. אבל זו השקעה שמחזירה את עצמה פי עשרה בסקייל, באמינות, ובגמישות. זו הדרך היחידה לבנות מערכת איסוף נתונים רצינית שתשרוד את מבחן הזמן.
שאלות נפוצות
היתרון המרכזי של Kafka הוא היותו log עמיד ובלתי ניתן לשינוי, ולא רק תור. הודעות ב-Kafka נשמרות על הדיסק לתקופה מוגדרת (למשל, 7 ימים) ואינן נמחקות לאחר ש-consumer קורא אותן. זה מאפשר "Replay" - עיבוד מחדש של כל הנתונים ההיסטוריים על ידי consumer חדש, מה שבלתי אפשרי ב-RabbitMQ. תכונה זו חיונית להתאוששות מתקלות, הוספת שירותים חדשים, וניתוח נתונים בדיעבד.
הכלי הטוב ביותר לטיפול בסכמות משתנות הוא Schema Registry, כמו זה של Confluent. הוא משמש כמקור אמת מרכזי לסכמות הנתונים (בפורמט Avro, Protobuf או JSON Schema). כל הודעה שנשלחת על ידי ה-producer מקושרת לגרסת סכמה ספציפית. ה-consumer משתמש בגרסה זו כדי לפענח את ההודעה. זה מאפשר אבולוציה מבוקרת של הסכמה, תומך בתאימות לאחור, ומונע מה-pipeline להישבר כאשר מבנה הנתונים משתנה.
עבור פרויקט scraping חד-פעמי וקטן מאוד, ארכיטקטורה כזו היא כנראה overkill. אבל, אם הפרויקט צפוי לרוץ לאורך זמן, לגדול בהיקפו, או אם הנתונים צריכים להזין יותר ממערכת אחת (למשל, גם דאטהבייס וגם מערכת התראות), המעבר למודל אירועים מצדיק את עצמו מהר מאוד. הגמישות והאמינות שהוא מספק חוסכות שעות רבות של תחזוקה וטיפול בתקלות בהמשך הדרך.
דפוס ה-Outbox פותר את בעיית הכתיבה הכפולה האטומית לדאטהבייס ול-Kafka. במקום שה-scraper יכתוב קודם לדאטהבייס ואז ישלח הודעה לקפקא (פעולה שעלולה להיכשל באמצע), הוא כותב את הנתונים ואת האירוע המיועד לקפקא לשולחן "outbox" מיוחד באותו דאטהבייס, בתוך טרנזקציה אחת. תהליך נפרד (כמו Debezium) עוקב אחר טבלת ה-outbox ומפרסם את האירועים לקפקא באופן אמין. זה מבטיח שאף אירוע לא יפורסם אם הכתיבה לדאטהבייס נכשלה, ולהיפך.
ארכיטקטורה זו מפרידה את קצב ה-scraping מקצב העיבוד, מה שמאפשר ניהול חסימות מתוחכם יותר. אם ה-scraper נתקל בגל של שגיאות 429, הוא יכול להפסיק לשלוח בקשות ולהיכנס למצב "cool down" מבלי לעצור את כל ה-pipeline. ה-consumers ימשיכו לעבד את האירועים שכבר נמצאים בתור בקפקא. בנוסף, ניתן ליצור consumer ייעודי שמאזין לאירועי שגיאה (כמו `captcha_detected` או `rate_limit_hit`) ומפעיל לוגיקה אוטומטית, כמו החלפת <a href="/blog/residential-proxy-guide">Residential Proxies</a> או דיווח למערכת ניטור.
