fix
This commit is contained in:
380
services/browser_service.py
Normal file
380
services/browser_service.py
Normal file
@@ -0,0 +1,380 @@
|
||||
"""
|
||||
Модуль для автоматизации работы с браузером через Selenium
|
||||
"""
|
||||
import time
|
||||
import logging
|
||||
import os
|
||||
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()
|
||||
found_path = None
|
||||
|
||||
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"),
|
||||
]
|
||||
|
||||
# Пытаемся найти через реестр (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 = [
|
||||
"/snap/bin/firefox",
|
||||
"/usr/bin/firefox",
|
||||
"/usr/local/bin/firefox",
|
||||
"/opt/firefox/firefox",
|
||||
]
|
||||
|
||||
# Ищем Firefox в возможных местах
|
||||
for path in possible_paths:
|
||||
if path and os.path.exists(path):
|
||||
logging.info(f"✅ Firefox найден: {path}")
|
||||
return path
|
||||
|
||||
# Пытаемся найти через which/where команды
|
||||
try:
|
||||
if system == "Windows":
|
||||
result = subprocess.run(["where", "firefox.exe"], capture_output=True, text=True, timeout=5)
|
||||
else:
|
||||
result = subprocess.run(["which", "firefox"], capture_output=True, text=True, timeout=5)
|
||||
|
||||
if result.returncode == 0 and result.stdout.strip():
|
||||
firefox_path = result.stdout.strip().split('\n')[0]
|
||||
logging.info(f"✅ Firefox найден через {system} 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")
|
||||
|
||||
# ===== ОТКЛЮЧАЕМ HEADLESS (окно ДОЛЖНО быть видимым) =====
|
||||
# Убедимся, что headless режим НЕ установлен
|
||||
# (это заставит браузер открыться в нормальном режиме)
|
||||
|
||||
# Устанавливаем путь к 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:
|
||||
try:
|
||||
logging.info(" Попытка 1: webdriver-manager...")
|
||||
service = FirefoxService(GeckoDriverManager().install())
|
||||
self.driver = webdriver.Firefox(service=service, options=firefox_options)
|
||||
except Exception as e:
|
||||
logging.warning(f" webdriver-manager не сработал: {e}")
|
||||
self.driver = None
|
||||
|
||||
# Попытка 2: Прямой запуск (Selenium сам найдет geckodriver в PATH)
|
||||
if not self.driver:
|
||||
try:
|
||||
logging.info(" Попытка 2: Прямой запуск (Selenium ищет geckodriver в PATH)...")
|
||||
self.driver = webdriver.Firefox(options=firefox_options)
|
||||
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. На 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 иначе
|
||||
"""
|
||||
try:
|
||||
logging.info(f"🔎 Ищу кнопку: '{button_text}'")
|
||||
button = self._find_button_by_text(button_text, exact_match, timeout)
|
||||
|
||||
if button:
|
||||
logging.info(f"✅ Кнопка найдена, нажимаю...")
|
||||
# Скролл к кнопке если нужно
|
||||
self.driver.execute_script("arguments[0].scrollIntoView(true);", button)
|
||||
time.sleep(0.5)
|
||||
button.click()
|
||||
logging.info(f"✅ Кнопка '{button_text}' нажата успешно.")
|
||||
time.sleep(1)
|
||||
return True
|
||||
else:
|
||||
logging.warning(f"⚠️ Кнопка '{button_text}' не найдена.")
|
||||
return False
|
||||
except Exception as e:
|
||||
logging.error(f"❌ Ошибка при нажатии кнопки '{button_text}': {e}")
|
||||
return False
|
||||
|
||||
def _find_button_by_text(self, button_text, exact_match=False, timeout=10):
|
||||
"""
|
||||
Находит кнопку по тексту различными способами.
|
||||
|
||||
Args:
|
||||
button_text (str): Текст для поиска
|
||||
exact_match (bool): Точное совпадение
|
||||
timeout (int): Время ожидания
|
||||
|
||||
Returns:
|
||||
WebElement или None
|
||||
"""
|
||||
button_text_lower = button_text.lower().strip()
|
||||
end_time = time.time() + timeout
|
||||
|
||||
while time.time() < end_time:
|
||||
# Вариант 1: Поиск по тегу <button>
|
||||
buttons = self.driver.find_elements(By.TAG_NAME, 'button')
|
||||
for button in buttons:
|
||||
if self._text_matches(button.text, button_text_lower, exact_match):
|
||||
if self._is_clickable(button):
|
||||
return button
|
||||
|
||||
# Вариант 2: Поиск по роли button
|
||||
buttons = self.driver.find_elements(By.CSS_SELECTOR, '[role="button"]')
|
||||
for button in buttons:
|
||||
if self._text_matches(button.text, button_text_lower, exact_match):
|
||||
if self._is_clickable(button):
|
||||
return button
|
||||
|
||||
# Вариант 3: Поиск по содержимому текста (для div/span с текстом)
|
||||
try:
|
||||
elements = self.driver.find_elements(By.XPATH,
|
||||
f"//*[contains(translate(text(), 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz'), '{button_text_lower}')][not(self::script or self::style)]")
|
||||
for elem in elements:
|
||||
if self._is_clickable(elem):
|
||||
return elem
|
||||
except:
|
||||
pass
|
||||
|
||||
# Вариант 4: Поиск по атрибутам (aria-label, title, etc)
|
||||
for attr in ['aria-label', 'title', 'data-text', 'placeholder', 'value']:
|
||||
try:
|
||||
elements = self.driver.find_elements(By.XPATH, f"//*[@{attr}]")
|
||||
for elem in elements:
|
||||
attr_value = elem.get_attribute(attr)
|
||||
if attr_value and self._text_matches(attr_value, button_text_lower, exact_match):
|
||||
if self._is_clickable(elem):
|
||||
return elem
|
||||
except:
|
||||
pass
|
||||
|
||||
# Вариант 5: Поиск label и возврат его родителя (для div+label случаев)
|
||||
try:
|
||||
labels = self.driver.find_elements(By.TAG_NAME, 'label')
|
||||
for label in labels:
|
||||
if self._text_matches(label.text, button_text_lower, exact_match):
|
||||
if self._is_clickable(label):
|
||||
return label
|
||||
# Если сам label не кликабелен, ищем кликабельного родителя
|
||||
try:
|
||||
parent = label.find_element(By.XPATH, "ancestor::div[@onclick] | ancestor::div[@role='button'] | ancestor::button")
|
||||
if self._is_clickable(parent):
|
||||
return parent
|
||||
except:
|
||||
pass
|
||||
except:
|
||||
pass
|
||||
|
||||
# Вариант 6: <input> элементы (button, submit, reset)
|
||||
try:
|
||||
inputs = self.driver.find_elements(By.CSS_SELECTOR, 'input[type="button"], input[type="submit"], input[type="reset"]')
|
||||
for inp in inputs:
|
||||
value = inp.get_attribute('value')
|
||||
if value and self._text_matches(value, button_text_lower, exact_match):
|
||||
if self._is_clickable(inp):
|
||||
return inp
|
||||
except:
|
||||
pass
|
||||
|
||||
time.sleep(0.5)
|
||||
|
||||
return None
|
||||
|
||||
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:
|
||||
if len(self.driver.window_handles) > 1:
|
||||
logging.info("📑 Закрываю текущую вкладку...")
|
||||
self.driver.close()
|
||||
# Переходим на последнюю оставшуюся вкладку
|
||||
self.driver.switch_to.window(self.driver.window_handles[-1])
|
||||
time.sleep(1)
|
||||
logging.info("✅ Перешел на предыдущую вкладку.")
|
||||
return True
|
||||
else:
|
||||
logging.warning("⚠️ Это последняя вкладка, не закрываю.")
|
||||
return False
|
||||
except Exception as e:
|
||||
logging.error(f"❌ Ошибка при закрытии вкладки: {e}")
|
||||
return False
|
||||
|
||||
def get_tabs_count(self):
|
||||
"""Возвращает количество открытых вкладок."""
|
||||
return len(self.driver.window_handles)
|
||||
Reference in New Issue
Block a user