This commit is contained in:
2026-02-12 01:41:09 +05:00
parent c348a19052
commit a255e9296f
2 changed files with 143 additions and 265 deletions

View File

@@ -4,6 +4,7 @@
import time
import logging
import os
import shutil
import sys
import platform
import subprocess
@@ -37,7 +38,16 @@ class BrowserService:
def _find_firefox_path():
"""Автоматически находит путь к Firefox для текущей ОС."""
system = platform.system()
found_path = None
# 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
@@ -47,6 +57,7 @@ class BrowserService:
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)
@@ -72,10 +83,12 @@ class BrowserService:
else: # Linux
# Linux пути
possible_paths = [
"/snap/bin/firefox",
"/usr/bin/firefox",
"/usr/bin/firefox-esr",
"/snap/bin/firefox",
"/usr/local/bin/firefox",
"/opt/firefox/firefox",
"/usr/lib/firefox/firefox",
]
# Ищем Firefox в возможных местах
@@ -84,16 +97,15 @@ class BrowserService:
logging.info(f"✅ Firefox найден: {path}")
return path
# Пытаемся найти через which/where команды
# Последний шанс: subprocess (если shutil.which пропустил что-то специфичное)
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)
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]
logging.info(f"✅ Firefox найден через {system} search: {firefox_path}")
if os.path.exists(firefox_path):
logging.info(f"✅ Firefox найден через команду search: {firefox_path}")
return firefox_path
except:
pass
@@ -112,10 +124,6 @@ class BrowserService:
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}")
@@ -126,20 +134,45 @@ class BrowserService:
logging.info("🚀 Запуск Firefox драйвера с ВИДИМЫМ окном...")
# Попытка 1: Использовать webdriver-manager (самый надежный способ)
if HAS_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: Прямой запуск (Selenium сам найдет geckodriver в PATH)
# Попытка 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(" Попытка 2: Прямой запуск (Selenium ищет geckodriver в PATH)...")
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
@@ -154,6 +187,18 @@ class BrowserService:
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 Решения:")
@@ -180,119 +225,24 @@ class BrowserService:
def click_button_by_text(self, button_text, exact_match=False, timeout=10):
"""
Ищет и нажимает кнопку по тексту в различных формах.
Ищет и нажимает кнопку (или любой элемент) по тексту, используя продвинутый метод поиска.
Args:
button_text (str): Текст кнопки для поиска
exact_match (bool): Точное совпадение или частичное
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
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):
"""
Находит кнопку по тексту различными способами.
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):
"""Проверяет совпадение текста."""
@@ -454,182 +404,101 @@ class BrowserService:
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()
tab_text_lower = tab_text_normalized.lower()
logging.info(f"🔍 Начинаю поиск вкладки: '{tab_text}'")
logging.info(f"🔍 Начинаю поиск вкладки: '{tab_text}' (v2)")
while time.time() < end_time:
attempt += 1
logging.info(f" Попытка {attempt}: перебираю все элементы на странице...")
logging.info(f" Попытка {attempt}: поиск элементов через JS...")
try:
# Ищем и кликаем через JavaScript в одном скрипте
script = """
var searchText = arguments[0].toLowerCase();
var allElements = document.querySelectorAll('*');
var allElements = document.querySelectorAll('a, div, span, li, button, td, th');
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();
if (el.offsetParent === null) continue; // Skip hidden
// Проверяем только прямой текст элемента (не включая детей)
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 elementText = el.innerText || el.textContent || '';
var cleanText = elementText.trim().toLowerCase();
// Совпадение по прямому тексту или полному тексту
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)
});
// Strict match or contains if short
if (cleanText === searchText || (cleanText.includes(searchText) && cleanText.length < searchText.length + 50)) {
foundElements.push(el);
}
}
}
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: 'Элементы найдены, но ни один метод клика не сработал'};
return foundElements;
"""
result = self.driver.execute_script(script, tab_text_lower)
candidates = self.driver.execute_script(script, tab_text_normalized)
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}'")
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
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")
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}")
import traceback
logging.debug(traceback.format_exc())
time.sleep(1)
logging.error(f"Не удалось найти и кликнуть на вкладку '{tab_text}' за {timeout} секунд после {attempt} попыток")
logging.error(f"Не удалось найти и кликнуть на вкладку '{tab_text}' за {timeout} секунд")
return False

View File

@@ -79,6 +79,15 @@ class FileProcessorService:
self.browser.open_page(self.url)
self.browser.wait_page_load(5)
# ПЕРВЫЙ ПРОГОН НАСТРОЙКИ: Если выбрана вкладка, пробуем её активировать на главной странице сразу
if selected_tab:
logging.info(f"🔍 [Pre-Check] Пробую найти вкладку '{selected_tab}' на стартовой странице...")
# Попытка найти вкладку с увеличенным таймаутом для первого раза
if self.browser.select_tab_by_multiple_methods(selected_tab, timeout=10):
logging.info(f"✅ [Pre-Check] Вкладка '{selected_tab}' найдена на старте.")
else:
logging.info(f"⚠️ [Pre-Check] Вкладка '{selected_tab}' пока не найдена (возможно появится после авторизации/загрузки).")
# Шаг 3: Обработка каждого файла
logging.info("\n📋 ШАГ 3: ОБРАБОТКА ФАЙЛОВ")
logging.info("-" * 70)