Files
mario_scraper/web_interface.py
2025-04-17 13:56:40 +03:00

614 lines
21 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
from flask import Flask, render_template, request, jsonify, send_from_directory, redirect, url_for
from flask_login import LoginManager, UserMixin, login_user, login_required, logout_user, current_user
from werkzeug.security import generate_password_hash, check_password_hash
import re
from euro_scraper import create_retry_session, fetch_products, setup_selenium
import os
import threading
from datetime import datetime
import json
from translator import ProductTranslator
from feed_generator import RobotVacuumYMLGenerator
from pathlib import Path
from werkzeug.exceptions import NotFound
from urllib.parse import urljoin
BASE_URL = "https://mario.mrakells.pp.ua"
# Добавляем в начало файла
login_manager = LoginManager()
login_manager.login_view = 'login'
class User(UserMixin):
def __init__(self, id, username, password_hash):
self.id = id
self.username = username
self.password_hash = password_hash
# Хранилище пользователей (в реальном приложении использовать базу данных)
users = {
'mario': User(1, 'mario', generate_password_hash('2htC9YlEMXAhNE'))
}
@login_manager.user_loader
def load_user(user_id):
for user in users.values():
if user.id == int(user_id):
return user
return None
app = Flask(__name__)
# Добавляем после создания app
login_manager.init_app(app)
app.config['SECRET_KEY'] = 'your-secret-key-here' # Замените на случайный ключ
# Глобальные настройки
app_settings = {"items_limit": -1} # Ограничение количества обрабатываемых товаров
# Глобальная переменная для хранения статуса перевода
translation_status = {
"is_running": False,
"total_items": 0,
"processed_items": 0,
"error": None,
}
# Добавить в начало файла
CATEGORIES_FILE = "categories.json"
# Создаем константы для путей
OUTPUT_DIR = Path("output")
TRANSLATED_DIR = OUTPUT_DIR / "translated"
YML_DIR = OUTPUT_DIR / "yml"
# Глобальное состояние парсинга
parsing_status = {
"is_running": False,
"total_items": 0,
"processed_items": 0,
"error": None,
}
def load_categories():
"""Загрузка категорий из файла"""
if os.path.exists(CATEGORIES_FILE):
with open(CATEGORIES_FILE, "r", encoding="utf-8") as f:
return json.load(f)
return []
def save_categories(categories):
"""Сохранение категорий в файл"""
with open(CATEGORIES_FILE, "w", encoding="utf-8") as f:
json.dump(categories, f, ensure_ascii=False, indent=2)
def extract_category(url: str) -> str:
"""Извлекает название категории из URL"""
# Пример URL: https://www.euro.com.pl/odkurzacze-automatyczne.bhtml
match = re.search(r"euro\.com\.pl/([^/]+)", url)
if match:
category = match.group(1).replace(".bhtml", "")
return category
return None
def start_parsing(category):
"""Запуск парсинга категории"""
global parsing_status
try:
parsing_status.update(
{"is_running": True, "total_items": 0, "processed_items": 0, "error": None}
)
# Создаем сессию и драйвер Selenium
session = create_retry_session()
driver = setup_selenium()
try:
# Парсим с использованием драйвера
products = fetch_products(category, session, driver, parsing_status)
# Сохраняем результаты
if products:
output_file = os.path.join("output", f"{category}_products.json")
with open(output_file, "w", encoding="utf-8") as f:
json.dump(products, f, ensure_ascii=False, indent=2)
finally:
# Обязательно закрываем драйвер
driver.quit()
except Exception as e:
parsing_status["error"] = str(e)
print(f"Error during parsing: {e}")
finally:
parsing_status["is_running"] = False
def get_file_info(filename, directory="output"):
"""Получение информации о файле"""
filepath = os.path.join(directory, filename)
stat = os.stat(filepath)
return {
"name": filename,
"modified": datetime.fromtimestamp(stat.st_mtime).strftime("%d.%m.%Y %H:%M:%S"),
"size": f"{stat.st_size / 1024:.1f} KB",
}
def get_oldest_parsed_file():
"""Повертає найстаріший _products.json файл"""
folder = "output"
files = [
f for f in os.listdir(folder)
if f.endswith("_products.json") and not f.endswith("_translated_products.json")
]
if not files:
return None
oldest_file = min(files, key=lambda f: os.path.getmtime(os.path.join(folder, f)))
category = oldest_file.replace("_products.json", "")
return category
@app.route('/login', methods=['GET', 'POST'])
def login():
if request.method == 'POST':
username = request.form.get('username')
password = request.form.get('password')
user = users.get(username)
if user and check_password_hash(user.password_hash, password):
login_user(user)
return redirect(url_for('index'))
return render_template('login.html', error='Неверный логин или пароль')
return render_template('login.html')
@app.route('/logout')
@login_required
def logout():
logout_user()
return redirect(url_for('login'))
@app.route("/")
@login_required
def index():
"""Главная страница"""
# Получаем спарсенные файлы
parsed_files = []
if os.path.exists("output"):
files = [f for f in os.listdir("output") if f.endswith("_products.json")]
parsed_files = [get_file_info(f, "output") for f in files]
# Получаем переведенные файлы
translated_files = []
if os.path.exists("output/translated"):
files = [
f
for f in os.listdir("output/translated")
if f.endswith("_translated_products.json")
]
translated_files = [get_file_info(f, "output/translated") for f in files]
# Получаем YML файлы
yml_files = []
if os.path.exists("output/yml"):
files = [f for f in os.listdir("output/yml") if f.endswith(".yml")]
yml_files = [get_file_info(f, "output/yml") for f in files]
# Загружаем категории
categories = load_categories()
return render_template(
"index.html",
status=parsing_status,
translation_status=translation_status,
parsed_files=parsed_files,
translated_files=translated_files,
yml_files=yml_files,
categories=categories,
app_settings=app_settings,
)
@app.route("/auto-refresh", methods=["POST"])
def auto_refresh():
"""Запускає парсинг найстарішої збереженої категорії"""
if parsing_status["is_running"]:
return jsonify({"error": "Парсинг уже запущено"})
category = get_oldest_parsed_file()
if not category:
return jsonify({"error": "Немає жодної категорії для оновлення"})
print(f"[AUTO REFRESH] Повторний парсинг для категорії: {category}")
thread = threading.Thread(target=start_parsing, args=(category,))
thread.start()
return jsonify({"success": True, "category": category})
@app.route("/parse", methods=["POST"])
@login_required
def parse():
"""Обработчик запуска парсинга"""
url = request.form.get("url")
if not url:
return jsonify({"error": "URL не указан"})
category = extract_category(url)
if not category:
return jsonify({"error": "Неверный формат URL"})
if parsing_status["is_running"]:
return jsonify({"error": "Парсинг уже запущен"})
# Запускаем парсинг в отдельном потоке
thread = threading.Thread(target=start_parsing, args=(category,))
thread.start()
return jsonify({"status": "ok"})
@app.route("/status")
def get_status():
"""Получение статуса парсинга"""
return jsonify(parsing_status)
@app.route("/download/<path:filename>")
def download_file(filename):
"""Скачивание файла с результатами"""
directory = request.args.get(
"directory", "output"
) # Получаем директорию из параметров запроса
if directory == "translated":
directory = "output/translated"
elif directory == "yml":
directory = "output/yml"
else:
directory = "output"
return send_from_directory(directory, filename, as_attachment=True)
@app.route("/delete/<path:filename>", methods=["POST"])
def delete_file(filename):
"""Удаление файла"""
try:
directory = request.args.get("directory", "output")
if directory == "translated":
file_path = os.path.join("output/translated", filename)
elif directory == "yml":
file_path = os.path.join("output/yml", filename)
else:
file_path = os.path.join("output", filename)
if os.path.exists(file_path):
os.remove(file_path)
return jsonify({"success": True})
else:
return jsonify({"error": "Файл не найден"}), 404
except Exception as e:
return jsonify({"error": str(e)}), 400
@app.route("/translate", methods=["POST"])
def translate():
"""Обработчик запуска перевода"""
if translation_status["is_running"]:
return jsonify({"error": "Перевод уже запущен"})
filename = request.form.get("filename")
if not filename:
return jsonify({"error": "Файл не выбран"})
# Запускаем перевод в отдельном потоке
thread = threading.Thread(target=start_translation, args=(filename,))
thread.start()
return jsonify({"status": "Перевод запущен"})
@app.route("/translation-status")
def get_translation_status():
"""Возвращает текущий статус перевода"""
return jsonify(translation_status)
def start_translation(filename: str):
"""Функция для запуска перевода в отдельнм потоке"""
global translation_status
translation_status["is_running"] = True
translation_status["processed_items"] = 0
translation_status["error"] = None
try:
os.makedirs("output/translated", exist_ok=True)
with open(os.path.join("output", filename), "r", encoding="utf-8") as f:
products = json.load(f)
# Ограничиваем количество товаров только если лимит больше 0
if app_settings["items_limit"] > 0:
products = products[: app_settings["items_limit"]]
translation_status["total_items"] = len(products)
# Создаем экземпляр переводчика
translator = ProductTranslator()
# Переводим товары
translated_products = []
for i, product in enumerate(products):
translated_product = translator.translate_product(product)
translated_products.append(translated_product)
translation_status["processed_items"] = i + 1
# Сохраняем переведенные данные в отдельную директорию
output_filename = filename.replace(
"_products.json", "_translated_products.json"
)
with open(
os.path.join("output/translated", output_filename), "w", encoding="utf-8"
) as f:
json.dump(translated_products, f, ensure_ascii=False, indent=2)
except Exception as e:
translation_status["error"] = str(e)
print(f"Ошибка перевода: {e}")
finally:
translation_status["is_running"] = False
@app.route("/update-settings", methods=["POST"])
def update_settings():
"""Обновление настроек приложения"""
try:
data = request.json
if "items_limit" in data:
items_limit = int(data["items_limit"])
if items_limit == -1 or items_limit >= 1:
app_settings["items_limit"] = items_limit
return jsonify({"success": True})
else:
return jsonify({"error": "Значение должно быть -1 или больше 0"})
except Exception as e:
return jsonify({"error": str(e)})
@app.route("/generate-yml", methods=["POST"])
def generate_yml():
"""Обработчик генерации YML файла"""
try:
data = request.get_json()
print(f"Received data: {data}")
filename = data.get("filename")
category_id = data.get("category_id")
if not filename or not category_id:
return jsonify({"error": "Не вказано файл або категорію"})
# Загружаем категории
categories = load_categories()
category = next((c for c in categories if str(c["id"]) == str(category_id)), None)
if not category:
return jsonify({"error": "Категорія не знайдена"})
portal_category_id = category.get("portal_id")
if not portal_category_id:
return jsonify({"error": "Категорія не має portal_id (ідентифікатор категорії Prom.ua)"})
os.makedirs("output/yml", exist_ok=True)
# Читаем JSON файл с переведенными товарами
input_path = os.path.join("output/translated", filename)
if not os.path.exists(input_path):
return jsonify({"error": "Файл з товарами не знайдено"})
with open(input_path, "r", encoding="utf-8") as f:
products = json.load(f)
# Присваиваем portal_category_id всем товарам
for product in products:
product["portal_category_id"] = portal_category_id
product["local_category_id"] = category["id"]
# Создаем генератор YML с указанием базового URL
generator = RobotVacuumYMLGenerator(base_url=BASE_URL)
generator.add_category(str(category["id"]), category["name"])
# Генерируем имя выходного файла
output_filename = filename.replace("_translated_products.json", ".yml")
output_path = os.path.join("output/yml", output_filename)
# Генерируем YML файл
result = generator.generate_yml(products, output_path)
if result:
return jsonify({"success": True})
else:
return jsonify({"error": "Помилка при генерації YML файлу"})
except Exception as e:
print(f"Error generating YML: {str(e)}")
return jsonify({"error": str(e)})
@app.route("/add-category", methods=["POST"])
def add_category():
"""Додавання нової категорії (локальної + portal_id)"""
try:
data = request.json
categories = load_categories()
# Перевірка обов'язкових полів
if "id" not in data or "name" not in data:
return jsonify({"error": "Обов'язкові поля: id, name"})
# Перевірка унікальності ID
if any(str(c["id"]) == str(data["id"]) for c in categories):
return jsonify({"error": "Категорія з таким ID вже існує"})
# Додаємо категорію з optional portal_id
new_category = {
"id": data["id"],
"name": data["name"],
}
if "portal_id" in data:
new_category["portal_id"] = data["portal_id"]
categories.append(new_category)
save_categories(categories)
return jsonify({"success": True})
except Exception as e:
return jsonify({"error": str(e)})
@app.route("/delete-category", methods=["POST"])
def delete_category():
"""Удаление категории"""
try:
data = request.json
categories = load_categories()
categories = [c for c in categories if c["id"] != data["id"]]
save_categories(categories)
return jsonify({"success": True})
except Exception as e:
return jsonify({"error": str(e)})
@app.route("/get-yml-files")
def get_yml_files():
"""Получение списка YML файлов"""
yml_files = []
if os.path.exists("output/yml"):
files = [f for f in os.listdir("output/yml") if f.endswith(".yml")]
yml_files = [get_file_info(f, "output/yml") for f in files]
return jsonify(yml_files)
@app.route("/get-translated-files")
def get_translated_files():
"""Получение списка переведенных файлов"""
translated_files = []
if os.path.exists("output/translated"):
files = [
f
for f in os.listdir("output/translated")
if f.endswith("_translated_products.json")
]
translated_files = [get_file_info(f, "output/translated") for f in files]
return jsonify(translated_files)
@app.route("/get-parsed-files")
def get_parsed_files():
"""Получение списа спарсенных файлов"""
parsed_files = []
if os.path.exists("output"):
files = [
f
for f in os.listdir("output")
if f.endswith("_products.json")
and not f.endswith("_translated_products.json")
]
parsed_files = [get_file_info(f, "output") for f in files]
return jsonify(parsed_files)
@app.errorhandler(404)
def not_found_error(error):
return jsonify({"error": "Файл не найден"}), 404
@app.errorhandler(500)
def internal_error(error):
return jsonify({"error": "Внутренняя ошибка сервера"}), 500
@app.route("/get-files/<file_type>")
def get_files(file_type):
"""Получение списка файлов"""
files = []
if file_type == "parsed":
directory = "output"
pattern = lambda f: f.endswith("_products.json") and not f.endswith(
"_translated_products.json"
)
elif file_type == "translated":
directory = "output/translated"
pattern = lambda f: f.endswith("_translated_products.json")
elif file_type == "yml":
directory = "output/yml"
pattern = lambda f: f.endswith(".yml")
else:
return jsonify([])
if os.path.exists(directory):
files = [f for f in os.listdir(directory) if pattern(f)]
files = [get_file_info(f, directory) for f in files]
return jsonify(files)
# Добавляем роуты для отдачи изображений
@app.route("/images/products/<path:filename>")
def serve_product_image(filename):
"""Отдача изображений товаров"""
return send_from_directory("images/products", filename)
@app.route("/images/descriptions/<path:filename>")
def serve_description_image(filename):
"""Отдача изображений описаний"""
return send_from_directory("images/descriptions", filename)
# Добавляем функцию для получения полного URL изображения
def get_image_url(local_path: str) -> str:
"""Преобразует локальный путь в полный URL"""
if not local_path:
return None
return urljoin(BASE_URL, local_path)
def get_file_info(filename, directory):
"""Получение информации о файле"""
path = os.path.join(directory, filename)
stat = os.stat(path)
return {
"name": filename,
"modified": datetime.fromtimestamp(stat.st_mtime).strftime("%Y-%m-%d %H:%M:%S"),
"size": f"{stat.st_size / 1024:.1f} KB",
}
if __name__ == "__main__":
# Создаем необходимые директории
for directory in ["output", "output/translated", "output/yml"]:
os.makedirs(directory, exist_ok=True)
# Создаем файл категорий, если его нет
if not os.path.exists(CATEGORIES_FILE):
save_categories([])
app.run(host='0.0.0.0', port=5000, debug=True)