Реализована система анализа комментариев

This commit is contained in:
Starodumov Danil 2026-04-04 10:11:39 +03:00
parent 31f845f17b
commit 35800b825d
11 changed files with 381 additions and 0 deletions

5
.idea/.gitignore generated vendored Normal file
View File

@ -0,0 +1,5 @@
# Default ignored files
/shelf/
/workspace.xml
# Editor-based HTTP Client requests
/httpRequests/

View File

@ -0,0 +1,6 @@
<component name="InspectionProjectProfileManager">
<settings>
<option name="USE_PROJECT_PROFILE" value="false" />
<version value="1.0" />
</settings>
</component>

7
.idea/misc.xml generated Normal file
View File

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="Black">
<option name="sdkName" value="Python 3.14" />
</component>
<component name="ProjectRootManager" version="2" project-jdk-name="Python 3.14" project-jdk-type="Python SDK" />
</project>

8
.idea/modules.xml generated Normal file
View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/praktika.iml" filepath="$PROJECT_DIR$/.idea/praktika.iml" />
</modules>
</component>
</project>

8
.idea/praktika.iml generated Normal file
View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="PYTHON_MODULE" version="4">
<component name="NewModuleRootManager">
<content url="file://$MODULE_DIR$" />
<orderEntry type="jdk" jdkName="Python 3.14" jdkType="Python SDK" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
</module>

6
.idea/vcs.xml generated Normal file
View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="" vcs="Git" />
</component>
</project>

10
data/comments.txt Normal file
View File

@ -0,0 +1,10 @@
alice|Это плохое кино, ужас просто! Актеры играют отвратительно
bob|Отлично! Фильм супер, мне очень понравилось. Напишите мне на test@example.com
charlie|Нормально, но могло быть и лучше. Спецэффекты слабоваты
alice|Фигня полная, bad фильм. Зря потратил время и деньги
bob|Хорошо, спасибо за рекомендацию. Буду ждать продолжение
david|Коротко и неинформативно
alice|Отстой! Ужасная игра актеров и сценарий ни о чем
bob|Прекрасный фильм, спасибо огромное! Обратная связь: feedback@site.ru
eve|Неплохо, но есть к чему стремиться. Сценарий слабоват
alice|Это плохое кино, ужас просто!

View File

@ -0,0 +1,10 @@
это *** кино *** просто! актеры играют отвратительно
отлично! фильм супер мне очень понравилось. напишите мне на test@example.com
нормально но могло быть и лучше. спецэффекты слабоваты
*** полная *** фильм. зря потратил время и деньги
хорошо спасибо за рекомендацию. буду ждать продолжение
коротко и неинформативно
отстой! ***ная игра актеров и сценарий ни о чем
прекрасный фильм спасибо огромное! обратная связь feedback@site.ru
неплохо но есть к чему стремиться. сценарий слабоват
это *** кино *** просто!

0
src/__init__.py Normal file
View File

169
src/comment_processor.py Normal file
View File

