Files
test-loderweb/services/browser_service.py
2026-02-16 10:36:43 +05:00

534 lines
28 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
class BrowserService:
"""Сервис для автоматизации работы с браузером."""
# ...existing code...
def click_tab_and_wait(self, url, tab_text, wait_before_click=5, wait_after_click=5):
"""Открывает страницу, ждёт, нажимает вкладку по тексту, ждёт загрузку."""
try:
logging.info(f"🌐 Открываю страницу: {url}")
self.driver.get(url)
logging.info(f"⏳ Жду {wait_before_click} сек перед кликом...")
time.sleep(wait_before_click)
logging.info(f"🔍 Нажимаю вкладку: '{tab_text}'")
result = self.select_tab_by_multiple_methods(tab_text, timeout=10)
if result:
logging.info(f"✅ Вкладка '{tab_text}' нажата, жду {wait_after_click} сек...")
time.sleep(wait_after_click)
else:
logging.warning(f"Не удалось нажать вкладку '{tab_text}'")
return result
except Exception as e:
logging.error(f"❌ Ошибка click_tab_and_wait: {e}")
return False
"""
Модуль для автоматизации работы с браузером через 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[type='file'] с максимальной надёжностью."""
try:
logging.info(f"📤 Загружаю файл: {Path(file_path).name}")
file_input = self.wait.until(EC.presence_of_element_located((by, selector)))
# Если input скрыт, делаем его видимым через JS
is_displayed = file_input.is_displayed()
if not is_displayed:
try:
self.driver.execute_script("arguments[0].style.display = 'block'; arguments[0].style.visibility = 'visible'; arguments[0].style.opacity = 1; arguments[0].style.height = 'auto'; arguments[0].style.width = 'auto';", file_input)
logging.info(" Сделал input[type='file'] видимым через JS")
except Exception as e:
logging.warning(f" Не удалось сделать input[type='file'] видимым: {e}")
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().lower()
logging.info(f"🔍 Начинаю поиск вкладки/кнопки: '{tab_text}' (v3)")
tag_selector = 'a, div, span, li, button, td, th, input, label, strong, b, em, i, p, section, header, footer, article, aside, nav, form'
while time.time() < end_time:
attempt += 1
logging.info(f" Попытка {attempt}: поиск элементов через JS...")
try:
script = f"""
var searchText = arguments[0];
var allElements = document.querySelectorAll('{tag_selector}');
var foundElements = [];
for (var i = 0; i < allElements.length; i++) {{
var el = allElements[i];
if (el.offsetParent === null) continue;
var elementText = (el.innerText || el.textContent || '').trim().toLowerCase();
var aria = (el.getAttribute('aria-label') || '').trim().toLowerCase();
var title = (el.getAttribute('title') || '').trim().toLowerCase();
if (
elementText == searchText ||
elementText.includes(searchText) ||
aria == searchText ||
aria.includes(searchText) ||
title == searchText ||
title.includes(searchText)
) {{
foundElements.push(el);
}}
var children = el.querySelectorAll('span, b, strong, em, i');
for (var j = 0; j < children.length; j++) {{
var childText = (children[j].innerText || children[j].textContent || '').trim().toLowerCase();
if (childText == searchText || childText.includes(searchText)) {{
foundElements.push(el);
break;
}}
}}
}}
return foundElements;
"""
candidates = self.driver.execute_script(script, tab_text_normalized)
if not candidates:
logging.info(" ⚠️ JS поиск ничего не дал, пробую XPath...")
try:
xpath = (
f"//*[contains(translate(text(), 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz'), '{tab_text_normalized}') "
f"or contains(translate(@aria-label, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz'), '{tab_text_normalized}') "
f"or contains(translate(@title, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz'), '{tab_text_normalized}')]"
)
candidates = self.driver.find_elements(By.XPATH, xpath)
except Exception as e_xpath:
logging.warning(f" XPath fail: {e_xpath}")
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)
try:
logging.info(f" 👉 Кандидат #{i+1} (<{element.tag_name}>): text='{element.text[:40]}', aria-label='{element.get_attribute('aria-label')}', title='{element.get_attribute('title')}'")
except Exception as elog:
logging.info(f" 👉 Кандидат #{i+1} (<unknown>): {elog}")
# Сохраняем текущее содержимое body
old_body = self.driver.execute_script("return document.body.innerHTML;")
# 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)
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)
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)
# Проверяем изменение содержимого body
try:
WebDriverWait(self.driver, 5).until(lambda d: d.execute_script("return document.body.innerHTML;") != old_body)
logging.info(" ✅ Содержимое страницы изменилось после клика")
return True
except Exception:
logging.warning(" ⚠️ Содержимое страницы не изменилось после клика")
continue
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