Enhance application with new features, improved error handling, and performance optimizations. Key updates include: added data validation, retry strategies for HTTP requests, detailed logging, and support for RabbitMQ exports. Updated dependencies and enhanced README documentation for better setup instructions.
This commit is contained in:
@@ -4,8 +4,69 @@
|
||||
# Семантика: Этот модуль является единственным источником истины для всех
|
||||
# конфигурационных параметров приложения. Использует Pydantic для типизации и валидации.
|
||||
|
||||
import os
|
||||
from pathlib import Path
|
||||
from pydantic import BaseModel, Field
|
||||
from pydantic import BaseModel, Field, validator, HttpUrl
|
||||
from typing import Optional
|
||||
from dotenv import load_dotenv
|
||||
|
||||
# ANCHOR: Environment_Loading
|
||||
# Семантика: Загрузка переменных окружения из .env файла
|
||||
load_dotenv()
|
||||
|
||||
# ANCHOR: Base_Paths
|
||||
# Семантика: Базовые пути для приложения
|
||||
BASE_DIR = Path(__file__).parent.parent.parent
|
||||
DATA_DIR = BASE_DIR / "price_data_final"
|
||||
|
||||
# ANCHOR: Database_Settings
|
||||
# Семантика: Настройки базы данных SQLite
|
||||
DATABASE_URL = os.getenv("DATABASE_URL", f"sqlite:///{BASE_DIR}/price_parser.db")
|
||||
|
||||
# ANCHOR: Scraping_Settings
|
||||
# Семантика: Настройки для веб-скрапинга
|
||||
SCRAPING_DELAY = float(os.getenv("SCRAPING_DELAY", "1.0")) # Задержка между запросами в секундах
|
||||
MAX_RETRIES = int(os.getenv("MAX_RETRIES", "3")) # Максимальное количество попыток
|
||||
REQUEST_TIMEOUT = int(os.getenv("REQUEST_TIMEOUT", "30")) # Таймаут запросов в секундах
|
||||
USER_AGENT = os.getenv("USER_AGENT", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36")
|
||||
|
||||
# ANCHOR: Logging_Settings
|
||||
# Семантика: Настройки логирования
|
||||
LOG_LEVEL = os.getenv("LOG_LEVEL", "INFO")
|
||||
LOG_FORMAT = os.getenv("LOG_FORMAT", "%(asctime)s - %(name)s - %(levelname)s - %(message)s")
|
||||
LOG_FILE = os.getenv("LOG_FILE", str(BASE_DIR / "logs" / "price_parser.log"))
|
||||
|
||||
# ANCHOR: RabbitMQ_Settings
|
||||
# Семантика: Настройки для подключения к RabbitMQ
|
||||
RABBITMQ_HOST = os.getenv("RABBITMQ_HOST", "localhost")
|
||||
RABBITMQ_PORT = int(os.getenv("RABBITMQ_PORT", "5672"))
|
||||
RABBITMQ_USERNAME = os.getenv("RABBITMQ_USERNAME", "guest")
|
||||
RABBITMQ_PASSWORD = os.getenv("RABBITMQ_PASSWORD", "guest")
|
||||
RABBITMQ_VIRTUAL_HOST = os.getenv("RABBITMQ_VIRTUAL_HOST", "/")
|
||||
|
||||
# ANCHOR: RabbitMQ_Queue_Settings
|
||||
# Семантика: Настройки очередей RabbitMQ
|
||||
RABBITMQ_PRODUCTS_QUEUE = os.getenv("RABBITMQ_PRODUCTS_QUEUE", "price_parser.products")
|
||||
RABBITMQ_LOGS_QUEUE = os.getenv("RABBITMQ_LOGS_QUEUE", "price_parser.logs")
|
||||
RABBITMQ_EXCHANGE = os.getenv("RABBITMQ_EXCHANGE", "price_parser.exchange")
|
||||
|
||||
# ANCHOR: RabbitMQ_Connection_Settings
|
||||
# СEMАНТИКА: Настройки подключения к RabbitMQ
|
||||
RABBITMQ_CONNECTION_TIMEOUT = int(os.getenv("RABBITMQ_CONNECTION_TIMEOUT", "30"))
|
||||
RABBITMQ_HEARTBEAT = int(os.getenv("RABBITMQ_HEARTBEAT", "600"))
|
||||
RABBITMQ_BLOCKED_CONNECTION_TIMEOUT = int(os.getenv("RABBITMQ_BLOCKED_CONNECTION_TIMEOUT", "300"))
|
||||
|
||||
# ANCHOR: Export_Settings
|
||||
# Семантика: Настройки экспорта данных
|
||||
ENABLE_RABBITMQ_EXPORT = os.getenv("ENABLE_RABBITMQ_EXPORT", "false").lower() == "true"
|
||||
ENABLE_CSV_EXPORT = os.getenv("ENABLE_CSV_EXPORT", "true").lower() == "true"
|
||||
ENABLE_DATABASE_EXPORT = os.getenv("ENABLE_DATABASE_EXPORT", "true").lower() == "true"
|
||||
|
||||
# ANCHOR: Validation_Settings
|
||||
# Семантика: Настройки валидации данных
|
||||
VALIDATE_DATA_BEFORE_EXPORT = os.getenv("VALIDATE_DATA_BEFORE_EXPORT", "true").lower() == "true"
|
||||
|
||||
# [COHERENCE_CHECK_PASSED] Все настройки определены с разумными значениями по умолчанию.
|
||||
|
||||
class ScraperSelectors(BaseModel):
|
||||
"""
|
||||
@@ -20,6 +81,13 @@ class ScraperSelectors(BaseModel):
|
||||
product_page_name: str = Field(..., alias='PRODUCT_PAGE_NAME')
|
||||
active_volume: str = Field(..., alias='ACTIVE_VOLUME')
|
||||
price_block: str = Field(..., alias='PRICE_BLOCK')
|
||||
|
||||
@validator('*')
|
||||
def validate_selectors(cls, v):
|
||||
"""[VALIDATOR] Проверяет, что селекторы не пустые."""
|
||||
if not v or not v.strip():
|
||||
raise ValueError('Селектор не может быть пустым')
|
||||
return v.strip()
|
||||
|
||||
class Settings(BaseModel):
|
||||
"""
|
||||
@@ -27,19 +95,27 @@ class Settings(BaseModel):
|
||||
@description: Главный класс конфигурации приложения. Собирает все настройки в одном месте.
|
||||
"""
|
||||
# [CONFIG] Основные настройки парсера
|
||||
base_url: str = 'https://elixirpeptide.ru'
|
||||
catalog_url: str = 'https://elixirpeptide.ru/catalog/'
|
||||
headers: dict = {
|
||||
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36'
|
||||
}
|
||||
base_url: str = Field(default='https://elixirpeptide.ru', description="Базовый URL сайта")
|
||||
catalog_url: str = Field(default='https://elixirpeptide.ru/catalog/', description="URL каталога товаров")
|
||||
headers: dict = Field(
|
||||
default={
|
||||
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36'
|
||||
},
|
||||
description="HTTP заголовки для запросов"
|
||||
)
|
||||
|
||||
# [CONFIG] Настройки вывода
|
||||
output_dir: Path = Path('price_data_final')
|
||||
save_to_csv: bool = True
|
||||
save_to_db: bool = True
|
||||
output_dir: Path = Field(default=Path('price_data_final'), description="Директория для сохранения результатов")
|
||||
save_to_csv: bool = Field(default=True, description="Сохранять ли данные в CSV")
|
||||
save_to_db: bool = Field(default=True, description="Сохранять ли данные в базу данных")
|
||||
|
||||
# [CONFIG] Настройки логирования
|
||||
log_to_db: bool = True
|
||||
log_to_db: bool = Field(default=True, description="Сохранять ли логи в базу данных")
|
||||
|
||||
# [ENHANCEMENT] Настройки производительности
|
||||
request_timeout: int = Field(default=30, description="Таймаут HTTP запросов в секундах")
|
||||
delay_between_requests: float = Field(default=1.0, description="Задержка между запросами в секундах")
|
||||
max_retries: int = Field(default=3, description="Максимальное количество попыток для запросов")
|
||||
|
||||
# [CONFIG] Вложенная модель с селекторами
|
||||
# Мы инициализируем ее прямо здесь, передавая словарь со значениями.
|
||||
@@ -50,6 +126,40 @@ class Settings(BaseModel):
|
||||
ACTIVE_VOLUME='.product-version-select li.active',
|
||||
PRICE_BLOCK='.product-sale-box .price span',
|
||||
)
|
||||
|
||||
@validator('base_url', 'catalog_url')
|
||||
def validate_urls(cls, v):
|
||||
"""[VALIDATOR] Проверяет корректность URL."""
|
||||
if not v.startswith(('http://', 'https://')):
|
||||
raise ValueError('URL должен начинаться с http:// или https://')
|
||||
return v
|
||||
|
||||
@validator('request_timeout')
|
||||
def validate_timeout(cls, v):
|
||||
"""[VALIDATOR] Проверяет корректность таймаута."""
|
||||
if v <= 0:
|
||||
raise ValueError('Таймаут должен быть положительным числом')
|
||||
if v > 300: # 5 минут максимум
|
||||
raise ValueError('Таймаут не может превышать 300 секунд')
|
||||
return v
|
||||
|
||||
@validator('delay_between_requests')
|
||||
def validate_delay(cls, v):
|
||||
"""[VALIDATOR] Проверяет корректность задержки."""
|
||||
if v < 0:
|
||||
raise ValueError('Задержка не может быть отрицательной')
|
||||
if v > 60: # 1 минута максимум
|
||||
raise ValueError('Задержка не может превышать 60 секунд')
|
||||
return v
|
||||
|
||||
@validator('max_retries')
|
||||
def validate_retries(cls, v):
|
||||
"""[VALIDATOR] Проверяет корректность количества попыток."""
|
||||
if v < 0:
|
||||
raise ValueError('Количество попыток не может быть отрицательным')
|
||||
if v > 10: # 10 попыток максимум
|
||||
raise ValueError('Количество попыток не может превышать 10')
|
||||
return v
|
||||
|
||||
@property
|
||||
def db_path(self) -> Path:
|
||||
@@ -58,9 +168,70 @@ class Settings(BaseModel):
|
||||
Гарантирует, что путь всегда будет актуальным, если изменится output_dir.
|
||||
"""
|
||||
return self.output_dir / 'parser_data.db'
|
||||
|
||||
def validate_configuration(self) -> list[str]:
|
||||
"""
|
||||
[NEW] Валидирует всю конфигурацию и возвращает список ошибок.
|
||||
|
||||
Returns:
|
||||
list[str]: Список ошибок конфигурации (пустой, если все корректно)
|
||||
"""
|
||||
errors = []
|
||||
|
||||
# Проверка доступности директории
|
||||
try:
|
||||
self.output_dir.mkdir(parents=True, exist_ok=True)
|
||||
except Exception as e:
|
||||
errors.append(f"Не удается создать директорию {self.output_dir}: {e}")
|
||||
|
||||
# Проверка URL
|
||||
try:
|
||||
import requests
|
||||
response = requests.head(self.base_url, timeout=10)
|
||||
if response.status_code >= 400:
|
||||
errors.append(f"Базовый URL недоступен: {self.base_url} (статус: {response.status_code})")
|
||||
except Exception as e:
|
||||
errors.append(f"Не удается подключиться к базовому URL {self.base_url}: {e}")
|
||||
|
||||
return errors
|
||||
|
||||
# [ENHANCEMENT] Загрузка настроек из переменных окружения
|
||||
def load_settings_from_env() -> Settings:
|
||||
"""
|
||||
[NEW] Загружает настройки из переменных окружения.
|
||||
|
||||
Returns:
|
||||
Settings: Объект настроек
|
||||
"""
|
||||
# Загружаем .env файл, если он существует
|
||||
env_file = Path('.env')
|
||||
if env_file.exists():
|
||||
try:
|
||||
from dotenv import load_dotenv
|
||||
load_dotenv()
|
||||
except ImportError:
|
||||
pass # python-dotenv не установлен
|
||||
|
||||
# Создаем настройки с возможностью переопределения через переменные окружения
|
||||
settings_data = {
|
||||
'base_url': os.getenv('PARSER_BASE_URL', 'https://elixirpeptide.ru'),
|
||||
'catalog_url': os.getenv('PARSER_CATALOG_URL', 'https://elixirpeptide.ru/catalog/'),
|
||||
'save_to_csv': os.getenv('PARSER_SAVE_TO_CSV', 'true').lower() == 'true',
|
||||
'save_to_db': os.getenv('PARSER_SAVE_TO_DB', 'true').lower() == 'true',
|
||||
'log_to_db': os.getenv('PARSER_LOG_TO_DB', 'true').lower() == 'true',
|
||||
'request_timeout': int(os.getenv('PARSER_TIMEOUT', '30')),
|
||||
'delay_between_requests': float(os.getenv('PARSER_DELAY', '1.0')),
|
||||
'max_retries': int(os.getenv('PARSER_RETRIES', '3')),
|
||||
}
|
||||
|
||||
return Settings(**settings_data)
|
||||
|
||||
# [SINGLETON] Создаем единственный экземпляр настроек, который будет использоваться
|
||||
# во всем приложении. Это стандартная практика для работы с конфигурацией.
|
||||
settings = Settings()
|
||||
try:
|
||||
settings = load_settings_from_env()
|
||||
except Exception as e:
|
||||
# Fallback к настройкам по умолчанию
|
||||
settings = Settings()
|
||||
|
||||
# [REFACTORING_COMPLETE] Этот модуль готов к использованию.
|
||||
Reference in New Issue
Block a user