Files
test-loderweb/services/browser_service.py
unknown c348a19052 fff
2026-02-12 00:19:38 +05:00

635 lines
32 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.
"""
Модуль для автоматизации работы с браузером через 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:
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
"""
end_time = time.time() + timeout
attempt = 0
# Нормализуем текст для поиска
tab_text_normalized = tab_text.strip()
tab_text_lower = tab_text_normalized.lower()
logging.info(f"🔍 Начинаю поиск вкладки: '{tab_text}'")
while time.time() < end_time:
attempt += 1
logging.info(f" Попытка {attempt}: перебираю все элементы на странице...")
try:
# Ищем и кликаем через JavaScript в одном скрипте
script = """
var searchText = arguments[0].toLowerCase();
var allElements = document.querySelectorAll('*');
var foundElements = [];
console.log('Ищу элемент с текстом:', searchText);
for (var i = 0; i < allElements.length; i++) {
var el = allElements[i];
var elementText = el.textContent || el.innerText || '';
var visibleText = elementText.trim();
// Проверяем только прямой текст элемента (не включая детей)
var directText = '';
for (var j = 0; j < el.childNodes.length; j++) {
if (el.childNodes[j].nodeType === 3) { // TEXT_NODE
directText += el.childNodes[j].nodeValue;
}
}
directText = directText.trim();
// Совпадение по прямому тексту или полному тексту
var match = false;
if (directText.toLowerCase() === searchText ||
visibleText.toLowerCase() === searchText) {
match = true;
} else if (directText.toLowerCase().includes(searchText) ||
visibleText.toLowerCase().includes(searchText)) {
match = true;
}
if (match) {
// Проверяем видимость элемента
var rect = el.getBoundingClientRect();
var style = window.getComputedStyle(el);
if (rect.width > 0 && rect.height > 0 &&
style.display !== 'none' &&
style.visibility !== 'hidden' &&
style.opacity !== '0') {
foundElements.push({
element: el,
tag: el.tagName.toLowerCase(),
text: visibleText.substring(0, 50),
directText: directText.substring(0, 50)
});
}
}
}
console.log('Найдено элементов:', foundElements.length);
// Пытаемся кликнуть на каждый найденный элемент
for (var k = 0; k < foundElements.length; k++) {
var item = foundElements[k];
var elem = item.element;
try {
// Прокручиваем к элементу (без анимации для скорости)
elem.scrollIntoView({block: 'center', behavior: 'auto'});
// Небольшая пауза для стабильности (выполняется синхронно)
var startWait = Date.now();
while (Date.now() - startWait < 200) {
// Ждем 200мс
}
console.log('Пробую кликнуть элемент:', item.tag, item.text);
// Сначала находим САМЫЙ кликабельный элемент в иерархии
var targetToClick = elem;
// Ищем родителя который является ссылкой или кнопкой
var clickableParent = elem.closest('a, button, [role="tab"], [role="button"], li');
if (clickableParent) {
console.log('Найден кликабельный родитель:', clickableParent.tagName, clickableParent.className);
targetToClick = clickableParent;
}
// Теперь пробуем все методы клика на найденном элементе
var methods = [
{
name: 'direct click',
fn: function(el) { el.click(); }
},
{
name: 'mousedown+mouseup',
fn: function(el) {
el.dispatchEvent(new MouseEvent('mousedown', {bubbles: true, cancelable: true}));
el.dispatchEvent(new MouseEvent('mouseup', {bubbles: true, cancelable: true}));
el.dispatchEvent(new MouseEvent('click', {bubbles: true, cancelable: true}));
}
},
{
name: 'dispatchEvent click',
fn: function(el) {
el.dispatchEvent(new MouseEvent('click', {
view: window,
bubbles: true,
cancelable: true,
clientX: el.getBoundingClientRect().left + 5,
clientY: el.getBoundingClientRect().top + 5
}));
}
},
{
name: 'focus + click',
fn: function(el) {
if (el.focus) el.focus();
el.click();
}
}
];
// Пробуем каждый метод
for (var m = 0; m < methods.length; m++) {
try {
console.log('Пробую метод:', methods[m].name, 'на', targetToClick.tagName);
methods[m].fn(targetToClick);
console.log('✅ Успешно кликнул методом:', methods[m].name);
return {success: true, tag: targetToClick.tagName, text: item.text, method: methods[m].name};
} catch(err) {
console.log('✗ Метод', methods[m].name, 'не сработал:', err.message);
}
}
} catch(err) {
console.log('Общая ошибка при обработке элемента:', err.message);
}
}
return {success: false, found: foundElements.length, message: 'Элементы найдены, но ни один метод клика не сработал'};
"""
result = self.driver.execute_script(script, tab_text_lower)
if result and isinstance(result, dict):
if result.get('success'):
method = result.get('method', 'unknown')
tag = result.get('tag', 'unknown')
text = result.get('text', '')
logging.info(f" ✅ УСПЕХ! Метод: {method}, тег: <{tag}>, текст: '{text}'")
time.sleep(0.5)
return True
else:
found_count = result.get('found', 0)
message = result.get('message', 'Не найдено')
if found_count > 0:
logging.warning(f" ✗ Попытка {attempt}: найдено {found_count} элементов, но кликнуть не удалось")
else:
logging.warning(f" ✗ Попытка {attempt}: {message}")
else:
logging.warning(f" ✗ Попытка {attempt}: некорректный результат от JavaScript")
time.sleep(1)
except Exception as e:
logging.warning(f" ✗ Ошибка при попытке {attempt}: {e}")
import traceback
logging.debug(traceback.format_exc())
time.sleep(1)
logging.error(f"Не удалось найти и кликнуть на вкладку '{tab_text}' за {timeout} секунд после {attempt} попыток")
return False