Files
Touchh/antifroud/data_sync.py
2024-12-18 10:49:40 +09:00

337 lines
14 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.

import logging
import pymysql
from datetime import datetime
from urllib.parse import unquote, parse_qs
import pytz
from django.utils import timezone
from django.conf import settings
import chardet
import html
from hotels.models import Room, Hotel
from .models import UserActivityLog, ExternalDBSettings
class DatabaseConnector:
"""
Класс для подключения к внешней базе данных.
"""
def __init__(self, db_settings_id):
self.db_settings_id = db_settings_id
self.connection = None
self.logger = self.setup_logger()
self.db_settings = None
def setup_logger(self):
default_level = logging.INFO # Уровень по умолчанию
level_name = getattr(settings, "DATA_SYNC_LOG_LEVEL", "INFO").upper()
log_level = getattr(logging, level_name, default_level)
logger.setLevel(log_level)
# Настройка обработчика для файла
handler = logging.FileHandler("data_sync.log")
handler.setFormatter(logging.Formatter("%(asctime)s - %(levelname)s - %(message)s"))
handler.setLevel(log_level)
# Удаляем старые обработчики, чтобы избежать дублирования
if logger.hasHandlers():
logger.handlers.clear()
logger.addHandler(handler)
# Сообщение о текущем уровне логирования
logger.info(f"Уровень логирования установлен: {logging.getLevelName(log_level)}")
logger.addHandler(handler)
return logger
def connect(self):
"""Подключение к базе данных."""
try:
self.db_settings = ExternalDBSettings.objects.get(id=self.db_settings_id)
self.connection = pymysql.connect(
host=self.db_settings.host,
port=self.db_settings.port,
user=self.db_settings.user,
password=self.db_settings.password,
database=self.db_settings.database,
charset="utf8mb4",
cursorclass=pymysql.cursors.DictCursor,
)
self.logger.info("Подключение к базе данных успешно установлено.")
except Exception as e:
self.logger.error(f"Ошибка подключения к БД: {e}")
raise ConnectionError(e)
def close(self):
"""Закрывает соединение с базой данных."""
if self.connection:
self.connection.close()
self.logger.info("Соединение с базой данных закрыто.")
def execute_query(self, query):
"""Выполнение запроса и возврат результатов."""
with self.connection.cursor() as cursor:
cursor.execute(query)
return cursor.fetchall()
class DataProcessor:
"""
Обрабатывает и сохраняет данные.
"""
def __init__(self, logger):
self.logger = logger
def decode_html_entities(self, text):
"""Декодирует URL и HTML-сущности."""
if text and isinstance(text, str):
text = unquote(text)
text = html.unescape(text)
try:
detected = chardet.detect(text.encode())
encoding = detected['encoding']
if encoding and encoding != 'utf-8':
text = text.encode(encoding).decode('utf-8', errors='ignore')
except Exception as e:
self.logger.error(f"Ошибка кодировки: {e}")
return text
def parse_datetime(self, dt_str):
"""Преобразует строку даты в aware datetime."""
try:
if isinstance(dt_str, datetime):
return timezone.make_aware(dt_str) if timezone.is_naive(dt_str) else dt_str
if dt_str:
return timezone.make_aware(datetime.strptime(dt_str, "%Y-%m-%d %H:%M:%S"))
except Exception as e:
self.logger.error(f"Ошибка парсинга даты: {e}")
return None
def url_parameters_parser(self, url_parameters):
"""
Парсит строку URL-параметров и возвращает словарь с ключами и значениями.
Пример входа:
url_parameters = "utm_medium=qr-3&utm_content=chisto-pozhit&utm_term=101"
Возвращает:
{
"utm_medium": "qr-3",
"utm_content": "chisto-pozhit",
"utm_term": "101"
}
"""
try:
if not url_parameters:
return {}
# Декодируем параметры URL
decoded_params = unquote(url_parameters)
parsed_params = parse_qs(decoded_params)
# Преобразуем список значений в строку
result = {key: value[0] for key, value in parsed_params.items()}
return result
except Exception as e:
self.logger.error(f"Ошибка парсинга URL-параметров: {e}")
return {}
class HotelRoomManager:
"""
Управляет созданием отелей и номеров.
"""
def __init__(self, logger):
self.logger = logger
def get_or_create_hotel(self, hotel_id, page_title):
"""
Создает или получает отель.
:param hotel_id: Значение из utm_content (индекс отеля)
:param page_title: Название отеля из поля page_title
"""
if not hotel_id:
self.logger.warning("Пропущено создание отеля: отсутствует hotel_id.")
return None
# Создаем или получаем отель с hotel_id и устанавливаем name по page_title
hotel, created = Hotel.objects.get_or_create(
hotel_id=hotel_id,
defaults={
"name": html.unescape(page_title) or f"Отель {hotel_id}",
"description": "Автоматически добавленный отель"
}
)
if created:
self.logger.info(f"Создан отель '{hotel.name}' с hotel_id: {hotel_id}")
else:
self.logger.info(f"Отель '{hotel.name}' уже существует с hotel_id: {hotel_id}")
return hotel
def get_or_create_room(self, hotel, room_number):
"""
Создает или получает номер отеля.
:param hotel: Экземпляр модели Hotel
:param room_number: Номер комнаты из utm_term
"""
if not hotel:
self.logger.warning("Пропущено создание номера: отсутствует отель.")
return None
if not room_number:
self.logger.warning(f"Пропущено создание номера: отсутствует room_number для отеля {hotel.name}.")
return None
# Генерация уникального external_id на основе hotel_id и room_number
external_id = f"{hotel.hotel_id}_{room_number}".lower()
# Создаем или получаем номер
room, created = Room.objects.get_or_create(
hotel=hotel,
number=room_number,
defaults={
"number": room_number, # Используем room_number как название номера
"external_id": external_id,
"description": "Автоматически добавленный номер"
}
)
if created:
self.logger.info(f"Создан номер '{room.number}' (external_id: {external_id}) в отеле '{hotel.name}'")
else:
self.logger.info(f"Номер '{room.number}' уже существует в отеле '{hotel.name}'")
return room
class DataSyncManager:
"""
Главный класс для синхронизации данных.
"""
def __init__(self, db_settings_id):
self.logger = self.setup_logger()
self.db_connector = DatabaseConnector(db_settings_id)
self.data_processor = DataProcessor(self.logger)
self.hotel_manager = HotelRoomManager(self.logger)
def setup_logger(self):
logger = logging.getLogger(__name__)
logger.setLevel(logging.DEBUG)
handler = logging.FileHandler("data_sync.log")
handler.setFormatter(logging.Formatter("%(asctime)s - %(levelname)s - %(message)s"))
logger.addHandler(handler)
return logger
def get_last_saved_record(self):
"""Получает ID последней записи."""
record = UserActivityLog.objects.order_by("-id").first()
return record.id if record else 0
def fetch_new_data(self, last_id):
"""Получает новые данные из БД."""
query = f"""
SELECT * FROM `{self.db_connector.db_settings.table_name}`
WHERE id > {last_id}
AND url_parameters IS NOT NULL
AND url_parameters LIKE '%utm_medium%'
AND page_url IS NOT NULL
ORDER BY id ASC
LIMIT 1000;
"""
self.logger.info(f"Запрос на получение новых данных отправлен. \n Содержание запроса: {query}")
return self.db_connector.execute_query(query)
def process_and_save_data(self, rows):
"""
Обрабатывает и сохраняет данные из внешней базы данных.
"""
for row in rows:
try:
# Декодирование URL-параметров
url_params = self.data_processor.decode_html_entities(row.get("url_parameters", ""))
params = self.data_processor.url_parameters_parser(url_params)
timestamp = params.get("timestamp")
date_time = params.get("date_time")
# Извлечение данных
hotel_id = params.get("utm_content")
room_number = params.get("utm_term")
page_title = row.get("page_title") # Название отеля из page_title
external_id = row.get("id")
# Создание отеля и комнаты
hotel = self.hotel_manager.get_or_create_hotel(hotel_id, page_title)
room = self.hotel_manager.get_or_create_room(hotel, room_number)
page_url = row.get("page_url")
# Заполнение записи
UserActivityLog.objects.update_or_create(
external_id=external_id,
defaults={
"user_id": row.get("user_id") or 0,
"timestamp": row.get("timestamp"),
"date_time": row.get("date_time"),
"ip": row.get("ip") or "0.0.0.0",
"created": self.data_processor.parse_datetime(row.get("created")) or timezone.now(),
"url_parameters": url_params,
"page_id": room.id if room else None,
"page_title": html.unescape(page_title),
"hits": row.get("hits") or 0,
"page_url": html.unescape(page_url),
}
)
self.logger.info(f"Запись ID {external_id} успешно обработана.")
except Exception as e:
self.logger.error(f"Ошибка при обработке записи ID {row.get('id')}: {e}")
def sync(self):
"""Запускает процесс синхронизации."""
self.db_connector.connect()
try:
last_id = self.get_last_saved_record()
rows = self.fetch_new_data(last_id)
self.process_and_save_data(rows)
self.logger.info("Синхронизация завершена.")
finally:
self.db_connector.close()
import logging
from concurrent.futures import ThreadPoolExecutor
from .models import ExternalDBSettings
logger = logging.getLogger(__name__)
logger.setLevel(logging.DEBUG)
def scheduled_sync():
"""
Планировщик синхронизации для всех активных подключений.
Каждое подключение обрабатывается отдельно.
"""
logger.info("Запуск планировщика синхронизации.")
# Получаем все активные настройки подключения
active_db_settings = ExternalDBSettings.objects.filter(is_active=True)
if not active_db_settings.exists():
logger.warning("Не найдено активных подключений для синхронизации.")
return
logger.info(f"Найдено активных подключений: {len(active_db_settings)}")
def sync_task(db_settings):
"""
Выполняет синхронизацию для одного подключения.
"""
try:
logger.info(f"Начало синхронизации для подключения: {db_settings.name} (ID={db_settings.id})")
sync_manager = DataSyncManager(db_settings.id)
sync_manager.sync()
logger.info(f"Синхронизация успешно завершена для подключения: {db_settings.name}")
except Exception as e:
logger.error(f"Ошибка синхронизации для подключения {db_settings.name}: {e}")
# Параллельное выполнение задач синхронизации
with ThreadPoolExecutor(max_workers=5) as executor: # Максимальное количество потоков = 5
for db_settings in active_db_settings:
executor.submit(sync_task, db_settings)
logger.info("Планировщик синхронизации завершил работу.")