@ -0,0 +1,169 @@
import re
from collections import defaultdict
def clean_text(raw: str) -> str:
"""
Удаляет лишние пробелы, приводит к нижнему регистру,
удаляет знаки пунктуации (кроме ., !), сохраняет email
"""
if not isinstance(raw, str):
raw = str(raw)
text = raw.lower()
# Временно заменяем email-адреса
email_pattern = r'[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}'
emails = re.findall(email_pattern, text)
for i, email in enumerate(emails):
text = text.replace(email, f'__EMAIL_{i}__')
# Удаляем пунктуацию, но сохраняем . и !
text = re.sub(r'[^\w\s.!]', '', text)
# Восстанавливаем email-адреса
for i, email in enumerate(emails):
text = text.replace(f'__EMAIL_{i}__', email)
# Нормализуем пробелы (один пробел между словами)
text = ' '.join(text.split())
return text
def extract_emails(text: str) -> list:
"""
Возвращает список всех email-адресов в тексте.
"""
if not isinstance(text, str):
return []
pattern = r'[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}'
return re.findall(pattern, text)
# ----- 3. Маскировка нецензурной лексики -----
def mask_profanity(text: str, bad_words: list) -> str:
"""
Заменяет все вхождения слов из bad_words на ***
"""
if not isinstance(text, str):
text = str(text)
result = text
for word in bad_words:
# Регистронезависимая замена
pattern = re.compile(re.escape(word), re.IGNORECASE)
result = pattern.sub('***', result)
return result
# ----- 4. Вычисление тональности -----
def calculate_sentiment_score(text: str, positive_words: set, negative_words: set) -> int:
"""
Возвращает 1, если больше позитивных слов,
-1 если больше негативных, 0 если поровну или слов нет
"""
if not isinstance(text, str):
text = str(text)
words = text.lower().split()
pos_count = sum(1 for w in words if w in positive_words)
neg_count = sum(1 for w in words if w in negative_words)
if pos_count > neg_count:
return 1
elif neg_count > pos_count:
return -1
else:
return 0
# ----- 5. Фильтрация по длине -----
def filter_by_length(comments: list, min_len: int, max_len: int) -> list:
"""
Возвращает список комментариев, длина которых входит в диапазон [min_len, max_len]
"""
if not isinstance(comments, list):
return []
return [c for c in comments if min_len <= len(str(c)) <= max_len]
# ----- 6. Тегирование пользователя по активности -----
def tag_user_by_activity(comments: list, user_name: str) -> str:
"""
Принимает список комментариев-словарей и имя пользователя.
Возвращает 'high' (>5), 'medium' (2-5), 'low' (0-1)
"""
if not isinstance(comments, list):
return 'low'
count = sum(1 for c in comments if isinstance(c, dict) and c.get('user') == user_name)
if count > 5:
return 'high'
elif count >= 2:
return 'medium'
else:
return 'low'
# ----- 7. Агрегация по пользователям -----
def aggregate_by_user(comments: list) -> dict:
"""
На входе список словарей {user: str, text: str}
Возвращает {user: [list_of_comments]}
"""
if not isinstance(comments, list):
return {}
result = defaultdict(list)
for c in comments:
if isinstance(c, dict) and 'user' in c and 'text' in c:
result[c['user']].append(c['text'])
return dict(result)
# ----- 8. Поиск дубликатов -----
def find_duplicates(comments: list) -> list:
"""
Возвращает список индексов элементов, встречающихся более одного раза
(первое вхождение не считается дубликатом)
"""
if not isinstance(comments, list):
return []
seen = {}
duplicate_indices = []
for idx, comment in enumerate(comments):
if comment in seen:
duplicate_indices.append(idx)
else:
seen[comment] = idx
return duplicate_indices
# ----- 9. Генерация отчёта по комментарию -----
def generate_comment_report(cleaned_text: str, sentiment: int, has_email: bool) -> dict:
"""
Возвращает словарь с метаданными комментария
"""
return {
"text": cleaned_text,
"sentiment": sentiment,
"contains_email": has_email,
"length": len(cleaned_text)
}
# ----- 10. Сохранение отфильтрованных комментариев -----
def save_filtered_comments(comments: list, file_path: str) -> None:
"""
Сохраняет список очищенных комментариев в файл (каждый с новой строки)
"""
try:
with open(file_path, 'w', encoding='utf-8') as f:
for comment in comments:
f.write(str(comment) + '\n')
except Exception as e:
print(f"Ошибка при сохранении файла {file_path}: {e}")

152
src/main.py Normal file
View File

