diff --git a/.gitignore b/.gitignore
index 9e64f72..5074d70 100644
--- a/.gitignore
+++ b/.gitignore
@@ -13,3 +13,6 @@ __pycache__/
!*/
!*.py
!.gitignor
+
+.venv/
+.env
\ No newline at end of file
diff --git a/.idea/food_scraper_project.iml b/.idea/food_scraper_project.iml
index d0876a7..64e2334 100644
--- a/.idea/food_scraper_project.iml
+++ b/.idea/food_scraper_project.iml
@@ -2,7 +2,7 @@
-
+
\ No newline at end of file
diff --git a/.idea/misc.xml b/.idea/misc.xml
index 2bdbc54..eb2ae55 100644
--- a/.idea/misc.xml
+++ b/.idea/misc.xml
@@ -1,4 +1,7 @@
-
+
+
+
+
\ No newline at end of file
diff --git a/core/base_store.py b/core/base_store.py
new file mode 100644
index 0000000..c56aec0
--- /dev/null
+++ b/core/base_store.py
@@ -0,0 +1,66 @@
+from selenium.webdriver.common.by import By
+from selenium.webdriver.support.ui import WebDriverWait
+from selenium.webdriver.support import expected_conditions as EC
+from selenium.common.exceptions import TimeoutException
+from core.online_store import OnlineStore
+
+class BaseStore(OnlineStore):
+
+ # ✅ פונקציית login - מרוכזת אחת בלבד
+ def login(self, username, password):
+ print(f"🌐 טוען דף: {self.get_login_url()}")
+ self.driver.get(self.get_login_url())
+
+ self.open_login_modal()
+
+ try:
+ print("⌛ ממתין לשדות התחברות...")
+ self.wait_for_element(self.get_username_selector())
+ self.fill_input(self.get_username_selector(), username)
+ self.fill_input(self.get_password_selector(), password)
+
+ self.wait_and_click(self.get_submit_selector())
+
+ # self.wait_until_disappear(self.get_username_selector())
+ self.wait_for_element("div#user-box")
+ print("✅ התחברות הצליחה")
+
+
+ except TimeoutException:
+ raise Exception("❌ שגיאה בהתחברות")
+
+ # ✅ פונקציית ברירת מחדל לפתיחת מודל התחברות
+ def open_login_modal(self):
+ pass
+
+ # ✅ פונקציות עזר כלליות לשימוש חוזר בכל תהליך (לא רק login)
+ def wait_for_element(self, selector, timeout=10):
+ return WebDriverWait(self.driver, timeout).until(
+ EC.presence_of_element_located((By.CSS_SELECTOR, selector))
+ )
+
+ def wait_and_click(self, selector, timeout=10):
+ WebDriverWait(self.driver, timeout).until(
+ EC.element_to_be_clickable((By.CSS_SELECTOR, selector))
+ ).click()
+
+ def fill_input(self, selector, value):
+ self.driver.find_element(By.CSS_SELECTOR, selector).send_keys(value)
+
+ def wait_until_disappear(self, selector, timeout=10):
+ WebDriverWait(self.driver, timeout).until_not(
+ EC.presence_of_element_located((By.CSS_SELECTOR, selector))
+ )
+
+ # ✅ פונקציות מופשטות שהמחלקות היורשות חייבות לממש
+ def get_login_url(self):
+ raise NotImplementedError
+
+ def get_username_selector(self):
+ raise NotImplementedError
+
+ def get_password_selector(self):
+ raise NotImplementedError
+
+ def get_submit_selector(self):
+ raise NotImplementedError
diff --git a/core/online_store.py b/core/online_store.py
new file mode 100644
index 0000000..93c2fb8
--- /dev/null
+++ b/core/online_store.py
@@ -0,0 +1,70 @@
+class OnlineStore:
+ def __init__(self, driver):
+ self.driver = driver
+
+ def login(self, username, password):
+ raise NotImplementedError
+
+ def search_item(self, item_name):
+ raise NotImplementedError
+
+ def add_to_cart(self, item_name):
+ raise NotImplementedError
+
+ def checkout(self):
+ raise NotImplementedError
+#
+# from abc import ABC, abstractmethod
+#
+# class OnlineStore(ABC):
+# def __init__(self, driver):
+# self.driver = driver
+#
+# @abstractmethod
+# def login(self, username, password):
+# pass
+#
+# @abstractmethod
+# def search_item(self, item_name):
+# pass
+#
+# @abstractmethod
+# def add_to_cart(self, item_name):
+# pass
+#
+# @abstractmethod
+# def checkout(self):
+# pass
+
+
+
+# דוגמא לקוד ששני הביאה
+#
+# class StoreA(OnlineStore):
+# def login(self, username, password):
+# self.driver.get("https://store-a.com/login")
+# WebDriverWait(self.driver, 10).until(
+# EC.presence_of_element_located((By.ID, "username"))
+# ).send_keys(username)
+# self.driver.find_element(By.ID, "password").send_keys(password)
+# self.driver.find_element(By.ID, "login-button").click()
+#
+# def search_item(self, item_name):
+# search_box = WebDriverWait(self.driver, 10).until(
+# EC.presence_of_element_located((By.ID, "search-box"))
+# )
+# search_box.send_keys(item_name)
+# search_box.submit()
+#
+# def add_to_cart(self, item_name):
+# self.search_item(item_name)
+# WebDriverWait(self.driver, 10).until(
+# EC.presence_of_element_located((By.CLASS_NAME, "add-to-cart"))
+# ).click()
+#
+# def checkout(self):
+# self.driver.get("https://store-a.com/cart")
+# WebDriverWait(self.driver, 10).until(
+# EC.presence_of_element_located((By.ID, "checkout-button"))
+# ).click()
+#
diff --git a/main.py b/main.py
index f0bb0c5..0707fa1 100644
--- a/main.py
+++ b/main.py
@@ -1,19 +1,329 @@
-import requests
-from bs4 import BeautifulSoup
-from models.product import Product
-
-from scrapers.shukcity_scraper import ShukCityScraper
-if __name__ == "__main__":
- scraper = ShukCityScraper()
- results = scraper.scrape_product("מים")
- print(f"התקבלו {len(results)} מוצרים")
- for product in results:
- print(product)
-
-# from scrapers.rami_levy_scraper import RamiLevyScraper
+# import requests
+# from bs4 import BeautifulSoup
+# from models.product import Product
+# from scarpers.shukcity_scraper import ShukCityScraper
+#
+# def print_product(product: Product):
+# print(f"מוצר: {product.name}")
+# print(f"מחיר: {product.get_formatted_price()}")
+# if product.brand:
+# print(f"יצרן: {product.brand}")
+# print(f"חנות: {product.store}")
+# if product.quantity:
+# print(f"כמות: {product.quantity}")
+# if product.get_unit_price_display():
+# print(f"מחיר ליחידה: {product.get_unit_price_display()}")
+# if product.description:
+# print(f"תיאור: {product.description}")
+# print("-" * 40)
+#
+# if __name__ == "__main__":
+# scraper = ShukCityScraper()
+# results = scraper.scrape_product("מים")
+# print(f"התקבלו {len(results)} מוצרים:\n")
+# for product in results:
+# print_product(product)
+#
+
+
+
+
+
+# from scarpers.rami_levy_scraper import RamiLevyScraper
# if __name__ == "__main__":
# scraper = RamiLevyScraper()
# results = scraper.scrape_product("מים")
# print(f"התקבלו {len(results)} מוצרים")
# for product in results:
-# print(product)
\ No newline at end of file
+# print(product)
+#
+# from scarpers.ybitan_scraper import YbitanScraper
+# from models.product import Product
+#
+# if __name__ == "__main__":
+# scraper = YbitanScraper()
+#
+#
+# product_to_search = "מים"
+#
+# print(f"=== חיפוש מוצר: {product_to_search} ===")
+#
+# products = scraper.search_products(product_to_search)
+#
+# if products:
+# print(f"נמצאו {len(products)} מוצרים:")
+# print("-" * 50)
+#
+# for i, product in enumerate(products, 1):
+# print(f"{i}. {product.name}")
+# print(f" מחיר: {product.price}₪")
+#
+# if product.brand:
+# print(f" מותג: {product.brand}")
+#
+# if product.quantity:
+# print(f" כמות: {product.quantity}")
+#
+# if hasattr(product, 'display_info') and product.display_info:
+# print(f" {product.display_info}")
+#
+# print("-" * 30)
+# else:
+# print(f"לא נמצאו מוצרים עבור '{product_to_search}'")
+# print("נסה:")
+# print("1. לוודא שהמוצר קיים באתר")
+# print("2. לנסות שם מוצר אחר")
+# print("3. לבדוק את החיבור לאינטרנט")
+#
+#
+# import unittest
+# import os
+# import undetected_chromedriver as uc
+# from scarpers.zap_market import ZapScraper
+# import time
+#
+# class TestZapScraper(unittest.TestCase):
+#
+# @classmethod
+# def setUpClass(cls):
+# print("🚀 מעלה את הדפדפן עם הגדרות משופרות...")
+#
+# options = uc.ChromeOptions()
+#
+# # הגדרות נגד זיהוי אוטומציה
+# options.add_argument(
+# "--user-agent=Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36")
+# options.add_argument("--no-sandbox")
+# options.add_argument("--disable-dev-shm-usage")
+# options.add_argument("--disable-blink-features=AutomationControlled")
+# # options.add_experimental_option("excludeSwitches", ["enable-automation"])
+# # options.add_experimental_option('useAutomationExtension', False)
+#
+# # הגדרות נוספות ליציבות
+# options.add_argument("--disable-extensions")
+# options.add_argument("--disable-plugins")
+# options.add_argument("--disable-images") # מאיץ טעינה
+# # הסרתי את --disable-javascript כי זה עלול לגרום לטעינה לקויה או חסימה באתר
+# # options.add_argument("--disable-javascript")
+# options.add_argument("--window-size=1920,1080")
+#
+# # הגדרות זיכרון
+# options.add_argument("--max_old_space_size=4096")
+# options.add_argument("--memory-pressure-off")
+#
+# # הסרת headless לבדיקה ויזואלית
+# # options.add_argument("--headless")
+#
+# try:
+# cls.driver = uc.Chrome(options=options, version_main=None) # אוטו-זיהוי גרסה
+#
+# # הגדרות נוספות אחרי יצירת הדרייבר
+# cls.driver.execute_script("Object.defineProperty(navigator, 'webdriver', {get: () => undefined})")
+#
+# print("✅ דפדפן הופעל בהצלחה")
+# print(f"✅ webdriver status: {cls.driver.execute_script('return navigator.webdriver')}")
+# print(f"✅ user agent: {cls.driver.execute_script('return navigator.userAgent')}")
+#
+# cls.scraper = ZapScraper(cls.driver)
+#
+# except Exception as e:
+# print(f"❌ כישלון בהפעלת הדפדפן: {e}")
+# raise
+#
+# @classmethod
+# def tearDownClass(cls):
+# print("🧹 סוגר את הדפדפן...")
+# try:
+# if hasattr(cls, 'driver'):
+# cls.driver.quit()
+# print("✅ דפדפן נסגר בהצלחה")
+# except Exception as e:
+# print(f"⚠️ בעיה בסגירת הדפדפן: {e}")
+#
+# def setUp(self):
+# """הכנה לפני כל טסט"""
+# print(f"\n{'=' * 50}")
+# print(f"מתחיל טסט חדש...")
+# time.sleep(2) # מנוחה בין טסטים - ייבוא המודול time מונע שגיאה
+#
+# def test_chalav_tnuva(self):
+# products = [
+# {
+# "name": "חלב תנובה",
+# "brand": "תנובה",
+# "weight": 1,
+# "weight_unit": "ליטר"
+# }
+# ]
+#
+# print(f"🔬 מבצע בדיקה עבור: {products[0]['name']}")
+#
+# if not self.scraper.is_driver_alive():
+# self.fail("❌ הדרייבר לא פעיל לפני התחלת הטסט")
+#
+# results = self.scraper.scrape_products(products)
+#
+# self.scraper.save_screenshot("final_test_chalav")
+#
+# if len(results) == 0:
+# print("❌ לא נמצאו תוצאות - בודק אם זה בגלל חסימה")
+# try:
+# self.scraper.driver.get("https://www.google.com")
+# print("✅ הגישה לגוגל עובדת - הבעיה ספציפית לזאפ")
+# except:
+# print("❌ בעיה כללית בחיבור לאינטרנט")
+#
+# if len(results) > 0:
+# print(f"✅ נמצאו {len(results)} תוצאות")
+# self.assertIn("תנובה", results[0]["name"],
+# f"❌ שם המוצר לא כולל את המותג 'תנובה': {results[0]['name']}")
+# else:
+# print("⚠️ לא נמצאו תוצאות - יתכן שהאתר חוסם")
+#
+# def test_gvina_tuv_taam(self):
+# products = [
+# {
+# "name": "גבינה לבנה 5%",
+# "brand": "טוב טעם",
+# "weight": 250,
+# "weight_unit": "גרם"
+# }
+# ]
+#
+# print(f"🔬 מבצע בדיקה עבור: {products[0]['name']}")
+#
+# if not self.scraper.is_driver_alive():
+# self.fail("❌ הדרייבר לא פעיל לפני התחלת הטסט")
+#
+# results = self.scraper.scrape_products(products)
+#
+# self.scraper.save_screenshot("final_test_gvina")
+#
+# if len(results) > 0:
+# print(f"✅ נמצאו {len(results)} תוצאות")
+# self.assertIn("גבינה", results[0]["name"],
+# f"❌ שם המוצר לא כולל את המילה 'גבינה': {results[0]['name']}")
+# else:
+# print("⚠️ לא נמצאו תוצאות - יתכן שהאתר חוסם")
+#
+# def test_basic_connectivity(self):
+# print("🔗 בודק חיבור בסיסי לאתר...")
+#
+# try:
+# if not self.scraper.is_driver_alive():
+# self.fail("❌ הדרייבר לא פעיל")
+#
+# success = self.scraper.load_homepage_with_retry()
+# self.assertTrue(success, "❌ כישלון בטעינת דף הבית")
+#
+# current_url = self.scraper.driver.current_url
+# print(f"✅ URL נוכחי: {current_url}")
+#
+# title = self.scraper.driver.title
+# print(f"✅ כותרת העמוד: {title}")
+#
+# except Exception as e:
+# self.fail(f"❌ בעיה בחיבור בסיסי: {e}")
+#
+#
+# if __name__ == "__main__":
+# unittest.main(verbosity=2)
+#
+# from selenium import webdriver
+# from selenuim.shufersal_site import ShufersalSite, Product, CustomerInfo, PaymentInfo
+# import time
+#
+# # הפעלת דפדפן
+# driver = webdriver.Chrome()
+#
+# # יצירת מופע
+# shufersal = ShufersalSite(driver)
+#
+# # פתיחת אתר
+# if shufersal.open_site():
+# if shufersal.set_delivery_location("תל אביב, הרצל 10"):
+# products = shufersal.search_product("חלב")
+# if products:
+# shufersal.add_to_cart(products[0], quantity=1)
+# shufersal.view_cart()
+# if shufersal.proceed_to_checkout():
+# customer = CustomerInfo(
+# first_name="טסט",
+# last_name="בדיקה",
+# email="test@example.com",
+# phone="0500000000",
+# address="הרצל 10",
+# city="תל אביב",
+# zip_code="12345"
+# )
+# payment = PaymentInfo(
+# card_number="1234123412341234", # לא אמיתי
+# expiry_month="12",
+# expiry_year="2027",
+# cvv="123",
+# card_holder_name="טסט בדיקה"
+# )
+#
+# if shufersal.fill_customer_info(customer):
+# if shufersal.fill_payment_info(payment):
+# order_summary = shufersal.complete_purchase()
+# print(order_summary)
+#
+# # המתנה לצפייה
+# time.sleep(10)
+# driver.quit()
+#
+
+
+#
+# from selenium import webdriver
+# from stores.shufersal_store import ShufersalStore
+# import time
+# print("פותח דפדפן...")
+#
+# driver = webdriver.Chrome() # ודא ש-Chromedriver מותקן
+# store = ShufersalStore(driver)
+#
+# print("מריץ חיפוש...")
+#
+# # store.search_item("גבינה צהובה עמק 28 400 גר") # לדוגמה – חיפוש חלב
+# store.add_to_cart("גבינה צהובה עמק 28 400 גר")
+#
+# print("המתנה לסגירה")
+# input("לחץ אנטר לסגור את הדפדפן")
+# driver.quit()
+
+# הרצת דימוי משתמש ברמי לוי
+from selenium import webdriver
+from stores.rami_levy_store import RamiLevyStore
+from dotenv import load_dotenv
+import os
+import traceback
+
+print("📦 טוען קובץ .env...")
+load_dotenv()
+
+username = os.getenv("RAMI_LEVY_USER")
+password = os.getenv("RAMI_LEVY_PASS")
+
+if not username or not password:
+ raise Exception("❌ חסר ערך ל־RAMI_LEVY_USER או RAMI_LEVY_PASS בקובץ .env")
+
+print("🚀 מפעיל את הדפדפן...")
+driver = webdriver.Chrome()
+
+try:
+ print("🌐 יוצר מופע של החנות רמי לוי...")
+ store = RamiLevyStore(driver)
+
+ print("🔐 מתחבר עם שם משתמש וסיסמה...")
+ store.login(username, password)
+
+ print("✅ התחברות הסתיימה בהצלחה.")
+except Exception as e:
+ print(f"❌ שגיאה במהלך ההרצה: {e}")
+ traceback.print_exc()
+finally:
+ print("🧹 סוגר את הדפדפן...")
+ input("לחץ אנטר כדי לסגור את הדפדפן ולהסיים...")
+ driver.quit()
\ No newline at end of file
diff --git a/models/product.py b/models/product.py
index d1bf22f..884bc76 100644
--- a/models/product.py
+++ b/models/product.py
@@ -180,44 +180,309 @@
# 'price_per_unit': self.get_price_per_unit()
# }
-class Product:
- def __init__(self, name, price, store, quantity=None, brand=None):
- self.name = name
- self.price = price
- self.store = store
- self.quantity = quantity
- self.brand = brand
- # שדות חדשים למחיר יחסי
- self.unit_price = None
- self.unit_type = None
- self.display_info = None
+# class Product:
+# def __init__(self, name, price, store, quantity=None, brand=None):
+# self.name = name
+# self.price = price
+# self.store = store
+# self.quantity = quantity
+# self.brand = brand
+# # שדות חדשים למחיר יחסי
+# self.unit_price = None
+# self.unit_type = None
+# self.display_info = None
+#
+# def __str__(self):
+# """הצגת המוצר עם מחיר יחסי"""
+# # בניית מחרוזת בסיסית
+# result = f"{self.store}: {self.name}"
+#
+# # הוספת יצרן אם קיים
+# if self.brand:
+# result += f" ({self.brand})"
+#
+# # הוספת כמות אם קיימת
+# if self.quantity:
+# result += f" [{self.quantity}]"
+#
+# # הוספת מחיר
+# result += f" - {self.price} ש\"ח"
+#
+# # הוספת מחיר יחסי אם קיים
+# if self.display_info:
+# result += f" ({self.display_info})"
+#
+# return result
+#
+# def get_unit_price_value(self):
+# """החזרת המחיר היחסי כמספר לצורך השוואה"""
+# return self.unit_price if self.unit_price else float('inf')
+#
+# def get_formatted_unit_price(self):
+# """החזרת המחיר היחסי מעוצב"""
+# return self.display_info if self.display_info else "לא זמין"
+
+#
+# import re
+#
+# class Product:
+# def __init__(self, name, price, store, quantity=None, brand=None):
+# self.name = name
+# self.price = price
+# self.store = store
+# self.quantity = quantity
+# self.brand = brand
+# self.unit_price = None
+# self.unit_type = None
+# self.display_info = None
+#
+# # חישוב מחיר יחסי אם אפשר
+# if quantity:
+# self.original_quantity = quantity # לשימור מחרוזת המקור
+# total_qty, unit = self._parse_total_quantity(quantity)
+# if total_qty and unit:
+# if unit == 'מ"ל':
+# unit_price = round(price / total_qty * 100, 2)
+# display_unit = '100 מ"ל'
+# elif unit == 'גרם':
+# unit_price = round(price / total_qty * 100, 2)
+# display_unit = '100 גרם'
+# elif unit == 'ליטר':
+# unit_price = round(price / total_qty, 2)
+# display_unit = 'ליטר'
+# elif unit == 'ק"ג':
+# unit_price = round(price / total_qty, 2)
+# display_unit = 'ק"ג'
+# else:
+# unit_price = None
+# display_unit = None
+#
+# if unit_price is not None:
+# self.unit_price = unit_price
+# self.unit_type = display_unit
+# self.display_info = f"{unit_price} ש\"ח ל{display_unit}"
+# self.quantity = f"{total_qty} {unit}" # שומר את כמות המקור (כולל יחידה)
+#
+# # def _parse_total_quantity(self, quantity_str):
+# # """
+# # מנתח מחרוזות כמו:
+# # - "6x2 ליטר" => 12 ליטר
+# # - "10x10 גרם" => 100 גרם
+# # - "1.5 ליטר" => 1.5 ליטר
+# # - "500 מ\"ל" => 500 מ"ל
+# # - "1 יחידה" => לא מחשב
+# # """
+# # quantity_str = quantity_str.strip()
+# #
+# # # תבנית למארזים: 6x2 ליטר, 10x10 גרם
+# # match_pack = re.search(
+# # r'(\d+)\s*(?:x|\*|כפול)?\s*(\d+\.?\d*)\s*(ליטר|מ"ל|גרם|ק"ג)',
+# # quantity_str.replace('מ\"ל', 'מ"ל').replace('ק\"ג', 'ק"ג') # נרמל צורת גרשיים
+# # )
+# # if match_pack:
+# # units = int(match_pack.group(1))
+# # amount = float(match_pack.group(2))
+# # unit = match_pack.group(3)
+# # total = units * amount
+# # return total, unit
+# #
+# # # תבנית רגילה: 1.5 ליטר, 500 מ"ל
+# # match_single = re.match(r'(\d+\.?\d*)\s*(ליטר|מ\"ל|גרם|ק\"ג)', quantity_str)
+# # if match_single:
+# # total = float(match_single.group(1))
+# # unit = match_single.group(2)
+# # return total, unit
+# #
+# # # לא הצליח לפרש – אין כמות מדידה
+# # return None, None
+#
+# @staticmethod
+# def _parse_total_quantity(quantity_str):
+# """
+# מפרש תיאור כמות כמו:
+# - "6x2 ליטר" => 12 ליטר
+# - "10x10 גרם" => 100 גרם
+# - "1.5 ליטר" => 1.5 ליטר
+# - "500 מ\"ל" => 500 מ"ל
+# - "שישייה 1.5 ליטר" => 9 ליטר
+# - "320*6 מ"ל" => 1920 מ"ל
+# - "1 יחידה" => None
+# """
+# quantity_str = quantity_str.strip().replace('\"', '"').replace('מ\"ל', 'מ"ל').replace('ק\"ג', 'ק"ג')
+#
+# # מילים מוכרות למארזים
+# known_packs = {
+# 'שישייה': 6,
+# 'רביעייה': 4,
+# 'שלישייה': 3,
+# 'זוג': 2,
+# }
+#
+# # 1. זיהוי לפי מילת מארז + נפח
+# for word, count in known_packs.items():
+# pattern = rf'{word}\s*(\d+\.?\d*)\s*(ליטר|מ"ל|גרם|ק"ג)'
+# match = re.search(pattern, quantity_str)
+# if match:
+# amount = float(match.group(1))
+# unit = match.group(2)
+# return count * amount, unit
+#
+# # 2. זיהוי לפי כמות × גודל – בשני הסדרים
+# patterns = [
+# r'(\d+)\s*[x\*]\s*(\d+\.?\d*)\s*(ליטר|מ"ל|גרם|ק"ג)', # כמות × גודל
+# r'(\d+\.?\d*)\s*[x\*]\s*(\d+)\s*(ליטר|מ"ל|גרם|ק"ג)', # גודל × כמות
+# ]
+# for pattern in patterns:
+# match = re.search(pattern, quantity_str)
+# if match:
+# a = float(match.group(1))
+# b = float(match.group(2))
+# unit = match.group(3)
+# # נניח שהכמות היא השלם והנפח הוא השבר (אם ברור)
+# if a.is_integer() and not b.is_integer():
+# units, amount = int(a), b
+# elif b.is_integer() and not a.is_integer():
+# units, amount = int(b), a
+# else:
+# # אם שניהם שלמים או שניהם עשרוניים – נניח הראשון זה כמות
+# units, amount = int(a), b
+# return units * amount, unit
+#
+# # 3. זיהוי רגיל – כמות אחת
+# match = re.match(r'(\d+\.?\d*)\s*(ליטר|מ"ל|גרם|ק"ג)', quantity_str)
+# if match:
+# return float(match.group(1)), match.group(2)
+#
+# # 4. לא הצליח לפרש
+# return None, None
+#
+# def __str__(self):
+# result = f"{self.store}: {self.name}"
+# if self.brand:
+# result += f" ({self.brand})"
+# if self.quantity:
+# result += f" [{self.quantity}]"
+# result += f" - {self.price} ש\"ח"
+# if self.display_info:
+# result += f" ({self.display_info})"
+# return result
+#
+# def get_unit_price_value(self):
+# return self.unit_price if self.unit_price else float('inf')
+#
+# def get_formatted_unit_price(self):
+# return self.display_info if self.display_info else "לא זמין"
+#
+# import re
+# from models.utils import parse_total_quantity
+#
+# class Product:
+# def __init__(self, name, price, store, quantity=None, brand=None):
+# self.name = name
+# self.price = price
+# self.store = store
+# self.quantity = quantity
+# self.brand = brand
+# self.unit_price = None
+# self.unit_type = None
+# self.display_info = None
+#
+# if quantity:
+# self.original_quantity = quantity
+# total_qty, unit = parse_total_quantity(quantity)
+# if total_qty and unit:
+# if unit == 'מ"ל':
+# unit_price = round(price / total_qty * 100, 2)
+# display_unit = '100 מ"ל'
+# elif unit == 'גרם':
+# unit_price = round(price / total_qty * 100, 2)
+# display_unit = '100 גרם'
+# elif unit == 'ליטר':
+# unit_price = round(price / total_qty, 2)
+# display_unit = 'ליטר'
+# elif unit == 'ק"ג':
+# unit_price = round(price / total_qty, 2)
+# display_unit = 'ק"ג'
+# else:
+# unit_price = None
+# display_unit = None
+#
+# if unit_price is not None:
+# self.unit_price = unit_price
+# self.unit_type = display_unit
+# self.display_info = f"{unit_price} ש\"ח ל{display_unit}"
+# self.quantity = f"{total_qty} {unit}"
+#
+# def __str__(self):
+# result = f"{self.store}: {self.name}"
+# if self.brand:
+# result += f" ({self.brand})"
+# if self.quantity:
+# result += f" [{self.quantity}]"
+# result += f" - {self.price} ש\"ח"
+# if self.display_info:
+# result += f" ({self.display_info})"
+# return result
+#
+# def get_unit_price_value(self):
+# return self.unit_price if self.unit_price else float('inf')
+#
+# def get_formatted_unit_price(self):
+# return self.display_info if self.display_info else "לא זמין"
+
- def __str__(self):
- """הצגת המוצר עם מחיר יחסי"""
- # בניית מחרוזת בסיסית
- result = f"{self.store}: {self.name}"
+from dataclasses import dataclass
+from typing import Optional
- # הוספת יצרן אם קיים
- if self.brand:
- result += f" ({self.brand})"
- # הוספת כמות אם קיימת
- if self.quantity:
- result += f" [{self.quantity}]"
+@dataclass
+class Product:
+ """
+ מודל למוצר עם תמיכה במחירים יחסיים
+ """
+ name: str
+ price: float
+ store: str
+ quantity: Optional[str] = None
+ brand: Optional[str] = None
+
+ # שדות למחיר יחסי
+ unit_price: Optional[float] = None
+ unit_type: Optional[str] = None
+ display_info: Optional[str] = None
- # הוספת מחיר
- result += f" - {self.price} ש\"ח"
+ # שדות נוספים לשימוש עתידי
+ description: Optional[str] = None
+ category: Optional[str] = None
+ is_available: bool = True
- # הוספת מחיר יחסי אם קיים
- if self.display_info:
- result += f" ({self.display_info})"
+ def __post_init__(self):
+ """אתחול לאחר יצירת האובייקט"""
+ if not self.description:
+ self.description = f"{self.name} - {self.store}"
- return result
+ def get_formatted_price(self) -> str:
+ """החזרת מחיר מעוצב"""
+ return f"₪{self.price:.2f}"
- def get_unit_price_value(self):
- """החזרת המחיר היחסי כמספר לצורך השוואה"""
- return self.unit_price if self.unit_price else float('inf')
+ def get_unit_price_display(self) -> str:
+ """החזרת מחיר יחסי מעוצב"""
+ if self.unit_price and self.unit_type:
+ return f"₪{self.unit_price:.2f} ל{self.unit_type}"
+ return ""
- def get_formatted_unit_price(self):
- """החזרת המחיר היחסי מעוצב"""
- return self.display_info if self.display_info else "לא זמין"
\ No newline at end of file
+ def to_dict(self) -> dict:
+ """המרה למילון"""
+ return {
+ 'name': self.name,
+ 'price': self.price,
+ 'store': self.store,
+ 'quantity': self.quantity,
+ 'brand': self.brand,
+ 'unit_price': self.unit_price,
+ 'unit_type': self.unit_type,
+ 'display_info': self.display_info,
+ 'description': self.description,
+ 'category': self.category,
+ 'is_available': self.is_available
+ }
\ No newline at end of file
diff --git a/models/quantity_extractor.py b/models/quantity_extractor.py
new file mode 100644
index 0000000..6040541
--- /dev/null
+++ b/models/quantity_extractor.py
@@ -0,0 +1,237 @@
+import re
+from typing import Optional, Tuple
+from models.product import Product
+
+
+class QuantityExtractor:
+ def __init__(self):
+ self.multi_unit_patterns = [
+ r'(\d+)\s*[*×x]\s*(\d+(?:\.\d+)?)\s*(ליטר|מ"ל|ק"ג|גרם|יח\'?|יחידות?)',
+ r'(\d+)\s*(?:×|x|\*)\s*(\d+(?:\.\d+)?)\s*(ליטר|מ"ל|ק"ג|גרם|יח\'?|יחידות?)',
+ r'(\d+)\s*(?:יח\'?|יחידות?)\s*(?:של|×|x|\*)\s*(\d+(?:\.\d+)?)\s*(ליטר|מ"ל|ק"ג|גרם)',
+ r'(\d+)\s*(?:בקבוקי|בקבוקים)\s*(?:של|×|x|\*)\s*(\d+(?:\.\d+)?)\s*(ליטר|מ"ל)',
+ r'(\d+)\s*(?:חבילות|חבי?לות)\s*(?:של|×|x|\*)\s*(\d+(?:\.\d+)?)\s*(ק"ג|גרם)',
+ ]
+
+ self.regular_patterns = [
+ (r'(\d+(?:\.\d+)?)\s*(?:ליטר|ל\'?)', 'ליטר'),
+ (r'(\d+(?:\.\d+)?)\s*(?:מ"?ל|מיליליטר)', 'מ"ל'),
+ (r'(\d+(?:\.\d+)?)\s*(?:ק"?ג|קילו|קילוגרם)', 'ק"ג'),
+ (r'(\d+(?:\.\d+)?)\s*(?:גר\'?|גרם)', 'גרם'),
+ (r'(\d+(?:\.\d+)?)\s*(?:יח\'?|יחידות?|יחידה)', 'יחידה'),
+ (r'(\d+(?:\.\d+)?)\s*(?:חבי?לות?|חבילה)', 'יחידה'),
+ (r'(\d+(?:\.\d+)?)\s*(?:בקבוקים?|בקבוק)', 'יחידה'),
+ (r'(\d+(?:\.\d+)?)\s*(?:כוסות?|כוס)', 'יחידה'),
+ (r'(\d+(?:\.\d+)?)\s*(?:פחיות?|פחית)', 'יחידה'),
+ ]
+
+ self.word_quantity_map = {
+ 'שישיה': 6,
+ 'שישיית': 6,
+ 'שלישיה': 3,
+ 'שלישיית': 3,
+ 'רביעייה': 4,
+ 'רביעיית': 4,
+ 'מארז של': 6,
+ 'מארז': 6,
+ 'שיש': 6,
+ }
+
+ def extract_quantity_from_fields(self, weight: Optional[float] = None,
+ unit_of_measure: Optional[str] = None,
+ quantity_field: Optional[str] = None,
+ size_field: Optional[str] = None) -> str:
+ if weight and unit_of_measure:
+ normalized_unit = self.normalize_unit_type(unit_of_measure)
+ return f"{weight} {normalized_unit}"
+
+ if quantity_field:
+ return str(quantity_field).strip()
+
+ if size_field:
+ return str(size_field).strip()
+
+ return ""
+
+ def extract_quantity_from_name(self, product_name: str) -> str:
+ if not product_name:
+ return ""
+
+ quantity = self._extract_multi_unit_quantity(product_name)
+ if quantity:
+ return quantity
+
+ quantity = self._extract_word_based_quantity(product_name)
+ if quantity:
+ return quantity
+
+ return self._extract_regular_quantity(product_name)
+
+ def _extract_multi_unit_quantity(self, product_name: str) -> str:
+ for pattern in self.multi_unit_patterns:
+ match = re.search(pattern, product_name, re.IGNORECASE)
+ if match:
+ units_count = match.group(1)
+ unit_size = match.group(2)
+ unit_type = match.group(3)
+ unit_type_normalized = self.normalize_unit_type(unit_type)
+ return f"{units_count} יחידות של {unit_size} {unit_type_normalized}"
+ return ""
+
+ def _extract_word_based_quantity(self, product_name: str) -> str:
+ for word, count in self.word_quantity_map.items():
+ if word in product_name:
+ match = re.search(r'(\d+(?:\.\d+)?)\s*(ליטר|מ"ל|גרם|ק"ג)', product_name)
+ if match:
+ size = match.group(1)
+ unit = match.group(2)
+ unit = self.normalize_unit_type(unit)
+ return f"{count} יחידות של {size} {unit}"
+ return ""
+
+ def _extract_regular_quantity(self, product_name: str) -> str:
+ for pattern, unit_type in self.regular_patterns:
+ match = re.search(pattern, product_name, re.IGNORECASE)
+ if match:
+ return match.group(0)
+ return ""
+
+ def normalize_unit_type(self, unit_type: str) -> str:
+ if not unit_type:
+ return ""
+
+ unit_type = unit_type.lower().strip()
+
+ unit_mapping = {
+ 'ליטר': ['ליטר', 'ל', 'ל\'', 'liter', 'l'],
+ 'מ"ל': ['מ"ל', 'מל', 'מיליליטר', 'ml', 'milliliter'],
+ 'ק"ג': ['ק"ג', 'קג', 'קילו', 'קילוגרם', 'kg', 'kilogram'],
+ 'גרם': ['גר\'', 'גר', 'גרם', 'g', 'gram', 'גרמים'],
+ 'יחידה': ['יח\'', 'יח', 'יחידות', 'יחידה', 'יחי', 'unit', 'piece']
+ }
+
+ for normalized, variations in unit_mapping.items():
+ if unit_type in variations:
+ return normalized
+
+ return unit_type
+
+ def parse_quantity_text(self, quantity_text: str) -> Tuple[Optional[float], Optional[str]]:
+ if not quantity_text:
+ return None, None
+
+ quantity_text = quantity_text.lower().strip()
+
+ # ניסיון רגיל עם תבנית מלאה
+ multi_unit_match = re.search(
+ r'(\d+)\s*(?:יחידות|יח\'?|בקבוקים?|חבילות?)\s*של\s*(\d+(?:\.\d+)?)\s*(ליטר|מ"ל|ק"ג|גרם)',
+ quantity_text
+ )
+ if multi_unit_match:
+ units_count = float(multi_unit_match.group(1))
+ unit_size = float(multi_unit_match.group(2))
+ unit_type = multi_unit_match.group(3)
+ total_quantity = units_count * unit_size
+ return total_quantity, self.normalize_unit_type(unit_type)
+
+ # ניסיון רגיל: למצוא כמות אחת
+ for pattern, unit in self.regular_patterns:
+ match = re.search(pattern, quantity_text)
+ if match:
+ try:
+ quantity_num = float(match.group(1))
+ unit = self.normalize_unit_type(unit)
+
+ # בדיקה אם יש מילה שמעידה על כמה יחידות
+ for word, count in self.word_quantity_map.items():
+ if word in quantity_text:
+ return quantity_num * count, unit
+
+ return quantity_num, unit
+ except (ValueError, IndexError):
+ continue
+
+ # ניסיון אחרון: מילה שמעידה על כמות אך בלי מספר
+ for word, count in self.word_quantity_map.items():
+ if word in quantity_text:
+ # נניח שהיחידה היא "מ"ל" ונחפש מספר
+ match = re.search(r'(\d+(?:\.\d+)?)\s*(מ"ל|ליטר|גרם|ק"ג)', quantity_text)
+ if match:
+ quantity_num = float(match.group(1))
+ unit = self.normalize_unit_type(match.group(2))
+ return quantity_num * count, unit
+
+ return None, None
+
+ def calculate_unit_price(self, product: Product):
+ if not product.price:
+ return
+
+ quantity_text = product.quantity
+
+ if not quantity_text:
+ quantity_text = self.extract_quantity_from_name(product.name)
+ if quantity_text:
+ product.quantity = quantity_text
+
+ if not quantity_text:
+ return
+
+ quantity_num, unit = self.parse_quantity_text(quantity_text)
+
+ if quantity_num and unit:
+ self._calculate_price_by_unit_type(product, quantity_num, unit)
+
+ def _calculate_price_by_unit_type(self, product: Product, quantity_num: float, unit: str):
+ try:
+ if unit == 'מ"ל':
+ unit_price = round((product.price / quantity_num) * 100, 2)
+ product.unit_price = unit_price
+ product.unit_type = '100 מ"ל'
+ product.display_info = f"₪{unit_price} ל-100 מ\"ל"
+
+ elif unit == 'ליטר':
+ unit_price = round(product.price / quantity_num, 2)
+ product.unit_price = unit_price
+ product.unit_type = 'ליטר'
+ product.display_info = f"₪{unit_price} לליטר"
+
+ elif unit == 'גרם':
+ unit_price = round((product.price / quantity_num) * 100, 2)
+ product.unit_price = unit_price
+ product.unit_type = '100 גרם'
+ product.display_info = f"₪{unit_price} ל-100 גרם"
+
+ elif unit == 'ק"ג':
+ unit_price = round(product.price / quantity_num, 2)
+ product.unit_price = unit_price
+ product.unit_type = 'ק"ג'
+ product.display_info = f"₪{unit_price} לק\"ג"
+
+ elif unit == 'יחידה':
+ unit_price = round(product.price / quantity_num, 2)
+ product.unit_price = unit_price
+ product.unit_type = 'יחידה'
+ product.display_info = f"₪{unit_price} ליחידה"
+
+ except (ValueError, ZeroDivisionError):
+ pass
+
+ def get_weight_and_unit_for_db(self, quantity_text: str) -> Tuple[Optional[float], Optional[str]]:
+ quantity_num, unit = self.parse_quantity_text(quantity_text)
+
+ if quantity_num and unit:
+ if unit in ['ליטר', 'מ"ל']:
+ if unit == 'ליטר':
+ return quantity_num * 1000, 'מ"ל'
+ return quantity_num, 'מ"ל'
+
+ elif unit in ['ק"ג', 'גרם']:
+ if unit == 'ק"ג':
+ return quantity_num * 1000, 'גרם'
+ return quantity_num, 'גרם'
+
+ elif unit == 'יחידה':
+ return quantity_num, 'יחידה'
+
+ return None, None
diff --git a/scarpers/shukcity_scraper.py b/scarpers/shukcity_scraper.py
index 338b7ef..ba73e8e 100644
--- a/scarpers/shukcity_scraper.py
+++ b/scarpers/shukcity_scraper.py
@@ -1,13 +1,16 @@
import requests
from bs4 import BeautifulSoup
from models.product import Product
-from scrapers.base_scraper import StoreScraper
+from scarpers.base_scraper import StoreScraper
+from models.quantity_extractor import QuantityExtractor
import re
+import time
class ShukCityScraper(StoreScraper):
def __init__(self):
super().__init__("https://www.shukcity.co.il")
+ self.quantity_extractor = QuantityExtractor()
def scrape_product(self, product_name):
url = f"{self.base_url}/v2/retailers/1254/branches/1639/products"
@@ -40,19 +43,16 @@ def scrape_product(self, product_name):
print("מבצע ביקור ראשון לאתר...")
home_response = session.get("https://www.shukcity.co.il/")
print(f"ביקור בעמוד הבית: {home_response.status_code}")
-
- # המתנה קטנה
- import time
time.sleep(2)
-
except Exception as e:
print(f"שגיאה בביקור בעמוד הבית: {e}")
all_products = []
- size = 50
+ size = 10 # שינוי לשליפת 10 תוצאות בלבד
start = 0
+ max_products = 10 # הגבלה על מספר המוצרים הכולל
- while True:
+ while len(all_products) < max_products:
params = {
"appId": "4",
"filters": '{"must":{"exists":["family.id","family.categoriesPaths.id","branch.regularPrice"],"term":{"branch.isActive":true,"branch.isVisible":true}},"mustNot":{"term":{"branch.regularPrice":0,"branch.isOutOfStock":true}}}',
@@ -64,12 +64,8 @@ def scrape_product(self, product_name):
try:
print(f"שולח בקשה עם start={start}, size={size}")
-
- # שימוש ב-session במקום requests ישירות
response = session.get(url, params=params, timeout=10)
print(f"סטטוס קוד: {response.status_code}")
-
- # הדפסת headers של התגובה לדיבוג
print(f"Content-Type: {response.headers.get('content-type', 'לא ידוע')}")
response.raise_for_status()
@@ -84,21 +80,26 @@ def scrape_product(self, product_name):
break
for i, item in enumerate(items):
+ # בדיקה אם הגענו למספר המוצרים המבוקש
+ if len(all_products) >= max_products:
+ print(f"הגענו ל-{max_products} מוצרים, מפסיק")
+ break
+
try:
- # חילוץ שם המוצר
+ # חילוץ נתוני המוצר
name = self._extract_product_name(item)
-
- # חילוץ מחיר
price = self._extract_price(item)
+ brand = self._extract_brand(item)
- # חילוץ כמות
- quantity = self._extract_quantity(item)
+ # חילוץ כמות באמצעות QuantityExtractor
+ quantity = self._extract_quantity_with_extractor(item)
- # חילוץ יצרן
- brand = self._extract_brand(item)
+ # יצירת מוצר
+ product = Product(name, price, "שוק העיר", quantity, brand)
+
+ # חישוב מחיר יחסי באמצעות QuantityExtractor
+ self.quantity_extractor.calculate_unit_price(product)
- # יצירת מוצר עם חישוב מחיר יחסי
- product = self._create_product_with_unit_price(name, price, quantity, brand)
all_products.append(product)
except Exception as e:
@@ -106,10 +107,12 @@ def scrape_product(self, product_name):
print(f"נתוני המוצר: {item}")
continue
- start += size
+ # בדיקה אם הגענו למספר המוצרים המבוקש
+ if len(all_products) >= max_products:
+ print(f"הושלמה שליפת {max_products} מוצרים")
+ break
- # הפסקה ארוכה יותר בין בקשות
- import time
+ start += size
time.sleep(1.5)
except requests.exceptions.HTTPError as e:
@@ -129,140 +132,64 @@ def scrape_product(self, product_name):
return all_products
- def _create_product_with_unit_price(self, name, price, quantity, brand):
- """יצירת מוצר עם חישוב מחיר יחסי"""
- # חישוב מחיר יחסי
- unit_price_info = self._calculate_unit_price(price, quantity)
-
- # יצירת מוצר בסיסי
- product = Product(name, price, "שוק העיר", quantity, brand)
-
- # הוספת מידע על מחיר יחסי
- if unit_price_info:
- product.unit_price = unit_price_info["price"]
- product.unit_type = unit_price_info["unit"]
- product.display_info = unit_price_info["display"]
-
- return product
-
- def _calculate_unit_price(self, price, quantity_str):
- """חישוב מחיר ליחידת מידה סטנדרטית"""
- if not quantity_str or price <= 0:
- return None
-
- # פרסוני כמות ויחידה
- quantity_info = self._parse_quantity(quantity_str)
- if not quantity_info:
- return None
-
- amount = quantity_info["amount"]
- unit = quantity_info["unit"]
-
- # הגדרת יחידות סטנדרטיות
- unit_standards = {
- # משקל
- "גרם": {"standard": 100, "display": "100 גרם"},
- "קילוגרם": {"standard": 1, "display": "1 ק\"ג"},
- "מיליגרם": {"standard": 100000, "display": "100 גרם"}, # המרה ל-100 גרם
-
- # נפח
- "מ\"ל": {"standard": 100, "display": "100 מ\"ל"},
- "ליטר": {"standard": 1, "display": "1 ליטר"},
-
- # יחידות
- "יחידות": {"standard": 1, "display": "יחידה"},
- }
-
- if unit not in unit_standards:
- return None
+ def _extract_quantity_with_extractor(self, item):
+ """חילוץ כמות באמצעות QuantityExtractor"""
+ # חילוץ נתונים מהשדות
+ weight = item.get("weight")
+ unit_of_measure_data = item.get("unitOfMeasure", {}).get("names", {}).get("1")
+ number_of_items = item.get("numberOfItems")
+ size_field = item.get("size")
- standard = unit_standards[unit]
+ # ניסיון ראשון - שימוש בשדות הישירים
+ quantity = self.quantity_extractor.extract_quantity_from_fields(
+ weight=weight,
+ unit_of_measure=unit_of_measure_data,
+ quantity_field=number_of_items,
+ size_field=size_field
+ )
- # חישוב מחיר יחסי
- unit_price = (price / amount) * standard["standard"]
+ if quantity:
+ return quantity
- return {
- "price": round(unit_price, 2),
- "unit": standard["display"],
- "display": f"{round(unit_price, 2)} ש\"ח ל{standard['display']}"
- }
+ # ניסיון שני - חילוץ משם המוצר
+ name = self._extract_product_name(item)
+ quantity_from_name = self.quantity_extractor.extract_quantity_from_name(name)
- def _parse_quantity(self, quantity_str):
- """פרסונג כמות ויחידה מתוך מחרוזת"""
- if not quantity_str:
- return None
+ if quantity_from_name:
+ return quantity_from_name
- # דפוסי חיפוש לכמות ויחידה
- patterns = [
- # דפוסים רגילים
- r'(\d+(?:\.\d+)?)\s*(ק"?ג|קילוגרם|גר\'?|גרם|מ"?ל|ליטר|מיליגרם|מ"?ג)',
- r'(\d+)\s*(יחידות?|יח\'?)',
+ # ניסיון שלישי - בדיקות נוספות ספציפיות לשוק העיר
+ return self._extract_quantity_fallback(item)
- # דפוסי מארז (4×1 ליטר)
- r'(\d+)\s*[×xX]\s*(\d+(?:\.\d+)?)\s*(ק"?ג|קילוגרם|גר\'?|גרם|מ"?ל|ליטר)',
+ def _extract_quantity_fallback(self, item):
+ """פונקציה גיבוי לחילוץ כמות - עבור מקרים מיוחדים"""
+ # בדיקות נוספות ספציפיות לשוק העיר
+ quantity_sources = [
+ item.get("original", {}).get("weight"),
+ item.get("original", {}).get("size"),
+ item.get("volume"),
+ item.get("original", {}).get("volume")
]
- for pattern in patterns:
- match = re.search(pattern, quantity_str, re.IGNORECASE)
- if match:
- groups = match.groups()
-
- if len(groups) == 3: # מארז
- count = float(groups[0])
- unit_amount = float(groups[1])
- unit = self._normalize_unit_for_calculation(groups[2])
- total_amount = count * unit_amount
- return {"amount": total_amount, "unit": unit}
- else: # רגיל
- amount = float(groups[0])
- unit = self._normalize_unit_for_calculation(groups[1])
- return {"amount": amount, "unit": unit}
-
- # ניסיון להבין מהשם אם לא נמצא דפוס
- if "ליטר" in quantity_str.lower():
- match = re.search(r'(\d+(?:\.\d+)?)', quantity_str)
- if match:
- return {"amount": float(match.group(1)), "unit": "ליטר"}
-
- if any(word in quantity_str.lower() for word in ["גרם", "ק\"ג", "קילו"]):
- match = re.search(r'(\d+(?:\.\d+)?)', quantity_str)
- if match:
- amount = float(match.group(1))
- if "ק" in quantity_str.lower() or "קילו" in quantity_str.lower():
- return {"amount": amount, "unit": "קילוגרם"}
- else:
- return {"amount": amount, "unit": "גרם"}
+ for quantity in quantity_sources:
+ if quantity and str(quantity).strip():
+ return str(quantity).strip()
- return None
+ # בדיקה לmultipack
+ number_of_items = item.get("numberOfItems")
+ weight = item.get("weight")
+ unit_of_measure = item.get("unitOfMeasure", {}).get("names", {}).get("1")
- def _normalize_unit_for_calculation(self, unit):
- """נירמול יחידות לחישוב מחיר יחסי"""
- unit_mapping = {
- 'ק"ג': 'קילוגרם',
- 'קילו': 'קילוגרם',
- 'קילוגרm': 'קילוגרם',
- 'גר\'': 'גרם',
- 'גרם': 'גרם',
- 'ג\'': 'גרם',
- 'מ"ל': 'מ"ל',
- 'מיליליטר': 'מ"ל',
- 'ל\'': 'ליטר',
- 'ליטר': 'ליטר',
- 'יח\'': 'יחידות',
- 'יחידה': 'יחידות',
- 'יחידות': 'יחידות',
- 'מ"ג': 'מיליגרם',
- 'מג': 'מיליגרם',
- 'מיליגרם': 'מיליגרם',
- }
+ if number_of_items and number_of_items > 1 and weight and unit_of_measure:
+ normalized_unit = self.quantity_extractor.normalize_unit_type(unit_of_measure)
+ return f"{number_of_items} יחידות של {weight} {normalized_unit}"
- return unit_mapping.get(unit.lower(), unit)
+ return None
def _extract_product_name(self, item):
"""חילוץ שם המוצר"""
- # מנסים מספר מקורות לשם המוצר
name_sources = [
- item.get("localName"), # שם מקומי - בדרך כלל הכי מדויק
+ item.get("localName"),
item.get("names", {}).get("1", {}).get("long"),
item.get("names", {}).get("1", {}).get("short"),
item.get("original", {}).get("names", {}).get("1", {}).get("long"),
@@ -286,7 +213,6 @@ def _extract_price(self, item):
except (ValueError, TypeError):
pass
- # נסיון נוסף למקרה שהמחיר נמצא במקום אחר
try:
price = item.get("price")
if price is not None:
@@ -296,80 +222,25 @@ def _extract_price(self, item):
return 0.0
- def _extract_quantity(self, item):
- """חילוץ כמות המוצר - גרסה משופרת"""
- # קודם כל נחפש בשדות הישירים
- quantity_sources = [
- item.get("weight"),
- item.get("unitOfMeasure", {}).get("names", {}).get("1"), # יחידת מידה
- item.get("numberOfItems"), # מספר יחידות
- item.get("original", {}).get("weight"),
- item.get("size"),
- item.get("original", {}).get("size"),
- item.get("volume"),
- item.get("original", {}).get("volume")
- ]
-
- # בדיקה מיוחדת עבור weight ו-unitOfMeasure
- weight = item.get("weight")
- unit_of_measure = item.get("unitOfMeasure", {}).get("names", {}).get("1")
-
- if weight and unit_of_measure:
- return f"{weight} {self._normalize_unit(unit_of_measure)}"
-
- # בדיקה לmultipack (כמו 4×1 ליטר)
- number_of_items = item.get("numberOfItems")
- if number_of_items and number_of_items > 1 and weight and unit_of_measure:
- return f"{number_of_items}×{weight} {self._normalize_unit(unit_of_measure)}"
-
- # בדיקה בשדות אחרים
- for quantity in quantity_sources:
- if quantity and str(quantity).strip():
- if quantity not in [weight, unit_of_measure, number_of_items]: # למנוע כפל
- return str(quantity).strip()
-
- # אם לא נמצאה כמות ישירה, מנסים לחלץ מהשם
- name = self._extract_product_name(item)
- extracted_quantity = self._extract_quantity_from_name(name)
- if extracted_quantity:
- return extracted_quantity
-
- # מנסים לחלץ מכל השמות הזמינים
- names_data = item.get("names", {})
- for lang_id, name_data in names_data.items():
- if isinstance(name_data, dict):
- for name_type in ["long", "short"]:
- if name_type in name_data and name_data[name_type]:
- extracted = self._extract_quantity_from_name(name_data[name_type])
- if extracted:
- return extracted
-
- return None
-
def _extract_brand(self, item):
- """חילוץ שם היצרן - גרסה משופרת"""
- # מנסים מספר מקורות ליצרן
+ """חילוץ שם היצרן"""
try:
- # בדיקה הדרגתית בmulti-level dictionaries
brand_data = item.get("brand", {})
if isinstance(brand_data, dict):
- # נסיון לגשת לשם היצרן
names_data = brand_data.get("names", {})
if isinstance(names_data, dict):
- # מחפשים בשפה העברית (1) או באנגלית (2)
for lang_id in ["1", "2"]:
if lang_id in names_data:
brand_name = names_data[lang_id]
if isinstance(brand_name, str) and brand_name.strip():
return brand_name.strip()
- # אם לא נמצא ב-names, מנסים ישירות ב-brand
if "name" in brand_data and brand_data["name"]:
brand_name = brand_data["name"]
if isinstance(brand_name, str) and brand_name.strip():
return brand_name.strip()
- except Exception as e:
+ except Exception:
pass
# מקורות נוספים ליצרן
@@ -384,100 +255,32 @@ def _extract_brand(self, item):
for brand in additional_brand_sources:
if brand and isinstance(brand, str) and brand.strip():
- brand_name = brand.strip()
- return brand_name
-
- # אם לא נמצא יצרן ישיר, מנסים לחלץ מהשם
- name = self._extract_product_name(item)
- extracted_brand = self._extract_brand_from_name(name)
- if extracted_brand:
- return extracted_brand
+ return brand.strip()
+ #
+ # # חילוץ יצרן מהשם באמצעות רשימה מובנית
+ # name = self._extract_product_name(item)
+ # extracted_brand = self._extract_brand_from_name(name)
+ # if extracted_brand:
+ # return extracted_brand
return None
- def _extract_brand_from_name(self, name):
- """חילוץ יצרן מתוך שם המוצר"""
- # רשימת יצרנים ידועים
- known_brands = [
- "אוסם", "תנובה", "יטבתה", "טעמן", "קוקה קולא", "פפסי",
- "נסטלה", "בישולי", "שטראוס", "עלית", "שופרסל", "סוגת",
- "מגדים", "אסם", "ברמן", "חרמון", "גלידות שטראוס", "דנונה",
- "מאיר בגל", "מעדן", "ויסוצקי", "עלי", "מילקי", "סימפליסימו",
- "בן עמי", "זוגלובק", "פיצה האט", "דומינו", "תל אביב", "מוצר ישראלי",
- "קוקה קולה", "coca cola", "cocacola", "ספרינג", "פריגת", "ג'אמפ",
- "רפאל'ס", "נובי", "פרינוק", "דניאלה", "אקטימל", "יופלה", "פרוט & ווג'",
- "פריטוב", "מטרנה", "בייבי ביס", "תנובה אלטרנטיב", "גמדים", "דנונה פרו"
- ]
-
- name_lower = name.lower()
- for brand in known_brands:
- if brand.lower() in name_lower:
- return brand
-
- return None
-
- def _extract_quantity_from_name(self, name):
- """חילוץ כמות מתוך שם המוצר - פונקציה משופרת"""
- import re
-
- patterns = [
- r'(\d+(?:\.\d+)?)\s*(ק"?ג|קילו|קילוגרם)', # קילוגרם
- r'(\d+(?:\.\d+)?)\s*(גר\'?|גרם)', # גרם
- r'(\d+(?:\.\d+)?)\s*(מ"?ל|מיליליטר)', # מיליליטר
- r'(\d+(?:\.\d+)?)\s*(ליטר|ל\')', # ליטר
- r'(\d+)\s*(יחידות?|יח\'?)', # יחידות
- r'(\d+(?:\.\d+)?)\s*(מ"?ג|מיליגרם)', # מיליגרם
- r'(\d+(?:\.\d+)?)\s*ג\'', # גרם בקיצור
- r'(\d+)\s*[×xX]\s*(\d+(?:\.\d+)?)\s*(מ"?ל|ליטר|גר\'?|גרם|ק"?ג)', # פורמט כמו 4×1 ליטר
- r'מארז\s*(\d+)\s*(יחידות?)', # מארז X יחידות
- r'(\d+)\s*חתיכות', # חתיכות
- r'(\d+)\s*עוגיות', # עוגיות
- r'(\d+)\s*כדורים', # כדורים
- ]
-
- for pattern in patterns:
- match = re.search(pattern, name, re.IGNORECASE)
- if match:
- groups = match.groups()
-
- if len(groups) == 3 and any(char in pattern.lower() for char in ['×', 'x']):
- # פורמט של כמות × יחידה
- count = groups[0]
- amount = groups[1]
- unit = groups[2]
- return f"{count}×{amount} {self._normalize_unit(unit)}"
- else:
- amount = groups[0]
- unit = groups[1] if len(groups) > 1 else "יחידות"
- return f"{amount} {self._normalize_unit(unit)}"
-
- return None
-
- def _normalize_unit(self, unit):
- """נירמול יחידות מידה"""
- if not unit:
- return "יחידות"
-
- unit_mapping = {
- 'ק"ג': 'קילוגרם',
- 'קילו': 'קילוגרם',
- 'קילוגרם': 'קילוגרם',
- 'גר\'': 'גרם',
- 'גרם': 'גרם',
- 'מ"ל': 'מ"ל',
- 'מיליליטר': 'מ"ל',
- 'ל\'': 'ליטר',
- 'ליטר': 'ליטר',
- 'יח\'': 'יחידות',
- 'יחידה': 'יחידות',
- 'יחידות': 'יחידות',
- 'מ"ג': 'מיליגרם',
- 'מג': 'מיליגרם',
- 'מיליגרם': 'מיליגרם',
- 'ג\'': 'גרם',
- 'חתיכות': 'יחידות',
- 'עוגיות': 'יחידות',
- 'כדורים': 'יחידות'
- }
-
- return unit_mapping.get(unit.lower(), unit)
\ No newline at end of file
+ # def _extract_brand_from_name(self, name):
+ # """חילוץ יצרן מתוך שם המוצר"""
+ # known_brands = [
+ # "אוסם", "תנובה", "יטבתה", "טעמן", "קוקה קולה", "פפסי",
+ # "נסטלה", "בישולי", "שטראוס", "עלית", "שופרסל", "סוגת",
+ # "מגדים", "אסם", "ברמן", "חרמון", "גלידות שטראוס", "דנונה",
+ # "מאיר בגל", "מעדן", "ויסוצקי", "עלי", "מילקי", "סימפליסימו",
+ # "בן עמי", "זוגלובק", "פיצה האט", "דומינו", "תל אביב", "מוצר ישראלי",
+ # "coca cola", "cocacola", "ספרינג", "פריגת", "ג'אמפ",
+ # "רפאל'ס", "נובי", "פרינוק", "דניאלה", "אקטימל", "יופלה", "פרוט & ווג'",
+ # "פריטוב", "מטרנה", "בייבי ביס", "תנובה אלטרנטיב", "גמדים", "דנונה פרו"
+ # ]
+ #
+ # name_lower = name.lower()
+ # for brand in known_brands:
+ # if brand.lower() in name_lower:
+ # return brand
+ #
+ # return None
\ No newline at end of file
diff --git a/scarpers/ybitan_scraper.py b/scarpers/ybitan_scraper.py
new file mode 100644
index 0000000..dcd48f7
--- /dev/null
+++ b/scarpers/ybitan_scraper.py
@@ -0,0 +1,330 @@
+import requests
+import urllib.parse
+import json
+from typing import List, Optional
+from models.product import Product
+from scarpers.base_scraper import StoreScraper
+from models.quantity_extractor import QuantityExtractor
+import time
+import random
+
+
+class YbitanScraper(StoreScraper):
+ def __init__(self):
+ super().__init__("https://www.ybitan.co.il")
+ self.api_base = "https://www.ybitan.co.il/v2"
+ self.session = requests.Session()
+
+ # יצירת מופע של חילוץ הכמויות
+ self.quantity_extractor = QuantityExtractor()
+
+ # Headers to mimic a real browser
+ self.session.headers.update({
+ 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
+ 'Accept': 'application/json, text/plain, */*',
+ 'Accept-Language': 'he-IL,he;q=0.9,en;q=0.8',
+ 'Accept-Encoding': 'gzip, deflate, br',
+ 'Connection': 'keep-alive',
+ 'Referer': 'https://www.ybitan.co.il/',
+ 'Origin': 'https://www.ybitan.co.il'
+ })
+
+ # פרמטרים קבועים לAPI
+ self.retailer_id = "1131"
+ self.branch_id = "1369"
+ self.app_id = "4"
+ self.language_id = "1" # עברית
+
+ def scrape_product(self, product_name):
+ """שיטה לשליפת מוצר בודד"""
+ return self.search_products(product_name)
+
+ def search_products(self, query: str, max_results: int = 10) -> List[Product]:
+ """חיפוש מוצרים באתר יינות ביתן באמצעות API"""
+ products = []
+
+ try:
+ print(f"מחפש מוצרים עבור: '{query}'")
+ encoded_query = urllib.parse.quote(query, safe='')
+ api_url = self.build_search_url(encoded_query, size=max_results)
+ print(f"API URL: {api_url}")
+
+ self.add_delay()
+
+ response = self.session.get(api_url, timeout=15)
+ response.raise_for_status()
+
+ print(f"קוד תגובה: {response.status_code}")
+ data = response.json()
+ products = self.parse_api_response(data)
+ print(f"נמצאו {len(products)} מוצרים")
+
+ except requests.RequestException as e:
+ print(f"שגיאה בבקשת API: {e}")
+ except json.JSONDecodeError as e:
+ print(f"שגיאה בפרסור JSON: {e}")
+ except Exception as e:
+ print(f"שגיאה כללית: {e}")
+
+ return products
+
+ def build_search_url(self, encoded_query: str, size: int = 10, from_idx: int = 0) -> str:
+ """בניית URL לחיפוש במוצרים"""
+ filters = {
+ "must": {
+ "exists": ["family.id", "family.categoriesPaths.id", "branch.regularPrice"],
+ "term": {
+ "branch.isActive": True,
+ "branch.isVisible": True
+ }
+ },
+ "mustNot": {
+ "term": {
+ "branch.regularPrice": 0,
+ "branch.isOutOfStock": True
+ }
+ }
+ }
+
+ filters_json = json.dumps(filters, separators=(',', ':'))
+ encoded_filters = urllib.parse.quote(filters_json, safe='')
+
+ url = (f"{self.api_base}/retailers/{self.retailer_id}/branches/{self.branch_id}/products"
+ f"?appId={self.app_id}"
+ f"&filters={encoded_filters}"
+ f"&from={from_idx}"
+ f"&isSearch=true"
+ f"&languageId={self.language_id}"
+ f"&query={encoded_query}"
+ f"&size={size}")
+
+ return url
+
+ def parse_api_response(self, data: dict) -> List[Product]:
+ """פרסור תגובת API וחילוץ מוצרים"""
+ products = []
+
+ try:
+ if 'data' in data and 'products' in data['data']:
+ products_data = data['data']['products']
+ elif 'products' in data:
+ products_data = data['products']
+ elif isinstance(data, list):
+ products_data = data
+ else:
+ print("מבנה תגובה לא מוכר")
+ print(f"מפתחות בתגובה: {list(data.keys()) if isinstance(data, dict) else 'לא dict'}")
+ return products
+
+ print(f"מעבד {len(products_data)} מוצרים מהAPI")
+
+ for item in products_data:
+ try:
+ product = self.parse_single_product(item)
+ if product:
+ products.append(product)
+ except Exception as e:
+ print(f"שגיאה בעיבוד מוצר: {e}")
+ continue
+
+ except Exception as e:
+ print(f"שגיאה בפרסור תגובת API: {e}")
+
+ return products
+
+ def parse_single_product(self, item: dict) -> Optional[Product]:
+ """פרסור מוצר בודד מתגובת API"""
+ try:
+ # חילוץ שם המוצר
+ name = self.extract_product_name(item)
+ if not name:
+ return None
+
+ # חילוץ מחיר
+ price = self.extract_price(item)
+ if price <= 0:
+ print(f"לא נמצא מחיר למוצר: {name}")
+ return None
+
+ # חילוץ מותג/יצרן
+ brand = self.extract_brand(item)
+
+ # חילוץ כמות באמצעות המחלקה החדשה
+ quantity = self.extract_quantity(item, name)
+
+ # יצירת מוצר
+ product = Product(
+ name=name,
+ price=price,
+ store="יינות ביתן",
+ quantity=quantity,
+ brand=brand
+ )
+
+ # חישוב מחיר יחסי באמצעות המחלקה החדשה
+ if quantity:
+ self.quantity_extractor.calculate_unit_price(product)
+
+ return product
+
+ except Exception as e:
+ print(f"שגיאה בפרסור מוצר: {e}")
+ return None
+
+ def extract_product_name(self, item: dict) -> str:
+ """חילוץ שם המוצר"""
+ name = ""
+
+ # ניסיון ראשון - localName
+ if 'localName' in item and item['localName']:
+ name = item['localName']
+
+ # ניסיון שני - names
+ elif 'names' in item and item['names']:
+ if '1' in item['names']: # עברית
+ if isinstance(item['names']['1'], dict):
+ name = item['names']['1'].get('short', '') or item['names']['1'].get('long', '')
+ else:
+ name = str(item['names']['1'])
+ elif 'he' in item['names']:
+ name = item['names']['he']
+
+ # ניסיון שלישי - name
+ elif 'name' in item and item['name']:
+ name = item['name']
+
+ # ניסיון רביעי - family name
+ elif 'family' in item and item['family'] and 'names' in item['family']:
+ if '1' in item['family']['names']:
+ name = item['family']['names']['1']
+
+ return name.strip() if name else ""
+
+ def extract_price(self, item: dict) -> float:
+ """חילוץ מחיר המוצר"""
+ price = 0.0
+
+ # ניסיון ראשון - branch data
+ if 'branch' in item and item['branch']:
+ branch_data = item['branch']
+ if 'regularPrice' in branch_data and branch_data['regularPrice']:
+ price = float(branch_data['regularPrice'])
+ elif 'price' in branch_data and branch_data['price']:
+ price = float(branch_data['price'])
+ elif 'currentPrice' in branch_data and branch_data['currentPrice']:
+ price = float(branch_data['currentPrice'])
+
+ # ניסיון שני - מחיר ישיר
+ if price <= 0:
+ for price_field in ['regularPrice', 'price', 'currentPrice']:
+ if price_field in item and item[price_field]:
+ try:
+ price = float(item[price_field])
+ if price > 0:
+ break
+ except (ValueError, TypeError):
+ continue
+
+ return price
+
+ def extract_brand(self, item: dict) -> str:
+ """חילוץ מותג/יצרן"""
+ brand = ""
+
+ # ניסיון ראשון - brand object
+ if 'brand' in item and item['brand']:
+ brand_data = item['brand']
+ if isinstance(brand_data, dict):
+ if 'names' in brand_data and '1' in brand_data['names']:
+ brand = brand_data['names']['1']
+ elif 'name' in brand_data:
+ brand = brand_data['name']
+ elif isinstance(brand_data, str):
+ brand = brand_data
+
+ # ניסיון שני - manufacturer
+ elif 'manufacturer' in item and item['manufacturer']:
+ manufacturer_data = item['manufacturer']
+ if isinstance(manufacturer_data, dict):
+ if 'names' in manufacturer_data and '1' in manufacturer_data['names']:
+ brand = manufacturer_data['names']['1']
+ elif 'name' in manufacturer_data:
+ brand = manufacturer_data['name']
+ elif isinstance(manufacturer_data, str):
+ brand = manufacturer_data
+
+ return brand.strip() if brand else ""
+
+ def extract_quantity(self, item: dict, product_name: str) -> str:
+ """
+ חילוץ כמות המוצר באמצעות המחלקה המשותפת
+ """
+ # ניסיון חילוץ מהשדות
+ weight = item.get('weight')
+ unit_of_measure = None
+
+ # חילוץ יחידת מידה
+ if 'unitOfMeasure' in item and item['unitOfMeasure']:
+ unit_data = item['unitOfMeasure']
+ if isinstance(unit_data, dict):
+ if 'names' in unit_data and '1' in unit_data['names']:
+ unit_of_measure = unit_data['names']['1']
+ elif 'name' in unit_data:
+ unit_of_measure = unit_data['name']
+ else:
+ unit_of_measure = str(unit_data)
+ else:
+ unit_of_measure = str(unit_data)
+
+ quantity_field = item.get('quantity')
+ size_field = item.get('size')
+
+ # ניסיון חילוץ מהשדות
+ quantity = self.quantity_extractor.extract_quantity_from_fields(
+ weight=weight,
+ unit_of_measure=unit_of_measure,
+ quantity_field=quantity_field,
+ size_field=size_field
+ )
+
+ # אם לא נמצא בשדות, נחפש בשם המוצר
+ if not quantity:
+ quantity = self.quantity_extractor.extract_quantity_from_name(product_name)
+
+ return quantity
+
+ def get_product_data_for_db(self, product: Product) -> dict:
+ """
+ הכנת נתוני המוצר עבור מסד הנתונים
+
+ Returns:
+ dict: נתונים מסודרים עבור הטבלה
+ """
+ weight, weight_unit = self.quantity_extractor.get_weight_and_unit_for_db(product.quantity)
+
+ return {
+ 'name': product.name[:50], # הגבלת אורך לפי הטבלה
+ 'description': f"{product.name} - {product.store}",
+ 'brand_id': None, # יש לקשר עם טבלת המותגים
+ 'weight': weight,
+ 'weight_unit': weight_unit,
+ 'price': product.price,
+ 'unit_price': getattr(product, 'unit_price', None),
+ 'unit_type': getattr(product, 'unit_type', None),
+ 'store': product.store
+ }
+
+ def get_branch_info(self) -> dict:
+ """קבלת מידע על הסניף"""
+ try:
+ url = f"{self.api_base}/retailers/{self.retailer_id}/branches"
+ response = self.session.get(url, timeout=10)
+ response.raise_for_status()
+ return response.json()
+ except Exception as e:
+ print(f"שגיאה בקבלת מידע סניפים: {e}")
+ return {}
+
+ def add_delay(self):
+ """הוספת עיכוב אקראי למניעת חסימה"""
+ time.sleep(random.uniform(1.0, 2.5))
\ No newline at end of file
diff --git a/stores/__init__.py b/stores/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/stores/rami_levy_store.py b/stores/rami_levy_store.py
new file mode 100644
index 0000000..7186c64
--- /dev/null
+++ b/stores/rami_levy_store.py
@@ -0,0 +1,24 @@
+from core.base_store import BaseStore
+from selenium.webdriver.common.by import By
+from selenium.webdriver.support.ui import WebDriverWait
+from selenium.webdriver.support import expected_conditions as EC
+
+class RamiLevyStore(BaseStore):
+
+ def get_login_url(self):
+ return "https://www.rami-levy.co.il/he"
+
+ def get_username_selector(self):
+ return "input#email"
+
+ def get_password_selector(self):
+ return "input#password"
+
+ def get_submit_selector(self):
+ return "button.focus-item.online-full-btn[aria-label='כניסה']"
+
+ def open_login_modal(self):
+ login_button_selector = "div#login-user"
+ print("⌛ ממתין שהכפתור התחברות יהיה קליקבילי...")
+ self.wait_and_click(login_button_selector)
+ print("✅ נלחץ על כפתור ההתחברות")
\ No newline at end of file
diff --git a/stores/shufersal_store.py b/stores/shufersal_store.py
new file mode 100644
index 0000000..6c49830
--- /dev/null
+++ b/stores/shufersal_store.py
@@ -0,0 +1,285 @@
+# shufersal_store.py
+from selenium.webdriver.common.by import By
+from selenium.webdriver.support.ui import WebDriverWait
+from selenium.webdriver.support import expected_conditions as EC
+from core.online_store import OnlineStore
+from selenium.webdriver.common.keys import Keys
+from selenium.common.exceptions import TimeoutException, NoSuchElementException
+import traceback
+
+class ShufersalStore(OnlineStore):
+ def login(self, username, password):
+ try:
+ self.driver.get("https://www.shufersal.co.il/")
+ login_button = WebDriverWait(self.driver, 10).until(
+ EC.element_to_be_clickable((By.ID, "header-login-button"))
+ )
+ login_button.click()
+
+ email_input = WebDriverWait(self.driver, 10).until(
+ EC.presence_of_element_located((By.ID, "j_username"))
+ )
+ email_input.send_keys(username)
+
+ password_input = self.driver.find_element(By.ID, "j_password")
+ password_input.send_keys(password)
+
+ submit_button = self.driver.find_element(By.ID, "loginFormSubmit")
+ submit_button.click()
+
+ WebDriverWait(self.driver, 10).until(
+ EC.presence_of_element_located((By.ID, "header-my-account-button"))
+ )
+ print("התחברות הצליחה")
+ except Exception as e:
+ print("שגיאה בהתחברות:", e)
+ traceback.print_exc()
+
+
+ # def search_item(self, item_name):
+ # self.driver.get("https://www.shufersal.co.il/") # כתובת חנות בפועל
+ # search_box = WebDriverWait(self.driver, 10).until(
+ # EC.presence_of_element_located((By.ID, "js-site-search-input"))
+ # )
+ # search_box.clear()
+ # search_box.send_keys(item_name)
+ # search_box.submit()
+
+ def search_item(self, item_name):
+ print(f"מחפש: {item_name}")
+ self.driver.get("https://www.shufersal.co.il/")
+
+ try:
+ # המתנה שהדף יטען לחלוטין
+ WebDriverWait(self.driver, 15).until(
+ EC.presence_of_element_located((By.TAG_NAME, "body"))
+ )
+
+ # חיפוש תיבת החיפוש והמתנה שתהיה לחיצה
+ search_box = WebDriverWait(self.driver, 15).until(
+ EC.element_to_be_clickable((By.ID, "js-site-search-input"))
+ )
+
+ search_box.clear()
+ search_box.send_keys(item_name)
+ search_box.send_keys(Keys.ENTER)
+
+ # המתנה שתוצאות החיפוש יטענו - מתבסס על מה שמצאנו
+ WebDriverWait(self.driver, 20).until(
+ lambda driver: (
+ "search" in driver.current_url and
+ len(driver.find_elements(By.CSS_SELECTOR, ".tile")) > 0
+ )
+ )
+ print("תוצאות החיפוש נטענו בהצלחה")
+
+ except TimeoutException:
+ print("בעיה במציאת תיבת החיפוש או תוצאות החיפוש לא נטענו")
+ raise
+
+ def add_to_cart(self, item_name):
+ try:
+ # חיפוש המוצר
+ self.search_item(item_name)
+
+ print("מחפש מוצרים בתוצאות...")
+
+ # לפי הדיבאג שלך - יש 29 tiles, בואי נחפש את אלה שהם מוצרים אמיתיים
+ tiles = WebDriverWait(self.driver, 10).until(
+ EC.presence_of_all_elements_located((By.CSS_SELECTOR, ".tile"))
+ )
+
+ print(f"נמצאו {len(tiles)} tiles")
+
+ # נמצא tile שמכיל מוצר (לא מלאי חסר וכו')
+ product_tile = None
+ for tile in tiles:
+ try:
+ # בודק שהוא לא חסר במלאי או הודעה אחרת
+ tile_html = tile.get_attribute('outerHTML')
+ if ('miglog-prod-inStock' in tile_html and
+ 'notOverlay' in tile_html and
+ len(tile.find_elements(By.CSS_SELECTOR, "button, .add")) > 0):
+ product_tile = tile
+ print("נמצא מוצר מתאים")
+ break
+ except Exception:
+ continue
+
+ if not product_tile:
+ print("לא נמצא מוצר זמין. בודק את כל ה-tiles:")
+ for i, tile in enumerate(tiles[:5]): # רק 5 הראשונים לדיבאג
+ print(f"Tile {i + 1}: {tile.get_attribute('outerHTML')[:200]}...")
+ return
+
+ # גלילה לאלמנט והמתנה שיהיה גלוי
+ print("מבצע גלילה למוצר...")
+ self.driver.execute_script(
+ "arguments[0].scrollIntoView({behavior: 'smooth', block: 'center'});",
+ product_tile
+ )
+
+ # המתנה שהאלמנט יהיה גלוי לחלוטין
+ WebDriverWait(self.driver, 10).until(
+ EC.visibility_of(product_tile)
+ )
+
+ # חיפוש כפתור הוספה - לפי מבנה שופרסל
+ add_button_selectors = [
+ ".js-add-to-cart",
+ "button.js-add-to-cart",
+ ".add-to-cart",
+ "button[data-add-to-cart]",
+ ".btn-add-cart",
+ ".addToCart",
+ "button.addToCart"
+ ]
+
+ add_button = None
+ for selector in add_button_selectors:
+ try:
+ add_button = product_tile.find_element(By.CSS_SELECTOR, selector)
+ if add_button.is_displayed():
+ print(f"נמצא כפתור הוספה: {selector}")
+ break
+ except NoSuchElementException:
+ continue
+
+ # אם לא נמצא כפתור ספציפי, נחפש כל כפתור
+ if not add_button:
+ try:
+ buttons = product_tile.find_elements(By.TAG_NAME, "button")
+ for btn in buttons:
+ if (btn.is_displayed() and
+ ("הוסף" in btn.text or "add" in btn.text.lower() or
+ "הוספה" in btn.get_attribute('title') or
+ "cart" in btn.get_attribute('class').lower())):
+ add_button = btn
+ print(f"נמצא כפתור עם טקסט/class: {btn.text} / {btn.get_attribute('class')}")
+ break
+ except Exception:
+ pass
+
+ if add_button:
+ try:
+ # המתנה שהכפתור יהיה לחיץ
+ WebDriverWait(self.driver, 10).until(
+ EC.element_to_be_clickable(add_button)
+ )
+
+ # ניסיון לחיצה רגילה
+ add_button.click()
+ print(f"המוצר '{item_name}' נוסף לעגלה!")
+
+ # המתנה לאישור הוספה
+ try:
+ WebDriverWait(self.driver, 5).until(
+ lambda driver: (
+ "נוסף" in driver.page_source or
+ "added" in driver.page_source.lower() or
+ len(driver.find_elements(By.CSS_SELECTOR,
+ ".cart-count, .notification, .success, .added, .cart-badge")) > 0
+ )
+ )
+ print("אישור הוספה לעגלה התקבל!")
+ self.check_and_replace_with_promo()
+ except TimeoutException:
+ print("לא התקבל אישור ברור, אבל הלחיצה בוצעה")
+
+ except Exception as click_error:
+ print(f"בעיה בלחיצה רגילה: {click_error}")
+ try:
+ # ניסיון לחיצה עם JavaScript
+ self.driver.execute_script("arguments[0].click();", add_button)
+ print(f"המוצר '{item_name}' נוסף לעגלה (JavaScript)!")
+
+ # המתנה לאישור הוספה
+ try:
+ WebDriverWait(self.driver, 5).until(
+ lambda driver: (
+ "נוסף" in driver.page_source or
+ "added" in driver.page_source.lower() or
+ len(driver.find_elements(By.CSS_SELECTOR,
+ ".cart-count, .notification, .success, .added, .cart-badge")) > 0
+ )
+ )
+ print("אישור הוספה לעגלה התקבל!")
+ self.check_and_replace_with_promo()
+
+ except TimeoutException:
+ print("לא התקבל אישור ברור, אבל הלחיצה בוצעה")
+
+ except Exception as js_error:
+ print(f"כישלון גם בלחיצה עם JavaScript: {js_error}")
+ else:
+ print("לא נמצא כפתור הוספה לעגלה")
+ print("HTML של המוצר לדיבאג:")
+ print(product_tile.get_attribute('outerHTML')[:500])
+
+ except Exception as e:
+ print(f"שגיאה כללית בהוספת מוצר: {e}")
+ traceback.print_exc()
+
+ def check_and_replace_with_promo(self):
+ try:
+ print("בודק אם קיימת הודעה חוסמת עם כפתור סגירה...")
+
+ try:
+ close_button = WebDriverWait(self.driver, 5).until(
+ EC.element_to_be_clickable((By.CSS_SELECTOR, "button.btnClose"))
+ )
+ print("נמצא כפתור X לסגירת ההודעה – לוחץ עליו...")
+ close_button.click()
+
+ # המתנה קלה לוודא סגירה
+ WebDriverWait(self.driver, 5).until_not(
+ EC.presence_of_element_located((By.CSS_SELECTOR, "button.btnClose"))
+ )
+ print("ההודעה נסגרה בהצלחה")
+
+ except TimeoutException:
+ print("לא נמצאה הודעה חוסמת – ממשיך כרגיל")
+
+ print("לוחץ על כפתור העגלה...")
+
+ cart_icon = WebDriverWait(self.driver, 10).until(
+ EC.element_to_be_clickable((By.CSS_SELECTOR, 'img[alt="הסל שלי"]'))
+ )
+ self.driver.execute_script("arguments[0].scrollIntoView(true);", cart_icon)
+ cart_icon.click()
+ print("נלחץ כפתור העגלה")
+
+ WebDriverWait(self.driver, 10).until(
+ EC.presence_of_element_located((By.CSS_SELECTOR, "div.miglog-prod-promo"))
+ )
+ print("העגלה נטענה – מחפש 'החלף וחסוך'")
+
+ promo_divs = self.driver.find_elements(By.CSS_SELECTOR, "div.miglog-prod-promo")
+ clicked = False
+ for div in promo_divs:
+ try:
+ span = div.find_element(By.CSS_SELECTOR, "span")
+ if "החלף וחסוך" in span.text:
+ self.driver.execute_script("arguments[0].scrollIntoView(true);", div)
+ WebDriverWait(self.driver, 5).until(EC.element_to_be_clickable(div))
+ div.click()
+ print("נלחץ כפתור 'החלף וחסוך'")
+ clicked = True
+ break
+ except NoSuchElementException:
+ continue
+
+ if not clicked:
+ print("לא נמצא כפתור 'החלף וחסוך'")
+ return
+
+ print("ממתין לכפתור 'לבחירה'...")
+ choose_button = WebDriverWait(self.driver, 10).until(
+ EC.element_to_be_clickable((By.CSS_SELECTOR, "a.js-replacing-btn"))
+ )
+ self.driver.execute_script("arguments[0].scrollIntoView(true);", choose_button)
+ choose_button.click()
+ print("נלחץ כפתור 'לבחירה' להצגת מוצרים חלופיים")
+
+ except Exception as e:
+ print(f"שגיאה כללית במהלך החלפה: {e}")