From 869911dd5c86b088e51776881c96d801bbb4449a Mon Sep 17 00:00:00 2001 From: ChayaEinhoren Date: Mon, 30 Jun 2025 16:05:14 +0300 Subject: [PATCH] Add Rami Levy login and implement product search + add to cart functionality for Shufersal --- .gitignore | 3 + .idea/food_scraper_project.iml | 2 +- .idea/misc.xml | 5 +- core/base_store.py | 66 ++++++ core/online_store.py | 70 ++++++ main.py | 338 ++++++++++++++++++++++++++-- models/product.py | 331 +++++++++++++++++++++++++--- models/quantity_extractor.py | 237 ++++++++++++++++++++ scarpers/shukcity_scraper.py | 391 ++++++++------------------------- scarpers/ybitan_scraper.py | 330 ++++++++++++++++++++++++++++ stores/__init__.py | 0 stores/rami_levy_store.py | 24 ++ stores/shufersal_store.py | 285 ++++++++++++++++++++++++ 13 files changed, 1739 insertions(+), 343 deletions(-) create mode 100644 core/base_store.py create mode 100644 core/online_store.py create mode 100644 models/quantity_extractor.py create mode 100644 scarpers/ybitan_scraper.py create mode 100644 stores/__init__.py create mode 100644 stores/rami_levy_store.py create mode 100644 stores/shufersal_store.py 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}")