למה Requests פשוט לא יספיק פה

אם הגישה הראשונית שלכם היא requests.get(), אתם בדרך לכאב ראש. חלקים רבים באתר עיריית תל אביב, במיוחד אלה שקשורים להגשת טפסים או חיפוש במאגרי מידע (כמו ארכיון הנדסי או מכרזים), מוגנים על ידי טוקנים של CSRF ודורשים סשן פעיל. שליחת בקשה בודדת תניב לרוב דף כניסה או שגיאה גנרית. לדוגמה, ניסיון לגשת ישירות לתוצאות חיפוש מכרזים ידרוש שליחת POST עם ה-payload הנכון, אבל גם עם cookies ו-headers שנאספו מהדף הקודם. זה הופך את התהליך למכונת מצבים (state machine) שצריך לנהל בזהירות.

הפתרון הוא לא לוותר, אלא לעבור לכלי שיודע לנהל את ההקשר הזה. Playwright הוא הבחירה שלי לפרויקטים כאלה. הוא מנהל סשנים, cookies ואינטראקציות JavaScript באופן אוטומטי, מה שמאפשר לנו להתמקד בלוגיקת החילוץ במקום בבנייה מחדש של כל מחזור בקשה-תגובה. בנינו scraper שעוקב אחר סטטוס היתרי בנייה, והוא נדרש לעבור 3-4 שלבים של טפסים ואימותים רק כדי להגיע לדף התוצאות. עם requests, היינו צריכים למפות את כל ה-flow הזה ידנית, כולל טיפול ב-redirects ובעוגיות. עם Playwright, כתבנו סקריפט שמחקה את פעולות המשתמש והגענו לנתונים ב-80% פחות קוד. זה לא עניין של מהירות ביצוע, אלא של זמן פיתוח ותחזוקה.

איסוף קטלוג ומודיעין: מעבר לטקסט פשוט

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

האתגר המרכזי הוא שהמידע לא תמיד מוגש כ-HTML נקי. נתקלתי במקרים רבים בהם המפרט המלא של מכרז נמצא אך ורק בקובץ PDF מקושר. זה אומר שה-scraper שלכם צריך לזהות את הקישורים האלה, להוריד את הקבצים, ולהשתמש בספרייה כמו PyMuPDF או pdfplumber כדי לחלץ את הטקסט. תהליך כזה דורש יותר משאבים ומוסיף עוד נקודת כשל אפשרית. בנינו פייפליין שסורק כ-500 מכרזים והיתרים חדשים בשבוע, ומתוכם כ-30% דורשים חילוץ מ-PDF. הצלחנו להגיע לדיוק של 95% בחילוץ טקסטואלי, אבל זה דרש לוגיקה ייעודית לטיפול בטבלאות ופורמטים שונים בתוך המסמכים. המטרה הסופית היא להפוך את המידע הלא-מובנה הזה לבסיס נתונים שאפשר להריץ עליו שאילתות, למשל, למצוא את כל המכרזים שכוללים את המילה "גינון" ופורסמו בחודש האחרון.

מעקב זמינות והתראות בזמן אמת

אחד ה-use cases המעניינים ביותר הוא מעקב מלאי/זמינות עיריית תל אביב. כאן, "מלאי" יכול להיות תורים פנויים לקבלת קהל במשרד הרישוי, מקומות פנויים בחוג קהילתי, או סטטוס של בקשה להיתר. לדוגמה, מערכת אוטומטית יכולה לבדוק כל 15 דקות אם התפנה תור למשרד מסוים ולשלוח התראה למשתמש. זה דורש scraping בקצב גבוה יחסית, אבל על מספר קטן של דפים.

הארכיטקטורה פה שונה מ-batch scraping קלאסי. במקום סריקה לילית של כל האתר, אנחנו מפעילים workers קטנים וממוקדים שרצים בתדירות גבוהה. האתגר הוא לא להפעיל את מנגנוני ההגנה של האתר. גם אם אין Cloudflare או Akamai, סביר שיש WAF בסיסי שמזהה בקשות חוזרות מאותו IP. הפתרון הוא להשתמש ב-proxy rotation, אבל לא חייבים ללכת על פתרונות יקרים. לפעמים מספיק מאגר קטן של 5-10 כתובות IP איכותיות שמתחלפות ביניהן. אם אתם לא בטוחים איך להתחיל, כדאי לקרוא איך לבחור פרוקסי residential כדי להבין את האפשרויות. הנקודה החשובה היא לשמור על קצב בקשות נמוך פר IP, למשל לא יותר מ-5 בקשות בדקה, כדי להיראות כמו תנועה אנושית לגיטימית. המטרה היא לא להעמיס על שרתי העירייה, אלא לקבל את המידע הדרוש ביעילות ובלי להרים דגלים אדומים.

