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

Scraping מבוסס תורים (Queue-based): המדריך המלא ל-Scale

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

למה ה-scraper שלך עומד להישבר (אם הוא עוד לא)

בוא נודה באמת. כל scraper מתחיל כסקריפט פשוט. לולאה שרצה על רשימת URL-ים, שולפת HTML, מנתחת אותו ושומרת לדאטהבייס. זה עובד. ליום אחד. אולי לשבוע. ואז משהו נשבר.

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

הבעיה המרכזית היא הצימוד ההדוק (tight coupling) בין גילוי המשימות (מציאת URL-ים חדשים) לביצוע שלהן (הורדת וניתוח העמוד). כשהכל קורה באותו תהליך, כל תקלה היא קטסטרופה. הפתרון הוא להפריד ביניהם. הפתרון הוא תורים.

העיקרון: הפרד ומשול עם Task Queues

תחשוב על זה כמו מסעדה. יש את המלצרים שמקבלים הזמנות מהלקוחות (מגלים עבודה חדשה) ויש את הטבחים במטבח שמכינים את המנות (מבצעים את העבודה). המלצר לא עומד מעל הטבח ומחכה שיסיים. הוא שם פתק הזמנה על הדלפק והולך לקבל עוד הזמנות. הדלפק הזה הוא ה-Queue שלנו.

ב-scraping, זה עובד בדיוק אותו דבר:

  • Producer (היצרן): תהליך שאחראי על גילוי URL-ים. הוא יכול לסרוק עמודי קטגוריות, לקרוא sitemap, או לקבל קלט ממקור אחר. הוא לא מגרד כלום. הוא רק דוחף את ה-URL-ים שמצא לתוך תור משימות.
  • Queue (התור): מאגר משימות מרכזי ועמיד (persistent). בדרך כלל זה שירות כמו Redis, RabbitMQ או AWS SQS. הוא מחזיק את כל ה-URL-ים שמחכים שיגרדו אותם.
  • Consumer / Worker (הצרכן): תהליך (או הרבה תהליכים) שכל תפקידו הוא למשוך משימה מהתור, לבצע את ה-scraping, ולשמור את התוצאה. אחרי שהוא מסיים, הוא מושך את המשימה הבאה.

היופי פה הוא שכל החלקים מנותקים. אפשר להריץ Producer אחד ו-50 Workers. אם Worker אחד קורס, שום דבר לא קורה. Worker אחר פשוט ייקח את המשימה שלו אחרי timeout. אם ה-Producer נופל, ה-Workers ימשיכו לעבד את מה שכבר קיים בתור. זוהי ארכיטקטורה עמידה לתקלות.

בחירת הטכנולוגיה הנכונה לתור

יש כמה שחקנים מרכזיים בשוק התורים, והבחירה ביניהם תלויה בסקייל ובמורכבות שאתה צריך.

מבוססי Redis: Celery ו-RQ

Redis הוא פתרון פופולרי כי הוא מהיר מאוד ונמצא בשימוש נרחב. שתי הספריות המובילות בפייתון הן Celery ו-RQ (Redis Queue).

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

# producer.py - מוסיף משימות לתור
from redis import Redis
from rq import Queue

q = Queue(connection=Redis())

urls_to_scrape = ["https://example.com/page/1", "https://example.com/page/2"]
for url in urls_to_scrape:
    q.enqueue('scraper_worker.scrape_page', url)
# scraper_worker.py - קוד ה-worker
import requests

def scrape_page(url):
    # כאן יתרחש ה-scraping האמיתי
    print(f"Scraping {url}...")
    response = requests.get(url)
    print(f"Finished {url} with status {response.status_code}")
    return len(response.text)

# כדי להריץ את ה-worker, מריצים מהטרמינל:
# > rq worker

Celery הוא חיה אחרת. הוא הרבה יותר חזק מ-RQ — תומך בניתוב משימות מורכב, backends שונים (לא רק Redis), ויש לו מערכת ניטור מובנית (Flower). אבל הוא גם הרבה יותר מורכב לקנפג. אם אתה בונה מערכת ארכיטקטורת scraping בסקייל תעשייתי, Celery הוא כנראה הבחירה הנכונה. לפרויקט צד, תישאר עם RQ.

שירותים מנוהלים: AWS SQS

