fix
This commit is contained in:
42
.gitignore
vendored
Normal file
42
.gitignore
vendored
Normal file
@@ -0,0 +1,42 @@
|
||||
.venv
|
||||
venv
|
||||
__pycache__
|
||||
*.pyc
|
||||
*.pyo
|
||||
*.pyd
|
||||
*.sqlite3
|
||||
*.log
|
||||
.DS_Store
|
||||
.env
|
||||
.env.*
|
||||
.idea
|
||||
.vscode
|
||||
*.swp
|
||||
*.swo
|
||||
coverage
|
||||
htmlcov
|
||||
.tox
|
||||
.pytest_cache
|
||||
.pyre
|
||||
.mypy_cache
|
||||
.pytype
|
||||
.pytest_cache/
|
||||
.cache
|
||||
.pyre/
|
||||
.mypy_cache/
|
||||
.pytype/
|
||||
dist
|
||||
build
|
||||
*.egg-info
|
||||
.eggs
|
||||
pip-wheel-metadata
|
||||
pip-wheel-metadata/
|
||||
instance
|
||||
.webassets-cache
|
||||
.scrapy
|
||||
.scrapy/
|
||||
.pytest_cache/
|
||||
.coverage
|
||||
.coverage.*
|
||||
nosetests.xml
|
||||
coverage.xml
|
||||
0
.python-version
Normal file → Executable file
0
.python-version
Normal file → Executable file
15
app.py
Normal file
15
app.py
Normal file
@@ -0,0 +1,15 @@
|
||||
"""
|
||||
Главная точка входа приложения для автоматизации загрузки файлов
|
||||
"""
|
||||
import logging
|
||||
from ui import main
|
||||
|
||||
if __name__ == "__main__":
|
||||
# Настройка логирования
|
||||
logging.basicConfig(
|
||||
level=logging.INFO,
|
||||
format='%(asctime)s - %(levelname)s - %(message)s'
|
||||
)
|
||||
|
||||
# Запуск интерфейса
|
||||
main()
|
||||
121
main.py
121
main.py
@@ -1,121 +0,0 @@
|
||||
import sys
|
||||
import time
|
||||
import logging
|
||||
from selenium import webdriver
|
||||
from selenium.webdriver.common.by import By
|
||||
from selenium.webdriver.firefox.service import Service as FirefoxService
|
||||
from selenium.webdriver.firefox.options import Options as FirefoxOptions
|
||||
from selenium.webdriver.common.action_chains import ActionChains
|
||||
from selenium.webdriver.support.ui import WebDriverWait
|
||||
from selenium.webdriver.support import expected_conditions as EC
|
||||
|
||||
# Настройка логирования в терминал
|
||||
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
|
||||
|
||||
def setup_driver(download_dir):
|
||||
"""Настраивает и возвращает веб-драйвер с опциями для загрузки файлов."""
|
||||
try:
|
||||
firefox_options = FirefoxOptions()
|
||||
firefox_options.set_preference("browser.download.folderList", 2)
|
||||
firefox_options.set_preference("browser.download.dir", download_dir)
|
||||
firefox_options.set_preference("browser.download.useDownloadDir", True)
|
||||
firefox_options.set_preference("browser.helperApps.neverAsk.saveToDisk", "application/octet-stream")
|
||||
# firefox_options.add_argument("--headless")
|
||||
firefox_options.binary_location = r"C:\Program Files\Mozilla Firefox\firefox.exe"
|
||||
logging.info("Запуск Firefox драйвера...")
|
||||
driver = webdriver.Firefox(options=firefox_options)
|
||||
driver.set_page_load_timeout(300) # Timeout для загрузки страницы
|
||||
logging.info("Драйвер запущен успешно.")
|
||||
return driver
|
||||
except Exception as e:
|
||||
logging.error(f"Ошибка при настройке драйвера: {e}")
|
||||
raise
|
||||
|
||||
def log_page_content(driver):
|
||||
"""Логирует содержимое страницы в терминал."""
|
||||
try:
|
||||
title = driver.title
|
||||
logging.info(f"Заголовок страницы: {title}")
|
||||
# Попробуем найти основной контент
|
||||
try:
|
||||
main_content = driver.find_element(By.TAG_NAME, 'h1').text
|
||||
logging.info(f"Основной заголовок: {main_content}")
|
||||
except:
|
||||
logging.info("Основной заголовок не найден.")
|
||||
logging.info("Логирование содержимого завершено.")
|
||||
except Exception as e:
|
||||
logging.error(f"Ошибка при получении содержимого страницы: {e}")
|
||||
|
||||
def upload_file(driver, file_path, selector='input[type="file"]', by=By.CSS_SELECTOR):
|
||||
"""Загружает файл в input по селектору."""
|
||||
try:
|
||||
# Сначала попробуем кликнуть на видимую область загрузки
|
||||
try:
|
||||
drop_area = driver.find_element(By.CSS_SELECTOR, '.upload-area, .drop-zone, [data-upload]')
|
||||
drop_area.click()
|
||||
time.sleep(1)
|
||||
except:
|
||||
pass
|
||||
file_input = WebDriverWait(driver, 10).until(EC.presence_of_element_located((by, selector)))
|
||||
file_input.send_keys(file_path)
|
||||
logging.info(f"Файл загружен: {file_path}")
|
||||
except Exception as e:
|
||||
logging.error(f"Ошибка при загрузке файла {file_path}: {e}")
|
||||
|
||||
def close_cookie_banner(driver):
|
||||
"""Закрывает cookie-баннер, если он есть."""
|
||||
try:
|
||||
logging.info("Попытка закрыть cookie-баннер...")
|
||||
logging.info("Ищу кнопки...")
|
||||
buttons = driver.find_elements(By.TAG_NAME, 'button')
|
||||
logging.info(f"Найдено кнопок: {len(buttons)}")
|
||||
for button in buttons:
|
||||
text = button.text.lower()
|
||||
logging.info(f"Кнопка: {text}")
|
||||
if 'принять' in text or 'accept' in text or 'ok' in text or 'соглас' in text:
|
||||
button.click()
|
||||
logging.info("Cookie-баннер закрыт.")
|
||||
time.sleep(1)
|
||||
return
|
||||
# Если не нашли, попробуем по классу или id
|
||||
try:
|
||||
driver.find_element(By.ID, 'cookie-accept').click()
|
||||
logging.info("Cookie-баннер закрыт по ID.")
|
||||
except:
|
||||
try:
|
||||
driver.find_element(By.CLASS_NAME, 'cookie-accept').click()
|
||||
logging.info("Cookie-баннер закрыт по классу.")
|
||||
except:
|
||||
logging.info("Cookie-баннер не найден.")
|
||||
except Exception as e:
|
||||
logging.error(f"Ошибка при закрытии cookie-баннера: {e}")
|
||||
|
||||
def main():
|
||||
url = "https://convertio.co/ru/document-converter/"
|
||||
file_path = "c:\\Users\\jze9\\Documents\\git\\test-loderweb\\test.txt"
|
||||
download_dir = "C:\\Users\\jze9\\Downloads" # Укажите путь для загрузок
|
||||
|
||||
driver = setup_driver(download_dir)
|
||||
try:
|
||||
logging.info(f"Переход на страницу: {url}")
|
||||
driver.get(url)
|
||||
logging.info("Страница загружена.")
|
||||
time.sleep(5) # Ждем загрузки
|
||||
|
||||
# close_cookie_banner(driver) # Закрываем cookie-баннер
|
||||
|
||||
log_page_content(driver)
|
||||
|
||||
# Загрузить файл
|
||||
upload_file(driver, file_path)
|
||||
|
||||
# Возможно, нажать кнопку конвертации, если нужно
|
||||
# click_button(driver, 'button.convert') # Пример селектора
|
||||
|
||||
time.sleep(300) # Ждем дольше для симуляции
|
||||
input("Нажмите Enter для выхода...") # Чтобы браузер не закрывался сразу
|
||||
finally:
|
||||
logging.info("Программа завершена, браузер остаётся открытым.")
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
0
pyproject.toml
Normal file → Executable file
0
pyproject.toml
Normal file → Executable file
2
requirements.txt
Normal file
2
requirements.txt
Normal file
@@ -0,0 +1,2 @@
|
||||
selenium==4.40.0
|
||||
webdriver-manager==4.0.2
|
||||
1
services/__init__.py
Normal file
1
services/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
# Services package
|
||||
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)
|
||||
97
services/file_finder.py
Normal file
97
services/file_finder.py
Normal file
@@ -0,0 +1,97 @@
|
||||
"""
|
||||
Модуль для поиска файлов формата .xlsx в директории
|
||||
"""
|
||||
import logging
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
class FileFinderService:
|
||||
"""Сервис для поиска и управления файлами .xlsx"""
|
||||
|
||||
def __init__(self):
|
||||
"""Инициализирует сервис поиска файлов."""
|
||||
self.xlsx_files = []
|
||||
logging.info("FileFinderService инициализирован.")
|
||||
|
||||
def find_xlsx_files(self, directory_path, recursive=True):
|
||||
"""
|
||||
Ищет все файлы формата .xlsx в директории и сортирует их.
|
||||
|
||||
Args:
|
||||
directory_path (str или Path): Путь к директории для поиска
|
||||
recursive (bool): Искать ли рекурсивно по подпапкам
|
||||
|
||||
Returns:
|
||||
list: Список путей до файлов .xlsx в порядке возрастания
|
||||
"""
|
||||
try:
|
||||
dir_path = Path(directory_path)
|
||||
|
||||
if not dir_path.exists():
|
||||
logging.error(f"❌ Директория не найдена: {directory_path}")
|
||||
return []
|
||||
|
||||
if not dir_path.is_dir():
|
||||
logging.error(f"❌ Это не директория: {directory_path}")
|
||||
return []
|
||||
|
||||
logging.info(f"🔍 Начинаю поиск .xlsx файлов в: {dir_path}")
|
||||
|
||||
# Ищем все файлы .xlsx
|
||||
if recursive:
|
||||
xlsx_files = list(dir_path.glob('**/*.xlsx'))
|
||||
logging.info(" (поиск во всех подпапках)")
|
||||
else:
|
||||
xlsx_files = list(dir_path.glob('*.xlsx'))
|
||||
logging.info(" (только текущая папка)")
|
||||
|
||||
# Сортируем в порядке возрастания по имени файла
|
||||
xlsx_files.sort(key=lambda x: x.name)
|
||||
|
||||
# Сохраняем результаты
|
||||
self.xlsx_files = xlsx_files
|
||||
|
||||
logging.info(f"✅ Найдено файлов .xlsx: {len(xlsx_files)}")
|
||||
|
||||
# Логируем каждый найденный файл
|
||||
if xlsx_files:
|
||||
for idx, file_path in enumerate(xlsx_files, 1):
|
||||
logging.info(f" [{idx}] {file_path.name}")
|
||||
else:
|
||||
logging.warning("⚠️ Файлы .xlsx не найдены в указанной директории")
|
||||
|
||||
return xlsx_files
|
||||
except Exception as e:
|
||||
logging.error(f"❌ Ошибка при поиске .xlsx файлов: {e}")
|
||||
return []
|
||||
|
||||
def get_file_paths(self):
|
||||
"""Возвращает список сохраненных путей файлов."""
|
||||
return self.xlsx_files
|
||||
|
||||
def get_file_paths_as_strings(self):
|
||||
"""Возвращает список путей в виде строк."""
|
||||
return [str(file_path) for file_path in self.xlsx_files]
|
||||
|
||||
def get_file_count(self):
|
||||
"""Возвращает количество найденных файлов."""
|
||||
return len(self.xlsx_files)
|
||||
|
||||
def get_file_by_index(self, index):
|
||||
"""Возвращает файл по индексу (0-based)."""
|
||||
try:
|
||||
if 0 <= index < len(self.xlsx_files):
|
||||
file_path = self.xlsx_files[index]
|
||||
logging.info(f"📄 Получен файл #{index + 1}: {file_path.name}")
|
||||
return file_path
|
||||
else:
|
||||
logging.warning(f"⚠️ Индекс {index} выходит за границы (всего: {len(self.xlsx_files)})")
|
||||
return None
|
||||
except Exception as e:
|
||||
logging.error(f"❌ Ошибка при получении файла по индексу: {e}")
|
||||
return None
|
||||
|
||||
def clear(self):
|
||||
"""Очищает список найденных файлов."""
|
||||
self.xlsx_files = []
|
||||
logging.info("🗑️ Список файлов очищен.")
|
||||
186
services/processor.py
Normal file
186
services/processor.py
Normal file
@@ -0,0 +1,186 @@
|
||||
"""
|
||||
Модуль для обработки файлов и управления циклом загрузок
|
||||
"""
|
||||
import logging
|
||||
import time
|
||||
from pathlib import Path
|
||||
from services.file_finder import FileFinderService
|
||||
from services.browser_service import BrowserService
|
||||
|
||||
|
||||
class FileProcessorService:
|
||||
"""Сервис для обработки файлов и управления процессом загрузок"""
|
||||
|
||||
def __init__(self, directory_path, url, download_dir, firefox_path=None):
|
||||
"""
|
||||
Инициализирует процессор файлов.
|
||||
|
||||
Args:
|
||||
directory_path (str): Путь к директории с .xlsx файлами
|
||||
url (str): URL страницы для загрузки файлов
|
||||
download_dir (str): Путь для загрузок
|
||||
firefox_path (str): Путь к Firefox (опционально)
|
||||
"""
|
||||
self.directory_path = directory_path
|
||||
self.url = url
|
||||
self.download_dir = download_dir
|
||||
self.firefox_path = firefox_path
|
||||
|
||||
self.file_finder = FileFinderService()
|
||||
self.browser = None
|
||||
self.processed_files = 0
|
||||
self.failed_files = []
|
||||
|
||||
logging.info("=" * 70)
|
||||
logging.info("🚀 ИНИЦИАЛИЗАЦИЯ ПРОЦЕССОРА ФАЙЛОВ")
|
||||
logging.info("=" * 70)
|
||||
|
||||
def start_processing(self, upload_button_texts=None, process_button_texts=None):
|
||||
"""
|
||||
Запускает процесс поиска и загрузки файлов.
|
||||
|
||||
Args:
|
||||
upload_button_texts (list): Варианты текста кнопки загрузки файла
|
||||
process_button_texts (list): Варианты текста кнопки обработки
|
||||
|
||||
Returns:
|
||||
dict: Результаты обработки
|
||||
"""
|
||||
if upload_button_texts is None:
|
||||
upload_button_texts = ["Загрузить файл", "Выбрать файл", "Добавить файл"]
|
||||
|
||||
if process_button_texts is None:
|
||||
process_button_texts = ["Загрузить", "Обработать", "Преобразовать", "Изменить", "Конвертировать"]
|
||||
|
||||
results = {
|
||||
"total_files": 0,
|
||||
"processed_files": 0,
|
||||
"failed_files": [],
|
||||
"errors": []
|
||||
}
|
||||
|
||||
try:
|
||||
# Шаг 1: Поиск файлов
|
||||
logging.info("\n📂 ШАГ 1: ПОИСК ФАЙЛОВ")
|
||||
logging.info("-" * 70)
|
||||
xlsx_files = self.file_finder.find_xlsx_files(self.directory_path, recursive=True)
|
||||
results["total_files"] = len(xlsx_files)
|
||||
|
||||
if not xlsx_files:
|
||||
logging.error("❌ Файлы не найдены. Процесс прерван.")
|
||||
results["errors"].append("Файлы .xlsx не найдены в указанной директории")
|
||||
return results
|
||||
|
||||
# Шаг 2: Инициализация браузера
|
||||
logging.info("\n🌐 ШАГ 2: ИНИЦИАЛИЗАЦИЯ БРАУЗЕРА")
|
||||
logging.info("-" * 70)
|
||||
self.browser = BrowserService(self.download_dir, self.firefox_path)
|
||||
self.browser.open_page(self.url)
|
||||
self.browser.wait_page_load(5)
|
||||
|
||||
# Шаг 3: Обработка каждого файла
|
||||
logging.info("\n📋 ШАГ 3: ОБРАБОТКА ФАЙЛОВ")
|
||||
logging.info("-" * 70)
|
||||
|
||||
for idx, file_path in enumerate(xlsx_files, 1):
|
||||
# Задержка 5 сек перед следующим файлом (кроме первого)
|
||||
if idx > 1:
|
||||
logging.info(f"\n⏳ Задержка 5 сек перед следующим файлом...")
|
||||
time.sleep(5)
|
||||
|
||||
logging.info(f"\n📄 Файл {idx}/{len(xlsx_files)}: {file_path.name}")
|
||||
logging.info("=" * 70)
|
||||
|
||||
try:
|
||||
# Открываем новую вкладку для каждого файла (кроме первого)
|
||||
if idx > 1:
|
||||
logging.info(f" Открываю новую вкладку для файла...")
|
||||
if not self.browser.open_new_tab(self.url):
|
||||
logging.warning(f"⚠️ Не удалось открыть новую вкладку")
|
||||
self.browser.wait_page_load(3)
|
||||
|
||||
# Поиск и клик по кнопке загрузки
|
||||
upload_button_found = False
|
||||
for btn_text in upload_button_texts:
|
||||
if self.browser.click_button_by_text(btn_text):
|
||||
upload_button_found = True
|
||||
break
|
||||
|
||||
if not upload_button_found:
|
||||
logging.warning(f"⚠️ Не найдена кнопка загрузки файла")
|
||||
self.failed_files.append({
|
||||
"file": file_path.name,
|
||||
"reason": "Кнопка загрузки не найдена"
|
||||
})
|
||||
results["failed_files"].append(file_path.name)
|
||||
continue
|
||||
|
||||
# Загрузка файла
|
||||
self.browser.wait_page_load(1)
|
||||
upload_success = self.browser.upload_file(str(file_path))
|
||||
|
||||
if not upload_success:
|
||||
logging.warning(f"⚠️ Ошибка при загрузке файла")
|
||||
self.failed_files.append({
|
||||
"file": file_path.name,
|
||||
"reason": "Ошибка при загрузке файла"
|
||||
})
|
||||
results["failed_files"].append(file_path.name)
|
||||
continue
|
||||
|
||||
# Поиск и клик по кнопке обработки
|
||||
self.browser.wait_page_load(2)
|
||||
process_button_found = False
|
||||
for btn_text in process_button_texts:
|
||||
if self.browser.click_button_by_text(btn_text):
|
||||
process_button_found = True
|
||||
break
|
||||
|
||||
if not process_button_found:
|
||||
logging.warning(f"⚠️ Не найдена кнопка обработки")
|
||||
# Это не критичная ошибка, файл уже загружен
|
||||
|
||||
# Ожидание обработки
|
||||
self.browser.wait_page_load(3)
|
||||
self.processed_files += 1
|
||||
results["processed_files"] += 1
|
||||
logging.info(f"✅ Файл обработан успешно")
|
||||
|
||||
# Закрываем вкладку после обработки (если это не первый файл)
|
||||
if idx > 1:
|
||||
logging.info(f" Закрываю вкладку после обработки...")
|
||||
self.browser.close_current_tab()
|
||||
|
||||
except Exception as e:
|
||||
logging.error(f"❌ Ошибка при обработке файла: {e}")
|
||||
self.failed_files.append({
|
||||
"file": file_path.name,
|
||||
"reason": str(e)
|
||||
})
|
||||
results["failed_files"].append(file_path.name)
|
||||
|
||||
# Итоговый отчет
|
||||
logging.info("\n" + "=" * 70)
|
||||
logging.info("📊 ИТОГОВЫЙ ОТЧЕТ")
|
||||
logging.info("=" * 70)
|
||||
logging.info(f"Всего файлов: {results['total_files']}")
|
||||
logging.info(f"Обработано успешно: {results['processed_files']}")
|
||||
logging.info(f"Ошибок: {len(results['failed_files'])}")
|
||||
|
||||
if results["failed_files"]:
|
||||
logging.warning("\n❌ Файлы с ошибками:")
|
||||
for file_info in self.failed_files:
|
||||
logging.warning(f" - {file_info['file']}: {file_info['reason']}")
|
||||
|
||||
logging.info("\n✅ ОБРАБОТКА ЗАВЕРШЕНА")
|
||||
logging.info("=" * 70)
|
||||
|
||||
return results
|
||||
|
||||
except Exception as e:
|
||||
logging.error(f"❌ Критическая ошибка: {e}")
|
||||
results["errors"].append(str(e))
|
||||
return results
|
||||
finally:
|
||||
if self.browser:
|
||||
self.browser.close()
|
||||
52
test.txt
52
test.txt
@@ -1,52 +0,0 @@
|
||||
qweqweqweqweqweqweqweqweqweqweqweqweqweqweqweqwe
|
||||
qweqweqweqweqweqweqweqweqweqweqweqweqweqweqweqwe
|
||||
qweqweqweqweqweqweqweqweqweqweqweqweqweqweqweqwe
|
||||
qweqweqweqweqweqweqweqweqweqweqweqweqweqweqweqwe
|
||||
qweqweqweqweqweqweqweqweqweqweqweqweqweqweqweqwe
|
||||
qweqweqweqweqweqweqweqweqweqweqweqweqweqweqweqwe
|
||||
qweqweqweqweqweqweqweqweqweqweqweqweqweqweqweqwe
|
||||
qweqweqweqweqweqweqweqweqweqweqweqweqweqweqweqwe
|
||||
qweqweqweqweqweqweqweqweqweqweqweqweqweqweqweqwe
|
||||
qweqweqweqweqweqweqweqweqweqweqweqweqweqweqweqwe
|
||||
qweqweqweqweqweqweqweqweqweqweqweqweqweqweqweqwe
|
||||
qweqweqweqweqweqweqweqweqweqweqweqweqweqweqweqwe
|
||||
qweqweqweqweqweqweqweqweqweqweqweqweqweqweqweqwe
|
||||
qweqweqweqweqweqweqweqweqweqweqweqweqweqweqweqwe
|
||||
qweqweqweqweqweqweqweqweqweqweqweqweqweqweqweqwe
|
||||
qweqweqweqweqweqweqweqweqweqweqweqweqweqweqweqwe
|
||||
qweqweqweqweqweqweqweqweqweqweqweqweqweqweqweqwe
|
||||
qweqweqweqweqweqweqweqweqweqweqweqweqweqweqweqwe
|
||||
qweqweqweqweqweqweqweqweqweqweqweqweqweqweqweqwe
|
||||
qweqweqweqweqweqweqweqweqweqweqweqweqweqweqweqwe
|
||||
|
||||
qweqweqweqweqweqweqweqweqweqweqweqweqweqweqweqwe
|
||||
qweqweqweqweqweqweqweqweqweqweqweqweqweqweqweqwe
|
||||
qweqweqweqweqweqweqweqweqweqweqweqweqweqweqweqwe
|
||||
qweqweqweqweqweqweqweqweqweqweqweqweqweqweqweqwe
|
||||
qweqweqweqweqweqweqweqweqweqweqweqweqweqweqweqwe
|
||||
qweqweqweqweqweqweqweqweqweqweqweqweqweqweqweqwe
|
||||
qweqweqweqweqweqweqweqweqweqweqweqweqweqweqweqwe
|
||||
qweqweqweqweqweqweqweqweqweqweqweqweqweqweqweqwe
|
||||
qweqweqweqweqweqweqweqweqweqweqweqweqweqweqweqwe
|
||||
qweqweqweqweqweqweqweqweqweqweqweqweqweqweqweqwe
|
||||
qweqweqweqweqweqweqweqweqweqweqweqweqweqweqweqwe
|
||||
qweqweqweqweqweqweqweqweqweqweqweqweqweqweqweqwe
|
||||
qweqweqweqweqweqweqweqweqweqweqweqweqweqweqweqwe
|
||||
qweqweqweqweqweqweqweqweqweqweqweqweqweqweqweqwe
|
||||
qweqweqweqweqweqweqweqweqweqweqweqweqweqweqweqwe
|
||||
qweqweqweqweqweqweqweqweqweqweqweqweqweqweqweqwe
|
||||
qweqweqweqweqweqweqweqweqweqweqweqweqweqweqweqwe
|
||||
qweqweqweqweqweqweqweqweqweqweqweqweqweqweqweqwe
|
||||
qweqweqweqweqweqweqweqweqweqweqweqweqweqweqweqwe
|
||||
qweqweqweqweqweqweqweqweqweqweqweqweqweqweqweqwe
|
||||
|
||||
qweqweqweqweqweqweqweqweqweqweqweqweqweqweqweqwe
|
||||
qweqweqweqweqweqweqweqweqweqweqweqweqweqweqweqwe
|
||||
qweqweqweqweqweqweqweqweqweqweqweqweqweqweqweqwe
|
||||
qweqweqweqweqweqweqweqweqweqweqweqweqweqweqweqwe
|
||||
qweqweqweqweqweqweqweqweqweqweqweqweqweqweqweqwe
|
||||
qweqweqweqweqweqweqweqweqweqweqweqweqweqweqweqwe
|
||||
qweqweqweqweqweqweqweqweqweqweqweqweqweqweqweqwe
|
||||
qweqweqweqweqweqweqweqweqweqweqweqweqweqweqweqwe
|
||||
qweqweqweqweqweqweqweqweqweqweqweqweqweqweqweqwe
|
||||
qweqweqweqweqweqweqweqweqweqweqweqweqweqweqweqwe
|
||||
322
ui.py
Normal file
322
ui.py
Normal file
@@ -0,0 +1,322 @@
|
||||
"""
|
||||
Интерфейс (UI) для приложения автоматизации загрузки файлов
|
||||
"""
|
||||
import tkinter as tk
|
||||
from tkinter import filedialog, messagebox, ttk
|
||||
import threading
|
||||
import logging
|
||||
from pathlib import Path
|
||||
from services.processor import FileProcessorService
|
||||
|
||||
|
||||
class UIManager:
|
||||
"""Менеджер интерфейса приложения"""
|
||||
|
||||
def __init__(self, root):
|
||||
"""Инициализирует интерфейс."""
|
||||
self.root = root
|
||||
self.root.title("🔄 Автоматизация загрузки файлов")
|
||||
self.root.geometry("700x600")
|
||||
self.root.configure(bg="#2C3E50")
|
||||
self.root.minsize(500, 400)
|
||||
|
||||
self.font_title = ("Arial", 14, "bold")
|
||||
self.font_label = ("Arial", 11)
|
||||
self.font_button = ("Arial", 10, "bold")
|
||||
|
||||
self.processor = None
|
||||
self.processing = False
|
||||
|
||||
self._setup_logging()
|
||||
self._create_ui()
|
||||
|
||||
def _setup_logging(self):
|
||||
"""Настраивает логирование в текстовое поле."""
|
||||
# Обычное логирование в консоль
|
||||
logging.basicConfig(
|
||||
level=logging.INFO,
|
||||
format='%(asctime)s - %(levelname)s - %(message)s'
|
||||
)
|
||||
|
||||
def _create_ui(self):
|
||||
"""Создает элементы интерфейса."""
|
||||
# Главный фрейм
|
||||
main_frame = tk.Frame(self.root, bg="#2C3E50")
|
||||
main_frame.pack(fill=tk.BOTH, expand=True, padx=20, pady=20)
|
||||
|
||||
# Заголовок
|
||||
title_label = tk.Label(
|
||||
main_frame,
|
||||
text="📁 Автоматизация загрузки файлов XLSX",
|
||||
font=self.font_title,
|
||||
bg="#2C3E50",
|
||||
fg="#ECF0F1"
|
||||
)
|
||||
title_label.pack(pady=(0, 20))
|
||||
|
||||
# ===== ПОЛЕ 1: ДИРЕКТОРИЯ С ФАЙЛАМИ =====
|
||||
dir_label = tk.Label(
|
||||
main_frame,
|
||||
text="📂 Директория с файлами .xlsx:",
|
||||
font=self.font_label,
|
||||
bg="#2C3E50",
|
||||
fg="#ECF0F1"
|
||||
)
|
||||
dir_label.pack(anchor="w", pady=(10, 5))
|
||||
|
||||
dir_frame = tk.Frame(main_frame, bg="#2C3E50")
|
||||
dir_frame.pack(fill=tk.X, pady=(0, 15))
|
||||
|
||||
self.dir_entry = tk.Entry(dir_frame, font=self.font_label, width=60)
|
||||
self.dir_entry.pack(side="left", fill=tk.X, expand=True)
|
||||
self.dir_entry.bind("<Button-1>", lambda e: self._select_directory())
|
||||
|
||||
dir_button = tk.Button(
|
||||
dir_frame,
|
||||
text="🗂️ Выбрать",
|
||||
command=self._select_directory,
|
||||
font=self.font_button,
|
||||
bg="#3498DB",
|
||||
fg="white",
|
||||
padx=10
|
||||
)
|
||||
dir_button.pack(side="left", padx=(10, 0))
|
||||
|
||||
# ===== ПОЛЕ 2: URL СТРАНИЦЫ =====
|
||||
url_label = tk.Label(
|
||||
main_frame,
|
||||
text="🌐 URL страницы для загрузки:",
|
||||
font=self.font_label,
|
||||
bg="#2C3E50",
|
||||
fg="#ECF0F1"
|
||||
)
|
||||
url_label.pack(anchor="w", pady=(10, 5))
|
||||
|
||||
self.url_entry = tk.Entry(main_frame, font=self.font_label)
|
||||
self.url_entry.pack(anchor="w", fill=tk.X, pady=(0, 15))
|
||||
self.url_entry.insert(0, "https://")
|
||||
|
||||
# ===== ПОЛЕ 3: ВАРИАНТЫ КНОПОК =====
|
||||
buttons_label = tk.Label(
|
||||
main_frame,
|
||||
text="🔘 Варианты текста кнопок (через запятую):",
|
||||
font=self.font_label,
|
||||
bg="#2C3E50",
|
||||
fg="#ECF0F1"
|
||||
)
|
||||
buttons_label.pack(anchor="w", pady=(10, 5))
|
||||
|
||||
self.upload_buttons_entry = tk.Entry(main_frame, font=self.font_label)
|
||||
self.upload_buttons_entry.pack(anchor="w", fill=tk.X, pady=(0, 5))
|
||||
self.upload_buttons_entry.insert(0, "Загрузить файл, Выбрать файл, Добавить файл")
|
||||
|
||||
upload_label = tk.Label(
|
||||
main_frame,
|
||||
text="(Кнопка для загрузки файла)",
|
||||
font=("Arial", 9),
|
||||
bg="#2C3E50",
|
||||
fg="#BDC3C7"
|
||||
)
|
||||
upload_label.pack(anchor="w", pady=(0, 10))
|
||||
|
||||
self.process_buttons_entry = tk.Entry(main_frame, font=self.font_label)
|
||||
self.process_buttons_entry.pack(anchor="w", fill=tk.X, pady=(0, 5))
|
||||
self.process_buttons_entry.insert(0, "Загрузить, Обработать, Преобразовать, Изменить, Конвертировать")
|
||||
|
||||
process_label = tk.Label(
|
||||
main_frame,
|
||||
text="(Кнопка для обработки/загрузки файла)",
|
||||
font=("Arial", 9),
|
||||
bg="#2C3E50",
|
||||
fg="#BDC3C7"
|
||||
)
|
||||
process_label.pack(anchor="w", pady=(0, 15))
|
||||
|
||||
# ===== ЛОГИРОВАНИЕ =====
|
||||
log_label = tk.Label(
|
||||
main_frame,
|
||||
text="📋 Логирование процесса:",
|
||||
font=self.font_label,
|
||||
bg="#2C3E50",
|
||||
fg="#ECF0F1"
|
||||
)
|
||||
log_label.pack(anchor="w", pady=(10, 5))
|
||||
|
||||
# Frame для логов с прокруткой
|
||||
log_frame = tk.Frame(main_frame, bg="#34495E")
|
||||
log_frame.pack(fill=tk.BOTH, expand=True, pady=(0, 15))
|
||||
|
||||
scrollbar = tk.Scrollbar(log_frame)
|
||||
scrollbar.pack(side="right", fill="y")
|
||||
|
||||
self.log_text = tk.Text(
|
||||
log_frame,
|
||||
font=("Courier", 9),
|
||||
bg="#1C2833",
|
||||
fg="#82E0AA",
|
||||
height=10,
|
||||
yscrollcommand=scrollbar.set
|
||||
)
|
||||
self.log_text.pack(fill=tk.BOTH, expand=True)
|
||||
scrollbar.config(command=self.log_text.yview)
|
||||
|
||||
# ===== КНОПКИ =====
|
||||
button_frame = tk.Frame(main_frame, bg="#2C3E50")
|
||||
button_frame.pack(fill=tk.X)
|
||||
|
||||
self.start_button = tk.Button(
|
||||
button_frame,
|
||||
text="🚀 Начать загрузку",
|
||||
command=self._start_processing,
|
||||
font=self.font_button,
|
||||
bg="#27AE60",
|
||||
fg="white",
|
||||
padx=20,
|
||||
pady=12
|
||||
)
|
||||
self.start_button.pack(side="right", padx=(10, 0))
|
||||
|
||||
self.clear_log_button = tk.Button(
|
||||
button_frame,
|
||||
text="🗑️ Очистить логи",
|
||||
command=self._clear_logs,
|
||||
font=self.font_button,
|
||||
bg="#E74C3C",
|
||||
fg="white",
|
||||
padx=15,
|
||||
pady=12
|
||||
)
|
||||
self.clear_log_button.pack(side="right")
|
||||
|
||||
# Перенаправление логов в текстовое поле
|
||||
self._setup_log_handler()
|
||||
|
||||
def _setup_log_handler(self):
|
||||
"""Настраивает обработчик логов в текстовое поле."""
|
||||
# Создаем кастомный обработчик для логирования в UI
|
||||
class TextHandler(logging.Handler):
|
||||
def __init__(self, text_widget):
|
||||
super().__init__()
|
||||
self.text_widget = text_widget
|
||||
|
||||
def emit(self, record):
|
||||
msg = self.format(record)
|
||||
self.text_widget.insert(tk.END, msg + "\n")
|
||||
self.text_widget.see(tk.END)
|
||||
self.text_widget.update()
|
||||
|
||||
handler = TextHandler(self.log_text)
|
||||
handler.setFormatter(logging.Formatter('%(asctime)s - %(levelname)s - %(message)s', datefmt='%H:%M:%S'))
|
||||
logging.getLogger().addHandler(handler)
|
||||
|
||||
def _select_directory(self):
|
||||
"""Открывает диалог выбора директории."""
|
||||
directory = filedialog.askdirectory(title="Выберите директорию с .xlsx файлами")
|
||||
if directory:
|
||||
self.dir_entry.delete(0, tk.END)
|
||||
self.dir_entry.insert(0, directory)
|
||||
logging.info(f"✅ Выбрана директория: {directory}")
|
||||
|
||||
def _clear_logs(self):
|
||||
"""Очищает логи."""
|
||||
self.log_text.delete(1.0, tk.END)
|
||||
logging.info("🗑️ Логи очищены")
|
||||
|
||||
def _start_processing(self):
|
||||
"""Запускает процесс обработки в отдельном потоке."""
|
||||
if self.processing:
|
||||
messagebox.showwarning("⚠️ Внимание", "Процесс уже запущен!")
|
||||
return
|
||||
|
||||
# Валидация входных данных
|
||||
directory = self.dir_entry.get().strip()
|
||||
url = self.url_entry.get().strip()
|
||||
|
||||
if not directory:
|
||||
messagebox.showerror("❌ Ошибка", "Выберите директорию с файлами!")
|
||||
return
|
||||
|
||||
if not Path(directory).exists():
|
||||
messagebox.showerror("❌ Ошибка", "Указанная директория не существует!")
|
||||
return
|
||||
|
||||
if not url or url == "https://":
|
||||
messagebox.showerror("❌ Ошибка", "Введите URL страницы!")
|
||||
return
|
||||
|
||||
# Парсим варианты кнопок
|
||||
upload_texts = [t.strip() for t in self.upload_buttons_entry.get().split(",")]
|
||||
process_texts = [t.strip() for t in self.process_buttons_entry.get().split(",")]
|
||||
|
||||
# Отключаем кнопку и запускаем обработку в потоке
|
||||
self.processing = True
|
||||
self.start_button.config(state="disabled", bg="#95A5A6")
|
||||
|
||||
thread = threading.Thread(
|
||||
target=self._processing_thread,
|
||||
args=(directory, url, upload_texts, process_texts),
|
||||
daemon=True
|
||||
)
|
||||
thread.start()
|
||||
|
||||
def _processing_thread(self, directory, url, upload_texts, process_texts):
|
||||
"""Потоковая функция обработки."""
|
||||
try:
|
||||
logging.info("\n" + "="*70)
|
||||
logging.info("🔄 ЗАПУСК АВТОМАТИЗАЦИИ ЗАГРУЗКИ ФАЙЛОВ")
|
||||
logging.info("="*70)
|
||||
|
||||
# Получаем путь для загрузок (рядом с программой)
|
||||
download_dir = str(Path(__file__).parent / "downloads")
|
||||
Path(download_dir).mkdir(exist_ok=True)
|
||||
|
||||
# Создаем процессор
|
||||
processor = FileProcessorService(directory, url, download_dir)
|
||||
|
||||
# Запускаем обработку
|
||||
results = processor.start_processing(upload_texts, process_texts)
|
||||
|
||||
# Показываем результаты
|
||||
self._show_results(results)
|
||||
|
||||
except Exception as e:
|
||||
logging.error(f"❌ Критическая ошибка: {e}")
|
||||
messagebox.showerror("❌ Ошибка", f"Произошла ошибка:\n{str(e)}")
|
||||
finally:
|
||||
self.processing = False
|
||||
self.start_button.config(state="normal", bg="#27AE60")
|
||||
|
||||
def _show_results(self, results):
|
||||
"""Показывает результаты обработки."""
|
||||
result_text = (
|
||||
f"✅ ОБРАБОТКА ЗАВЕРШЕНА\n\n"
|
||||
f"📊 Результаты:\n"
|
||||
f"• Всего файлов найдено: {results['total_files']}\n"
|
||||
f"• Обработано успешно: {results['processed_files']}\n"
|
||||
f"• Ошибок: {len(results['failed_files'])}\n"
|
||||
)
|
||||
|
||||
if results['failed_files']:
|
||||
result_text += f"\n❌ Файлы с ошибками:\n"
|
||||
for file in results['failed_files'][:5]:
|
||||
result_text += f" • {file}\n"
|
||||
if len(results['failed_files']) > 5:
|
||||
result_text += f" ... и еще {len(results['failed_files']) - 5}"
|
||||
|
||||
if results['errors']:
|
||||
result_text += f"\n⚠️ Системные ошибки:\n"
|
||||
for error in results['errors']:
|
||||
result_text += f" • {error}\n"
|
||||
|
||||
messagebox.showinfo("Результаты", result_text)
|
||||
|
||||
|
||||
def main():
|
||||
"""Главная функция."""
|
||||
root = tk.Tk()
|
||||
app = UIManager(root)
|
||||
root.mainloop()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user