מתי הגישה הזו נכשלת: כשמבנה האתר משתנה

הנה תרחיש שקרה לי יותר מפעם אחת עם אתרים ממשלתיים: הכל עובד חלק במשך חודשים, עם אחוזי הצלחה של 99.8%, ופתאום, בוקר אחד, הכל קורס. 100% שגיאות. הסיבה היא לא חסימה חדשה, אלא שינוי מבני באתר. עיריית תל אביב, כמו כל גוף גדול, עוברת מדי פעם שדרוגים או מעבר למערכת חדשה. לפעמים זה שינוי קטן, כמו החלפת id של כפתור מ-#submit-btn ל-#main-submit. במקרים אחרים, זה יכול להיות מעבר של מחלקה שלמה לתת-דומיין חדש עם לוגיקה שונה לגמרי.

זו נקודת התורפה הגדולה ביותר של scrapers שמבוססים על סלקטורים סטטיים (CSS או XPath). אם הסלקטור div.results-container > ul > li מפסיק להתקיים, ה-scraper מפסיק לעבוד. אין דרך למנוע את זה לחלוטין, אבל אפשר לבנות מערכות חסינות יותר. במקום להסתמך על סלקטורים שבירים, אנחנו מנסים למצוא "עוגנים" יציבים יותר ב-DOM, כמו טקסט ייחודי או data-testid אם קיים. בנוסף, כל פרויקט scraping רציני חייב לכלול מערכת ניטור ובדיקות. אנחנו מריצים בדיקות אינטגרציה קטנות כל שעה שמוודאות שהסלקטורים המרכזיים עדיין קיימים ומחזירים את מבנה הנתונים הצפוי. אם בדיקה נכשלת, אנחנו מקבלים התראה מיידית, הרבה לפני שהלקוח שם לב שהנתונים הפסיקו לזרום. זה הופך את התחזוקה מתגובה לאסון לתהליך פרואקטיבי. אם אתם נתקלים בחסימות אקטיביות יותר, שווה לקרוא על המדריך לעקיפת Cloudflare, למרות שכאן הבעיה היא לרוב מבנית.

השלב הסופי: בניית API וייצוא נתונים מובנים

המטרה הסופית של כל פרויקט scraping עיריית תל אביב היא לא רק לאסוף HTML, אלא לייצר API / קובץ נתונים נקי ושימושי. אחרי שהצלחתם לחלץ את המידע הגולמי, העבודה האמיתית מתחילה: ניקוי, נרמול, והעשרה של הנתונים. לדוגמה, חילוץ שמות מוצרים/מודעות (במקרה הזה, שמות של מכרזים או אירועים) הוא רק הצעד הראשון. השלב הבא הוא לנרמל תאריכים לפורמט ISO 8601, להפוך טקסט של כתובת לקואורדינטות גיאוגרפיות באמצעות שירות geocoding, ולקטלג כל פריט לפי קטגוריה אחידה.

התוצר הסופי יכול להיות קובץ CSV שמתעדכן מדי יום ומועלה ל-S3, או API פנימי שמאפשר למערכות אחרות לצרוך את המידע. בנינו מערכת כזו עבור לקוח שעקב אחרי נכסים עירוניים למכירה. ה-scraper רץ כל לילה, מעדכן מסד נתונים של PostgreSQL, ומאחוריו בנינו API פשוט מבוסס FastAPI שחשף את הנתונים המעובדים. זה איפשר לצוות האנליסטים שלהם לשלב את המידע ישירות במודלים שלהם בלי להתעסק עם קבצים או עם המורכבות של ה-scraping עצמו. אם אתם מתמודדים עם נפחים גדולים וצריכים שהכל ירוץ מהר, ודאו שאתם משתמשים ב-async IO. קראו על זה ב-מדריך Playwright stealth, שם יש התייחסות גם לאופטימיזציות ביצועים. ההשקעה בבניית data pipeline איכותי היא מה שמבדיל בין פרויקט חד-פעמי למקור מידע אמין וארוך טווח.