אם אתה כבר רץ על ענן כמו AWS, שימוש ב-Simple Queue Service (SQS) הוא אופציה מצוינת. אתה לא צריך לנהל שרת Redis או RabbitMQ. אמזון דואגת לסקייל, לגיבויים, לזמינות. זה פשוט עובד. החיסרון הוא כמובן עלות ונעילה לספק הענן, אבל השקט הנפשי שווה את זה לעיתים קרובות. SQS מציע תכונות מתקדמות כמו תורי FIFO (First-In-First-Out) ו-Dead-Letter Queues מובנים בקלות.

דפוסים מתקדמים ל-Scraping מקצועי

ברגע שיש לך תור בסיסי, אתה יכול להתחיל ליישם דפוסים חכמים שפותרים בעיות scraping אמיתיות.

Priority Queues: מה חשוב יותר?

נניח שאתה מגרד אתר e-commerce. גירוד עמודי מוצר (שם המחיר והמלאי נמצאים) כנראה חשוב יותר מגירוד פוסטים בבלוג של החברה. עם תורים כמו Redis או RabbitMQ, אפשר להגדיר תורים עם עדיפויות שונות. ה-Workers שלך תמיד ימשכו משימות מהתור עם העדיפות הגבוהה (למשל `priority_high_products`) לפני שהם נוגעים בתור `priority_low_blog`.

Delayed Queues ו-Rate Limiting

נתקלת בשגיאת 429 Too Many Requests? הגישה הנאיבית היא לעשות `sleep(60)` ולנסות שוב. גישה טובה יותר היא להחזיר את המשימה לתור עם השהייה. במקום לחסום את ה-worker, הוא יכול לעבור למשימה אחרת מדומיין אחר. כך אתה מוריד לחץ מהשרת הממוקד וממקסם את התפוקה של ה-workers שלך. זו הדרך הנכונה להתמודד עם שגיאות 429 ו-rate limiting.

# דוגמה קונספטואלית עם RQ
from rq.job import Job

def scrape_with_retry(url):
    try:
        response = make_request(url)
        response.raise_for_status() # יזרוק שגיאה על 4xx/5xx
        process_data(response)
    except requests.exceptions.HTTPError as e:
        if e.response.status_code == 429:
            # קיבלנו Rate Limit. ננסה שוב בעוד 5 דקות.
            job = Job.fetch(Job.get_current_job().id, connection=redis_conn)
            q.enqueue_in(timedelta(minutes=5), job.func, *job.args, **job.kwargs)
            print(f"Re-queueing {url} for 5 minutes from now.")

Dead Letter Queues (DLQ): לאן הולכות משימות שבורות

תרחיש כשל אמיתי: היה לנו scraper שרץ על אתר חדשות. יום אחד, צוות הפיתוח של האתר שינה את מבנה ה-HTML של עמודי הכתבות. ה-CSS selector שלנו שהיה מוצא את כותרת המאמר הפסיק לעבוד. כל משימת גירוד לעמוד כתבה נכשלה. אחרי 5 ניסיונות חוזרים אוטומטיים, המשימה נזרקה. לאן? ל-Dead Letter Queue.

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

ניטור וסקייל: איך יודעים שהכל בסדר?

עם מערכת מבוזרת מגיעה האחריות לנטר אותה. שני המדדים החשובים ביותר הם:

  1. עומק התור (Queue Depth): כמה משימות מחכות בתור. זה המדד החשוב ביותר. אם הגרף של עומק התור עולה לאורך זמן, זה אומר שה-Producers שלך מייצרים עבודה מהר יותר משה-Workers שלך יכולים לצרוך אותה. אתה צריך להוסיף עוד workers.
  2. זמן משימה ממוצע (Average Task Latency): כמה זמן לוקח ל-worker לסיים משימה. אם הזמן הזה קופץ פתאום מ-2 שניות ל-20 שניות, כנראה שהאתר המטרה נהיה איטי, או שה-proxy שלך גורם לבעיות. כדאי לבדוק את ה-setup של ה-residential proxies שלך.

על בסיס עומק התור, קל להגדיר Worker Autoscaling. לדוגמה: "אם עומק התור גדול מ-10,000 במשך 5 דקות, הפעל עוד 2 מכונות worker". כשהעומק יורד מתחת ל-1,000, כבה אותן כדי לחסוך כסף.

נקודה נוספת למתקדמים היא At-Least-Once vs. Exactly-Once Delivery. רוב מערכות התורים מבטיחות At-Least-Once. כלומר, במקרה של כשל, ייתכן שמשימה תבוצע יותר מפעם אחת. ל-scraping זה בדרך כלל בסדר גמור (עדיף לגרד עמוד פעמיים מאשר לא לגרד אותו בכלל), אבל זה אומר ששכבת הדאטהבייס שלך צריכה להתמודד עם כפילויות (למשל, עם `INSERT ... ON CONFLICT UPDATE`).

