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