domovoy_bot/services/topics_analyzer.py
Admin ba4676b0af v1.0 MVP - Инициализация проекта 🏠
 Регистрация и верификация жильцов
 Рейтинг активности (уровни)
 Детект шпионов (Score 0-100)
 Анти-мат система (3 предупреждения → бан)
 Админ-панель с рассылкой
 Учёт квартир (несколько жильцов)
 Телефоны экстренных служб и мастеров
 Еженедельный экспорт JSON
 Прокси (socks5) для обхода РКН

Бекап: backups/versions/v1.0_mvp_2026-02-26/

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2026-02-27 10:54:16 +00:00

278 lines
11 KiB
Python
Raw Permalink Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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.

"""
Topics Analyzer — анализ тем чата, облако тегов
Классификация сообщений по темам и категориям
"""
import logging
import re
from datetime import datetime, timedelta
from typing import Dict, List, Tuple, Optional
from collections import defaultdict, Counter
from sqlalchemy import select, func
from sqlalchemy.ext.asyncio import AsyncSession
logger = logging.getLogger(__name__)
class TopicsAnalyzer:
"""Анализатор тем чата"""
def __init__(self, session: AsyncSession):
self.session = session
self.topics = self._load_topics()
def _load_topics(self) -> Dict:
"""Загрузить темы и ключевые слова"""
return {
'лифты': {
'keywords': ['лифт', 'лифты', 'лифтовая', 'кабина', 'шахта лифта', 'лифт застрял', 'лифт не работает'],
'emoji': '🛗'
},
'парковка': {
'keywords': ['парковк', 'машина', 'автомобиль', 'место парковк', 'припаркован', 'шлагбаум'],
'emoji': '🚗'
},
'уборка': {
'keywords': ['уборк', 'грязн', 'мусор', 'подъезд', 'лестниц', 'пол', 'пыль'],
'emoji': '🧹'
},
'соседи': {
'keywords': ['сосед', 'соседи', 'шум', 'громко', 'музыка', 'ремонт', 'сверлит'],
'emoji': '👥'
},
'вода': {
'keywords': ['вод', 'кран', 'труб', 'протечк', 'засор', 'канализаци', 'стояк'],
'emoji': '💧'
},
'электричество': {
'keywords': ['свет', 'электричеств', 'розетк', 'проводк', 'автомат', 'лампочк', 'мигает'],
'emoji': ''
},
'отопление': {
'keywords': ['отоплен', 'батаре', 'радиатор', 'тепло', 'холод', 'температур'],
'emoji': '🔥'
},
'домофон': {
'keywords': ['домофон', 'ключ', 'карта', 'доступ', 'вход', 'дверь'],
'emoji': '🔑'
},
'мусор': {
'keywords': ['мусор', 'контейнер', 'баки', 'вывоз', 'раздельный', 'переработк'],
'emoji': '🗑️'
},
'дети': {
'keywords': ['ребенок', 'дети', 'детск', 'коляск', 'площадк', 'песочниц'],
'emoji': '👶'
},
'животные': {
'keywords': ['собак', 'кошк', 'животн', 'питомец', 'лают', 'мяукают'],
'emoji': '🐕'
},
'объявления': {
'keywords': ['объявлен', 'продам', 'куплю', 'отдам', 'ищу', 'предлагаю'],
'emoji': '📢'
},
'жалобы': {
'keywords': ['жалоб', 'жалуюсь', 'недовольн', 'возмущен', 'требую', 'безобраз'],
'emoji': '😠'
},
'благодарности': {
'keywords': ['спасиб', 'благодар', 'благодарю', 'молодец', 'хорошо', 'отличн'],
'emoji': '🙏'
},
'вопросы': {
'keywords': ['вопрос', 'подскажит', 'помогит', 'кто знает', 'как сделать'],
'emoji': ''
},
'предложения': {
'keywords': ['предлагаю', 'предложен', 'давайте', 'может быть', 'идея'],
'emoji': '💡'
},
'собрание': {
'keywords': ['собран', 'встреч', 'совещан', 'голосован', 'опрос', 'обсужден'],
'emoji': '📅'
},
'ук': {
'keywords': ['ук', 'управляющ', 'компан', 'жкх', 'паспортист', 'диспетчер'],
'emoji': '🏢'
},
}
async def analyze_message(self, text: str) -> Optional[str]:
"""
Классифицировать сообщение по теме
Returns: название темы или None
"""
if not text:
return None
text_lower = text.lower()
topic_scores = defaultdict(int)
for topic_name, topic_data in self.topics.items():
for keyword in topic_data['keywords']:
if keyword in text_lower:
topic_scores[topic_name] += 1
if not topic_scores:
return None
# Возвращаем тему с наибольшим score
best_topic = max(topic_scores, key=topic_scores.get)
return best_topic
async def get_topics_stats(self, days: int = 7) -> Dict:
"""
Получить статистику тем за период
Returns: dict с темами и количеством сообщений
"""
from database.models import Message
time_window = datetime.utcnow() - timedelta(days=days)
stmt = (
select(Message.text)
.where(Message.timestamp >= time_window)
.where(Message.text.isnot(None))
)
result = await self.session.execute(stmt)
messages = [row[0] for row in result.all()]
topic_counts = defaultdict(int)
for text in messages:
topic = await self.analyze_message(text)
if topic:
topic_counts[topic] += 1
# Считаем общее количество классифицированных сообщений
total = sum(topic_counts.values())
unclassified = len(messages) - total
# Формируем результат
result = {
'total_messages': len(messages),
'classified': total,
'unclassified': unclassified,
'topics': {}
}
for topic_name, count in sorted(topic_counts.items(), key=lambda x: x[1], reverse=True):
percent = (count / total * 100) if total > 0 else 0
emoji = self.topics[topic_name]['emoji']
result['topics'][topic_name] = {
'count': count,
'percent': round(percent, 1),
'emoji': emoji
}
return result
async def get_topics_cloud(self, days: int = 7) -> str:
"""
Получить облако тегов в текстовом виде
Returns: форматированный текст
"""
stats = await self.get_topics_stats(days)
if not stats['topics']:
return ' Нет данных за указанный период'
text = f'☁️ <b>Облако тем чата</b> (за {days} дн.)\n\n'
text += f'Всего сообщений: {stats["total_messages"]}\n'
text += f'Распознано: {stats["classified"]} ({stats["classified"]/stats["total_messages"]*100:.0f}%)\n\n'
# Формируем облако
sorted_topics = sorted(
stats['topics'].items(),
key=lambda x: x[1]['percent'],
reverse=True
)
for topic_name, data in sorted_topics:
bar_length = int(data['percent'] / 5) # 1 символ = 5%
bar = '' * bar_length + '' * (20 - bar_length)
text += f'{data["emoji"]} <b>{topic_name.capitalize()}</b>: {bar} {data["percent"]}% ({data["count"]})\n'
return text
async def get_top_topics(self, days: int = 7, limit: int = 5) -> List[Tuple[str, Dict]]:
"""
Получить топ тем за период
Returns: список (названиеемы, данные)
"""
stats = await self.get_topics_stats(days)
sorted_topics = sorted(
stats['topics'].items(),
key=lambda x: x[1]['percent'],
reverse=True
)
return sorted_topics[:limit]
async def get_topic_trend(self, topic: str, days: int = 14) -> Dict:
"""
Получить тренд темы по дням
Returns: dict с датами и количеством сообщений
"""
from database.models import Message
time_window = datetime.utcnow() - timedelta(days=days)
stmt = (
select(Message.text, func.date(Message.timestamp).label('msg_date'))
.where(Message.timestamp >= time_window)
.where(Message.text.isnot(None))
)
result = await self.session.execute(stmt)
messages = [(row[0], row[1]) for row in result.all()]
daily_counts = defaultdict(int)
for text, date in messages:
topic = await self.analyze_message(text)
if topic == topic:
daily_counts[date] += 1
return {
'topic': topic,
'days': days,
'daily_counts': dict(sorted(daily_counts.items())),
'total': sum(daily_counts.values())
}
async def get_weekly_report(self) -> str:
"""
Сформировать еженедельный отчёт по темам
Returns: форматированный текст отчёта
"""
stats = await self.get_topics_stats(7)
prev_stats = await self.get_topics_stats(14)
text = '📊 <b>Еженедельный отчёт по темам чата</b>\n\n'
text += f'Период: последние 7 дней\n\n'
# Топ тем
text += '🔥 <b>Топ тем недели:</b>\n\n'
top_topics = await self.get_top_topics(7, 5)
for i, (topic_name, data) in enumerate(top_topics, 1):
text += f'{i}. {data["emoji"]} {topic_name.capitalize()}: {data["percent"]}% ({data["count"]} сообщ.)\n'
# Сравнение с прошлой неделей
text += '\n📈 <b>Динамика:</b>\n\n'
if prev_stats['topics']:
for topic_name, data in stats['topics'].items():
prev_data = prev_stats['topics'].get(topic_name, {'count': 0})
diff = data['count'] - prev_data['count']
if diff > 0:
text += f'↗️ {topic_name.capitalize()}: +{diff} ({data["emoji"]})\n'
elif diff < 0:
text += f'↘️ {topic_name.capitalize()}: {diff} ({data["emoji"]})\n'
text += '\n💡 Используйте /topics для детальной статистики'
return text