מתי ארכיטקטורת תורים היא Overkill?

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

המקום השני שבו תורים פחות מתאימים הוא במערכות סינכרוניות בזמן אמת. אם אתה בונה API שמקבל URL ומחזיר את המידע המגורד מיד, המשתמש לא יכול לחכות שהמשימה תיכנס לתור ותעובד. במקרים כאלה, תהליך סינכרוני (עם timeout קצר) הוא הדרך הנכונה.

המורכבות הנוספת של ניהול Producers, Workers ומערכת תורים מגיעה עם עלות. תתחיל פשוט, אבל תמיד תחשוב צעד אחד קדימה איך תוכל לשלב תור כשהצורך יגיע. והוא יגיע.

מסקריפט שביר למכונה משומנת

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

שאלות נפוצות

ההבדל המרכזי הוא מורכבות מול יכולות. RQ (Redis Queue) הוא פשוט מאוד להקמה ומתאים ל-90% ממקרי ה-scraping, בהם אתה צריך תור בסיסי של משימות. Celery, לעומת זאת, היא מערכת מורכבת יותר אך חזקה בהרבה, התומכת בניתוב משימות מתקדם, תזמונים תקופתיים (cron), ומגוון רחב של backend-ים מעבר ל-Redis. אם אתה בונה מערכת תעשייתית עם דרישות ניטור וניתוב מורכבות, Celery היא הבחירה. לפרויקטים קטנים עד בינוניים, RQ יספק את כל מה שאתה צריך בפחות מאמץ.

הטיפול ב-state הוא אתגר מרכזי בארכיטקטורה מבוזרת. הדרך הטובה ביותר היא להפוך כל משימה לעצמאית ככל האפשר, כך שהיא לא תלויה בתוצאות של משימות קודמות. לדוגמה, במקום להעביר session cookie, כל משימה יכולה לבצע login מחדש. אם חייבים להעביר מידע, אפשר להעביר אותו כחלק מה-payload של המשימה הבאה. לדוגמה, משימה שמגרדת עמוד קטגוריה יכולה לייצר משימות חדשות לעמודי מוצר, ולהעביר להן את שם הקטגוריה כפרמטר.

רוב ספריות התורים, כמו Celery או RQ, מושכות משימה אחת בכל פעם כברירת מחדל, וזה בדרך כלל הגישה הנכונה ל-scraping. משיכת batch-ים גדולים (prefetch) יכולה לשפר ביצועים במערכות עם latency גבוה לתור, אך ב-scraping היא מסוכנת. אם worker מושך 10 משימות וקורס אחרי המשימה השנייה, 8 משימות ילכו לאיבוד עד שה-visibility timeout שלהן יפוג. לכן, prefetch-count של 1 (התנהגות ברירת המחדל) מבטיח מקסימום אמינות במקרה של קריסת worker.

תורי FIFO, כמו אלו ש-AWS SQS מציע, מבטיחים שהסדר שבו משימות נכנסות הוא הסדר שבו הן מעובדות. ברוב מקרי ה-scraping, זה לא הכרחי ואף יכול לפגוע בביצועים, כי אין חשיבות לשאלה אם תגרד את מוצר א' לפני מוצר ב'. המקרים בהם FIFO כן חשוב הם נדירים, למשל אם אתה מגרד רצף של עמודים עם פאגינציה וחייב לעבד אותם לפי הסדר, או בתהליכים מרובי-שלבים שבהם סדר הפעולות קריטי. לרוב, תור סטנדרטי (best-effort ordering) הוא מהיר וטוב יותר.

מניעת עיבוד כפול מתחילה במניעת הכנסה כפולה. לפני ש-producer דוחף URL לתור, הוא יכול לבדוק מול מאגר מרכזי (למשל, Redis Set או Bloom Filter) אם ה-URL כבר נראה בעבר. זהו פתרון יעיל ברמת ה-producer. בצד ה-consumer, מכיוון שרוב התורים מבטיחים 'at-least-once delivery', ייתכנו כפילויות. לכן, שכבת שמירת הנתונים (הדאטהבייס) צריכה להיות אידמפוטנטית. שימוש בפקודות כמו `INSERT ... ON CONFLICT UPDATE` ב-PostgreSQL מבטיח שגם אם תעבד את אותו עמוד פעמיים, הנתונים פשוט יתעדכנו ולא תיווצר שורה כפולה.

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

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

הירשמו עכשיו

עוד לקריאה