@ -0,0 +1,152 @@
import os
import sys
# Добавляем путь к src для импорта модуля
sys.path.append(os.path.dirname(os.path.abspath(__file__)))
from comment_processor import (
clean_text,
extract_emails,
mask_profanity,
calculate_sentiment_score,
filter_by_length,
tag_user_by_activity,
aggregate_by_user,
find_duplicates,
generate_comment_report,
save_filtered_comments
)
# ----- Захардкоженные данные для модерации -----
BAD_WORDS = ["плохое", "ругательство", "bad", "ужас", "фигня"]
POSITIVE_WORDS = {"хорошо", "отлично", "супер", "класс", "прекрасно", "нравится", "спасибо"}
NEGATIVE_WORDS = {"плохо", "ужасно", "отстой", "не нравится", "кошмар", "ужас"}
def load_raw_comments(file_path: str) -> list:
"""
Загружает сырые данные из файла формата: user|текст комментария
Возвращает список словарей [{"user": ..., "text": ...}]
"""
comments = []
try:
with open(file_path, 'r', encoding='utf-8') as f:
for line_num, line in enumerate(f, 1):
line = line.strip()
if not line:
continue
# Разделяем по первому символу '|'
if '|' not in line:
print(f"Предупреждение: строка {line_num} не содержит '|', пропускаем: {line}")
continue
user, text = line.split('|', 1)
comments.append({"user": user.strip(), "text": text.strip()})
except FileNotFoundError:
print(f"Файл {file_path} не найден. Работаем с пустым списком комментариев.")
except Exception as e:
print(f"Ошибка при чтении файла {file_path}: {e}")
return comments
def main():
print("=== Система анализа и очистки пользовательских комментариев ===\n")
# ----- Шаг 1: Загрузка данных -----
import os
input_file = os.path.join(os.path.dirname(__file__), "..", "data", "comments.txt")
raw_comments = load_raw_comments(input_file)
print(f"Загружено комментариев: {len(raw_comments)}")
if not raw_comments:
print("Нет данных для обработки. Завершение работы.")
return
# ----- Шаг 2-4: Обработка каждого комментария -----
processed_comments = [] # Для хранения очищенных текстов
comment_reports = [] # Для итоговых отчётов
emails_found_count = 0
sentiment_stats = {1: 0, -1: 0, 0: 0} # positive, negative, neutral
for item in raw_comments:
original_text = item['text']
user = item['user']
# Шаг 3: Очистка текста
cleaned = clean_text(original_text)
# Шаг 3 (продолжение): Маскировка плохих слов
masked = mask_profanity(cleaned, BAD_WORDS)
# Шаг 4: Вычисление тональности
sentiment = calculate_sentiment_score(masked, POSITIVE_WORDS, NEGATIVE_WORDS)
sentiment_stats[sentiment] += 1
# Дополнительно: проверка наличия email
emails = extract_emails(masked)
has_email = len(emails) > 0
if has_email:
emails_found_count += 1
# Шаг 8: Формирование отчёта
report = generate_comment_report(masked, sentiment, has_email)
comment_reports.append(report)
# Сохраняем для дальнейших шагов (сохраняем связь с пользователем)
processed_comments.append({"user": user, "text": masked})
# ----- Шаг 5: Фильтрация по длине -----
all_texts = [item['text'] for item in processed_comments]
filtered_texts = filter_by_length(all_texts, 10, 200)
print(f"\nПосле фильтрации по длине (10-200 символов) осталось: {len(filtered_texts)} комментариев")
# ----- Шаг 6: Поиск дубликатов среди очищенных комментариев -----
duplicate_indices = find_duplicates(all_texts)
if duplicate_indices:
print(f"Найдены дубликаты на индексах: {duplicate_indices}")
else:
print("Дубликатов не найдено")
# ----- Шаг 7: Агрегация по пользователям и тегирование -----
user_aggregated = aggregate_by_user(processed_comments)
print(f"\nУникальных пользователей: {len(user_aggregated)}")
user_activity = {}
for user, comments_list in user_aggregated.items():
tag = tag_user_by_activity(processed_comments, user)
user_activity[user] = tag
print(f" {user}: {tag} активность ({len(comments_list)} комментариев)")
# ----- Шаг 9: Сохранение отфильтрованных комментариев -----
import os
output_file = os.path.join(os.path.dirname(os.path.dirname(__file__)), "data", "filtered_comments.txt")
save_filtered_comments(filtered_texts, output_file)
print(f"\nОтфильтрованные комментарии сохранены в {output_file}")
# ----- Шаг 10: Вывод итоговой статистики -----
print("\n" + "=" * 50)
print("ИТОГОВАЯ СТАТИСТИКА:")
print("=" * 50)
print(f"1. Общее количество уникальных пользователей: {len(user_aggregated)}")
print(f"2. Количество комментариев, содержащих email: {emails_found_count}")
print(f"3. Распределение тональности:")
print(f" - Положительные (sentiment = 1): {sentiment_stats[1]}")
print(f" - Отрицательные (sentiment = -1): {sentiment_stats[-1]}")
print(f" - Нейтральные (sentiment = 0): {sentiment_stats[0]}")
print("=" * 50)
# Дополнительно: покажем пример первых 3 отчётов
print("\nПример первых 3 отчётов по комментариям:")
for i, report in enumerate(comment_reports[:3]):
print(f"\n Отчёт {i + 1}:")
print(f" Текст: {report['text'][:50]}...")
print(f" Тональность: {report['sentiment']}")
print(f" Содержит email: {report['contains_email']}")
print(f" Длина: {report['length']}")
if __name__ == "__main__":
main()