""" Модуль для автоматизации работы с браузером через Selenium """ import time import logging import os import shutil import sys import platform import subprocess from pathlib import Path from selenium import webdriver from selenium.webdriver.common.by import By from selenium.webdriver.firefox.options import Options as FirefoxOptions from selenium.webdriver.firefox.service import Service as FirefoxService from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support import expected_conditions as EC try: from webdriver_manager.firefox import GeckoDriverManager HAS_WEBDRIVER_MANAGER = True except ImportError: HAS_WEBDRIVER_MANAGER = False class BrowserService: """Сервис для автоматизации работы с браузером.""" def __init__(self, download_dir, firefox_binary_path=None): """Инициализирует браузер с указанными параметрами.""" self.download_dir = download_dir self.firefox_binary_path = firefox_binary_path or self._find_firefox_path() self.driver = None self.wait = None self._setup_driver() @staticmethod def _find_firefox_path(): """Автоматически находит путь к Firefox для текущей ОС.""" system = platform.system() # 1. Проверка через shutil.which (Поиск в PATH) - работает и на Win и на Linux path_from_which = shutil.which("firefox") if path_from_which: # На Windows shutil.which может вернуть .EXE, проверим существует ли if os.path.exists(path_from_which): logging.info(f"✅ Firefox найден в PATH: {path_from_which}") return path_from_which possible_paths = [] if system == "Windows": # Windows пути - разные места установки Firefox possible_paths = [ r"C:\Program Files\Mozilla Firefox\firefox.exe", r"C:\Program Files (x86)\Mozilla Firefox\firefox.exe", os.path.expandvars(r"%PROGRAMFILES%\Mozilla Firefox\firefox.exe"), os.path.expandvars(r"%PROGRAMFILES(X86)%\Mozilla Firefox\firefox.exe"), os.path.expandvars(r"%LOCALAPPDATA%\Mozilla Firefox\firefox.exe"), os.path.expandvars(r"%APPDATA%\Mozilla Firefox\firefox.exe"), ] # Пытаемся найти через реестр (Windows) try: import winreg try: reg_path = r"SOFTWARE\Mozilla\Mozilla Firefox" with winreg.OpenKey(winreg.HKEY_LOCAL_MACHINE, reg_path) as key: install_dir = winreg.QueryValueEx(key, "InstallDir")[0] possible_paths.insert(0, os.path.join(install_dir, "firefox.exe")) except: pass except: pass elif system == "Darwin": # macOS пути possible_paths = [ "/Applications/Firefox.app/Contents/MacOS/firefox", "/usr/local/bin/firefox", ] else: # Linux # Linux пути possible_paths = [ "/usr/bin/firefox", "/usr/bin/firefox-esr", "/snap/bin/firefox", "/usr/local/bin/firefox", "/opt/firefox/firefox", "/usr/lib/firefox/firefox", ] # Ищем Firefox в возможных местах for path in possible_paths: if path and os.path.exists(path): logging.info(f"✅ Firefox найден: {path}") return path # Последний шанс: subprocess (если shutil.which пропустил что-то специфичное) try: cmd = ["where", "firefox.exe"] if system == "Windows" else ["which", "firefox"] result = subprocess.run(cmd, capture_output=True, text=True, timeout=5) if result.returncode == 0 and result.stdout.strip(): firefox_path = result.stdout.strip().split('\n')[0] if os.path.exists(firefox_path): logging.info(f"✅ Firefox найден через команду search: {firefox_path}") return firefox_path except: pass logging.warning(f"⚠️ Firefox не найден в стандартных местах для {system}") return None def _setup_driver(self): """Настраивает и запускает веб-драйвер.""" try: firefox_options = FirefoxOptions() # ===== ОПЦИИ ЗАГРУЗКИ ===== firefox_options.set_preference("browser.download.folderList", 2) firefox_options.set_preference("browser.download.dir", self.download_dir) firefox_options.set_preference("browser.download.useDownloadDir", True) firefox_options.set_preference("browser.helperApps.neverAsk.saveToDisk", "application/octet-stream") # Устанавливаем путь к Firefox только если он найден if self.firefox_binary_path: logging.info(f" ✅ Используется Firefox: {self.firefox_binary_path}") firefox_options.binary_location = self.firefox_binary_path else: logging.info(" ⚠️ Firefox путь не определен, буду использовать системный поиск...") logging.info("🚀 Запуск Firefox драйвера с ВИДИМЫМ окном...") # Попытка 1: Использовать webdriver-manager (самый надежный способ) if HAS_WEBDRIVER_MANAGER and not self.driver: try: logging.info(" Попытка 1: webdriver-manager...") service = FirefoxService(GeckoDriverManager().install()) self.driver = webdriver.Firefox(service=service, options=firefox_options) logging.info(" ✅ Успешно запущено через webdriver-manager") except Exception as e: logging.warning(f" webdriver-manager не сработал: {e}") self.driver = None # Попытка 2: Локальный драйвер в папке проекта if not self.driver: logging.info(" Попытка 2: Поиск локального geckodriver...") system = platform.system() driver_filename = "geckodriver.exe" if system == "Windows" else "geckodriver" possible_driver_paths = [ os.path.join(os.getcwd(), driver_filename), os.path.join(os.getcwd(), "drivers", driver_filename), os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", driver_filename), # В корне проекта ] for dp in possible_driver_paths: if os.path.exists(dp): try: logging.info(f" ⚠️ Нашел локальный драйвер: {dp}") service = FirefoxService(executable_path=dp) self.driver = webdriver.Firefox(service=service, options=firefox_options) logging.info(" ✅ Успешно запущено через локальный драйвер") break except Exception as e: logging.warning(f" Ошибка локального драйвера {dp}: {e}") # Попытка 3: Прямой запуск (Selenium сам найдет geckodriver в PATH) if not self.driver: try: logging.info(" Попытка 3: Прямой запуск (Selenium ищет geckodriver в PATH)...") self.driver = webdriver.Firefox(options=firefox_options) logging.info(" ✅ Успешно запущено через системный PATH") except Exception as e: logging.warning(f" Прямой запуск не сработал: {e}") self.driver = None if not self.driver: raise Exception("Не удалось инициализировать Firefox драйвер ни одним способом") # Даем браузеру время открыться time.sleep(2) self.driver.set_page_load_timeout(300) self.wait = WebDriverWait(self.driver, 10) logging.info("✅ Браузер открыт и готов к работе.") except Exception as e: logging.error(f"❌ Ошибка при настройке драйвера: {e}") logging.error(f"\n Решения:") logging.error(f" 1. Убедитесь, что Firefox установлен") logging.error(f" 2. Скачайте geckodriver для вашей ОС и положите рядом с программой") logging.error(f" 3. Ссылка на драйвер: https://github.com/mozilla/geckodriver/releases") time.sleep(2) self.driver.set_page_load_timeout(300) self.wait = WebDriverWait(self.driver, 10) logging.info("✅ Браузер открыт и готов к работе.") except Exception as e: logging.error(f"❌ Ошибка при настройке драйвера: {e}") logging.error(f"\n Решения:") logging.error(f" 1. Убедитесь, что Firefox установлен") logging.error(f" 2. На Windows: установите Firefox с https://mozilla.org/firefox") raise def open_page(self, url): """Открывает страницу по URL.""" try: logging.info(f"🌐 Переход на страницу: {url}") self.driver.get(url) logging.info("✅ Страница загружена.") time.sleep(2) except Exception as e: logging.error(f"❌ Ошибка при открытии страницы: {e}") raise def wait_page_load(self, seconds=5): """Ждет загрузки страницы.""" logging.info(f"⏳ Ожидание {seconds} сек...") time.sleep(seconds) def click_button_by_text(self, button_text, exact_match=False, timeout=10): """ Ищет и нажимает кнопку (или любой элемент) по тексту, используя продвинутый метод поиска. Args: button_text (str): Текст кнопки для поиска exact_match (bool): Игнорируется в новой реализации (используется умный поиск) timeout (int): Время ожидания в секундах Returns: bool: True если кнопка найдена и нажата, False иначе """ logging.info(f"🔎 Ищу кнопку/элемент с текстом: '{button_text}' (Robust)") return self.select_tab_by_multiple_methods(button_text, timeout) def _find_button_by_text(self, button_text, exact_match=False, timeout=10): # Этот метод больше не используется основным кодом, но оставлен для совместимости # или если старый метод понадобится. pass def _text_matches(self, element_text, search_text, exact_match): """Проверяет совпадение текста.""" element_text_clean = element_text.lower().strip() if exact_match: return element_text_clean == search_text else: return search_text in element_text_clean def _is_clickable(self, element): """Проверяет, можно ли кликнуть на элемент.""" try: return element.is_displayed() and element.is_enabled() except: return False def upload_file(self, file_path, selector='input[type="file"]', by=By.CSS_SELECTOR): """Загружает файл через input.""" try: logging.info(f"📤 Загружаю файл: {Path(file_path).name}") # Попытка кликнуть на область загрузки try: drop_area = self.driver.find_element(By.CSS_SELECTOR, '.upload-area, .drop-zone, [data-upload]') drop_area.click() time.sleep(1) logging.info(" Найдена область загрузки") except: pass file_input = self.wait.until(EC.presence_of_element_located((by, selector))) file_input.send_keys(file_path) logging.info(f"✅ Файл загружен: {Path(file_path).name}") time.sleep(1) return True except Exception as e: logging.error(f"❌ Ошибка при загрузке файла {Path(file_path).name}: {e}") return False def close(self): """Закрывает браузер.""" if self.driver: self.driver.quit() logging.info("🛑 Браузер закрыт.") def get_page_title(self): """Возвращает заголовок страницы.""" return self.driver.title def open_new_tab(self, url): """Открывает новую вкладку с заданным URL.""" try: logging.info(f"📑 Открываю новую вкладку: {url}") # Открываем новую вкладку self.driver.execute_script("window.open(arguments[0], '_blank');", url) # Переключаемся на последнюю открытую вкладку self.driver.switch_to.window(self.driver.window_handles[-1]) time.sleep(2) logging.info("✅ Новая вкладка открыта и активна.") return True except Exception as e: logging.error(f"❌ Ошибка при открытии новой вкладки: {e}") return False def close_current_tab(self): """Закрывает текущую вкладку и переходит на предыдущую.""" try: logging.info("📑 Метод close_current_tab вызван, но вкладки не будут закрываться.") # Удаляем логику закрытия вкладок return True except Exception as e: logging.error(f"❌ Ошибка при попытке закрыть вкладку: {e}") return False def get_tabs_count(self): """Возвращает количество открытых вкладок.""" return len(self.driver.window_handles) def select_tab_by_name(self, tab_name): """Выбирает вкладку на странице по её названию.""" try: logging.info(f"🔍 Поиск вкладки с названием: {tab_name}") # Ищем элемент вкладки по тексту tab_element = self.wait.until( EC.presence_of_element_located((By.XPATH, f"//a[text()='{tab_name}']")) ) tab_element.click() logging.info(f"✅ Вкладка '{tab_name}' выбрана.") return True except Exception as e: logging.error(f"❌ Ошибка при выборе вкладки '{tab_name}': {e}") return False def select_tab_by_text_or_name(self, target_text, timeout=30): """Ищет и выбирает вкладку по тексту или названию, делая повторные попытки. Args: target_text (str): Текст или название вкладки для поиска. timeout (int): Общее время ожидания в секундах. Returns: bool: True, если вкладка найдена и выбрана, иначе False. """ end_time = time.time() + timeout while time.time() < end_time: try: logging.info(f"🔍 Поиск вкладки с текстом или названием: {target_text}") # Ищем элемент вкладки по тексту tab_element = self.wait.until( EC.presence_of_element_located((By.XPATH, f"//*[text()='{target_text}']")) ) self.driver.execute_script("arguments[0].scrollIntoView(true);", tab_element) time.sleep(0.5) tab_element.click() logging.info(f"✅ Вкладка с текстом '{target_text}' выбрана.") return True except Exception as e: logging.warning(f"⚠️ Вкладка с текстом '{target_text}' не найдена, повторная попытка...") time.sleep(1) # Ждем перед повторной попыткой logging.error(f"❌ Не удалось найти вкладку с текстом '{target_text}' за {timeout} секунд.") return False def set_tabs(self, tabs): """Устанавливает массив с названиями вкладок.""" self.tabs = tabs def select_tab_from_list(self, tab_index): """Выбирает вкладку из массива по индексу. Args: tab_index (int): Индекс вкладки в массиве. Returns: bool: True, если вкладка выбрана успешно, иначе False. """ try: if not hasattr(self, 'tabs') or not self.tabs: logging.error("❌ Массив вкладок не установлен или пуст.") return False if tab_index < 0 or tab_index >= len(self.tabs): logging.error("❌ Индекс вкладки выходит за пределы массива.") return False tab_name = self.tabs[tab_index] logging.info(f"🔍 Выбор вкладки с индексом {tab_index}: {tab_name}") return self.select_tab_by_name(tab_name) except Exception as e: logging.error(f"❌ Ошибка при выборе вкладки по индексу {tab_index}: {e}") return False def select_tab_by_multiple_methods(self, tab_text, timeout=15): """Пытается выбрать вкладку на странице перебором всех элементов. Args: tab_text (str): Текст вкладки для поиска timeout (int): Общее время ожидания в секундах Returns: bool: True, если вкладка найдена и выбрана, иначе False """ from selenium.webdriver.common.action_chains import ActionChains end_time = time.time() + timeout attempt = 0 # Нормализуем текст для поиска tab_text_normalized = tab_text.strip() logging.info(f"🔍 Начинаю поиск вкладки: '{tab_text}' (v2)") while time.time() < end_time: attempt += 1 logging.info(f" Попытка {attempt}: поиск элементов через JS...") try: # Ищем и кликаем через JavaScript в одном скрипте script = """ var searchText = arguments[0].toLowerCase(); var allElements = document.querySelectorAll('a, div, span, li, button, td, th'); var foundElements = []; for (var i = 0; i < allElements.length; i++) { var el = allElements[i]; if (el.offsetParent === null) continue; // Skip hidden var elementText = el.innerText || el.textContent || ''; var cleanText = elementText.trim().toLowerCase(); // Strict match or contains if short if (cleanText === searchText || (cleanText.includes(searchText) && cleanText.length < searchText.length + 50)) { foundElements.push(el); } } return foundElements; """ candidates = self.driver.execute_script(script, tab_text_normalized) if not candidates: logging.info(" ⚠️ JS поиск ничего не дал, пробую XPath...") try: xpath = f"//*[contains(text(), '{tab_text}')]" candidates = self.driver.find_elements(By.XPATH, xpath) except: pass logging.info(f" Найдено кандидатов: {len(candidates)}") for i, element in enumerate(candidates): try: if not element.is_displayed(): continue # Подсветка self.driver.execute_script("arguments[0].style.border='3px solid red';", element) time.sleep(0.5) logging.info(f" 👉 Кандидат #{i+1} (<{element.tag_name}>): '{element.text[:40]}'") # 1. ActionChains try: ActionChains(self.driver).move_to_element(element).click().perform() logging.info(" ✅ ActionChains click OK") self.driver.execute_script("arguments[0].style.border='';", element) return True except Exception as e1: logging.warning(f" ActionChains fail: {e1}") # 2. Native Click try: element.click() logging.info(" ✅ Element click OK") self.driver.execute_script("arguments[0].style.border='';", element) return True except Exception as e2: logging.warning(f" Native click fail: {e2}") # 3. JS Click self.driver.execute_script("arguments[0].click();", element) logging.info(" ✅ JS click OK") self.driver.execute_script("arguments[0].style.border='';", element) return True except Exception as e: logging.error(f" Ошибка обработки элемента: {e}") try: self.driver.execute_script("arguments[0].style.border='';", element) except: pass time.sleep(1) except Exception as e: logging.warning(f" ✗ Ошибка при попытке {attempt}: {e}") time.sleep(1) logging.error(f"❌ Не удалось найти и кликнуть на вкладку '{tab_text}' за {timeout} секунд") return False