255 lines
10 KiB
Python
Executable File
255 lines
10 KiB
Python
Executable File
#!/usr/bin/env python3
|
||
"""
|
||
Утилита для автоматического извлечения потенциальных тем и ключевых слов
|
||
из собранных сырых данных Telegram
|
||
"""
|
||
|
||
import os
|
||
import sys
|
||
import json
|
||
import glob
|
||
import re
|
||
from collections import Counter
|
||
import nltk
|
||
from pathlib import Path
|
||
import logging
|
||
|
||
# Настройка логирования
|
||
logging.basicConfig(
|
||
level=logging.INFO,
|
||
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
|
||
handlers=[
|
||
logging.FileHandler("topic_extractor.log"),
|
||
logging.StreamHandler()
|
||
]
|
||
)
|
||
|
||
logger = logging.getLogger(__name__)
|
||
|
||
# Директории
|
||
INPUT_DIR = os.getenv('OUTPUT_DIR', 'data')
|
||
RAW_DIR = f"{INPUT_DIR}/raw"
|
||
STOPWORDS_FILE = "stopwords.txt"
|
||
|
||
def download_nltk_data():
|
||
"""Скачивает необходимые данные для NLTK"""
|
||
try:
|
||
nltk.data.find('tokenizers/punkt')
|
||
except LookupError:
|
||
nltk.download('punkt')
|
||
|
||
try:
|
||
nltk.data.find('corpora/stopwords')
|
||
except LookupError:
|
||
nltk.download('stopwords')
|
||
|
||
def load_stopwords():
|
||
"""Загружает стоп-слова из файла или использует встроенные"""
|
||
try:
|
||
if os.path.exists(STOPWORDS_FILE):
|
||
with open(STOPWORDS_FILE, 'r', encoding='utf-8') as f:
|
||
return set(line.strip().lower() for line in f if line.strip())
|
||
else:
|
||
# Если файл не существует, используем стандартные стоп-слова NLTK
|
||
from nltk.corpus import stopwords
|
||
return set(stopwords.words('russian') + stopwords.words('english'))
|
||
except Exception as e:
|
||
logger.error(f"Ошибка при загрузке стоп-слов: {e}")
|
||
return set()
|
||
|
||
def preprocess_text(text):
|
||
"""Предобработка текста: удаление спецсимволов, приведение к нижнему регистру"""
|
||
# Удаляем ссылки
|
||
text = re.sub(r'https?://\S+', '', text)
|
||
# Удаляем упоминания
|
||
text = re.sub(r'@\w+', '', text)
|
||
# Удаляем хештеги
|
||
text = re.sub(r'#\w+', '', text)
|
||
# Удаляем спецсимволы и цифры
|
||
text = re.sub(r'[^\w\s]', ' ', text)
|
||
text = re.sub(r'\d+', ' ', text)
|
||
# Приводим к нижнему регистру
|
||
text = text.lower()
|
||
# Удаляем лишние пробелы
|
||
text = re.sub(r'\s+', ' ', text).strip()
|
||
|
||
return text
|
||
|
||
def extract_keywords(texts, stopwords, min_length=4, max_length=20, top_n=30):
|
||
"""Извлекает ключевые слова из текстов"""
|
||
# Объединяем все тексты в один
|
||
all_text = ' '.join(texts)
|
||
|
||
# Токенизируем
|
||
tokens = nltk.word_tokenize(all_text)
|
||
|
||
# Фильтруем стоп-слова и короткие слова
|
||
filtered_tokens = [token for token in tokens
|
||
if token.lower() not in stopwords
|
||
and min_length <= len(token) <= max_length]
|
||
|
||
# Считаем частоту слов
|
||
word_counts = Counter(filtered_tokens)
|
||
|
||
# Возвращаем top_n самых частых слов
|
||
return [word for word, _ in word_counts.most_common(top_n)]
|
||
|
||
def extract_bigrams(texts, stopwords, min_length=4, top_n=20):
|
||
"""Извлекает биграммы из текстов"""
|
||
all_tokens = []
|
||
|
||
for text in texts:
|
||
tokens = nltk.word_tokenize(text)
|
||
# Фильтруем стоп-слова и короткие слова
|
||
filtered_tokens = [token.lower() for token in tokens
|
||
if token.lower() not in stopwords
|
||
and len(token) >= min_length]
|
||
all_tokens.extend(filtered_tokens)
|
||
|
||
# Создаем биграммы
|
||
bigrams = list(nltk.bigrams(all_tokens))
|
||
|
||
# Считаем частоту биграмм
|
||
bigram_counts = Counter(bigrams)
|
||
|
||
# Возвращаем top_n самых частых биграмм в виде строк
|
||
return [f"{w1} {w2}" for (w1, w2), _ in bigram_counts.most_common(top_n)]
|
||
|
||
def extract_topics_from_raw_data(min_messages=10):
|
||
"""Извлекает потенциальные темы из собранных сырых данных"""
|
||
# Ищем все .json файлы в директории с сырыми данными
|
||
raw_files = glob.glob(f"{RAW_DIR}/*.json")
|
||
|
||
if not raw_files:
|
||
logger.warning("Нет файлов для анализа")
|
||
return None
|
||
|
||
# Загружаем стоп-слова
|
||
stopwords = load_stopwords()
|
||
|
||
# Собираем все тексты
|
||
all_texts = []
|
||
|
||
for file in raw_files:
|
||
try:
|
||
with open(file, 'r', encoding='utf-8') as f:
|
||
data = json.load(f)
|
||
|
||
for msg in data["messages"]:
|
||
if msg["text"]:
|
||
preprocessed = preprocess_text(msg["text"])
|
||
if preprocessed: # Проверяем, что текст не пустой после предобработки
|
||
all_texts.append(preprocessed)
|
||
except Exception as e:
|
||
logger.error(f"Ошибка при чтении файла {file}: {e}")
|
||
|
||
if len(all_texts) < min_messages:
|
||
logger.warning(f"Недостаточно сообщений для анализа: {len(all_texts)} < {min_messages}")
|
||
return None
|
||
|
||
# Извлекаем ключевые слова
|
||
keywords = extract_keywords(all_texts, stopwords)
|
||
|
||
# Извлекаем биграммы
|
||
bigrams = extract_bigrams(all_texts, stopwords)
|
||
|
||
# Формируем предложения тем на основе биграмм и популярных слов
|
||
suggested_topics = []
|
||
|
||
for bigram in bigrams[:10]: # Берем 10 лучших биграмм
|
||
if bigram not in suggested_topics:
|
||
suggested_topics.append(bigram)
|
||
|
||
# Добавляем популярные ключевые слова, которые еще не в списке
|
||
for keyword in keywords[:15]: # Берем 15 лучших ключевых слов
|
||
if keyword not in suggested_topics:
|
||
suggested_topics.append(keyword)
|
||
|
||
# Группируем слова по общей основе (очень упрощенный подход)
|
||
grouped_topics = {}
|
||
|
||
for topic in suggested_topics:
|
||
# Используем первые 5 символов как приблизительную основу слова
|
||
if len(topic) >= 5:
|
||
root = topic[:5].lower()
|
||
if root not in grouped_topics:
|
||
grouped_topics[root] = []
|
||
grouped_topics[root].append(topic)
|
||
|
||
# Формируем финальный список тем и ключевых слов
|
||
final_topics = {}
|
||
|
||
for root, words in grouped_topics.items():
|
||
# Выбираем самое длинное слово как название темы
|
||
topic_name = max(words, key=len)
|
||
|
||
# Остальные слова используем как ключевые слова
|
||
topic_keywords = [word for word in words if word != topic_name]
|
||
|
||
# Добавляем дополнительные ключевые слова из общего списка
|
||
for keyword in keywords:
|
||
if keyword.startswith(root[:3]) and keyword not in topic_keywords and keyword != topic_name:
|
||
topic_keywords.append(keyword)
|
||
|
||
final_topics[topic_name] = topic_keywords[:10] # Ограничиваем 10 ключевыми словами
|
||
|
||
return final_topics
|
||
|
||
def save_suggested_topics(topics):
|
||
"""Сохраняет предложенные темы в файл"""
|
||
if not topics:
|
||
logger.warning("Нет тем для сохранения")
|
||
return False
|
||
|
||
try:
|
||
suggested_file = "suggested_topics.json"
|
||
|
||
with open(suggested_file, 'w', encoding='utf-8') as f:
|
||
json.dump(topics, f, indent=2, ensure_ascii=False)
|
||
|
||
logger.info(f"Предложенные темы сохранены в {suggested_file}")
|
||
|
||
# Также создаем более читаемую версию в Markdown
|
||
md_file = "suggested_topics.md"
|
||
|
||
with open(md_file, 'w', encoding='utf-8') as f:
|
||
f.write("# Предложенные темы и ключевые слова\n\n")
|
||
|
||
for topic, keywords in topics.items():
|
||
f.write(f"## {topic.capitalize()}\n\n")
|
||
f.write("Ключевые слова:\n")
|
||
for keyword in keywords:
|
||
f.write(f"- {keyword}\n")
|
||
f.write("\n")
|
||
|
||
return True
|
||
except Exception as e:
|
||
logger.error(f"Ошибка при сохранении предложенных тем: {e}")
|
||
return False
|
||
|
||
def main():
|
||
"""Основная функция для запуска экстрактора тем"""
|
||
try:
|
||
download_nltk_data()
|
||
|
||
logger.info("Извлечение тем из собранных данных...")
|
||
topics = extract_topics_from_raw_data()
|
||
|
||
if topics:
|
||
save_suggested_topics(topics)
|
||
|
||
# Выводим результаты в консоль
|
||
print("\nПредложенные темы и ключевые слова:")
|
||
for topic, keywords in topics.items():
|
||
print(f"\n{topic.capitalize()}:")
|
||
print("Ключевые слова:")
|
||
for keyword in keywords:
|
||
print(f"- {keyword}")
|
||
else:
|
||
print("Не удалось извлечь темы из данных. Возможно, недостаточно сообщений.")
|
||
except Exception as e:
|
||
logger.error(f"Ошибка: {e}")
|
||
sys.exit(1)
|
||
|
||
if __name__ == "__main__":
|
||
main() |