From 35800b825d34edae3f7727b9cb1d749bc245a38a Mon Sep 17 00:00:00 2001 From: Starodumov Danil Date: Sat, 4 Apr 2026 10:11:39 +0300 Subject: [PATCH] =?UTF-8?q?=D0=A0=D0=B5=D0=B0=D0=BB=D0=B8=D0=B7=D0=BE?= =?UTF-8?q?=D0=B2=D0=B0=D0=BD=D0=B0=20=D1=81=D0=B8=D1=81=D1=82=D0=B5=D0=BC?= =?UTF-8?q?=D0=B0=20=D0=B0=D0=BD=D0=B0=D0=BB=D0=B8=D0=B7=D0=B0=20=D0=BA?= =?UTF-8?q?=D0=BE=D0=BC=D0=BC=D0=B5=D0=BD=D1=82=D0=B0=D1=80=D0=B8=D0=B5?= =?UTF-8?q?=D0=B2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .idea/.gitignore | 5 + .../inspectionProfiles/profiles_settings.xml | 6 + .idea/misc.xml | 7 + .idea/modules.xml | 8 + .idea/praktika.iml | 8 + .idea/vcs.xml | 6 + data/comments.txt | 10 ++ data/filtered_comments.txt | 10 ++ src/__init__.py | 0 src/comment_processor.py | 169 ++++++++++++++++++ src/main.py | 152 ++++++++++++++++ 11 files changed, 381 insertions(+) create mode 100644 .idea/.gitignore create mode 100644 .idea/inspectionProfiles/profiles_settings.xml create mode 100644 .idea/misc.xml create mode 100644 .idea/modules.xml create mode 100644 .idea/praktika.iml create mode 100644 .idea/vcs.xml create mode 100644 data/comments.txt create mode 100644 data/filtered_comments.txt create mode 100644 src/__init__.py create mode 100644 src/comment_processor.py create mode 100644 src/main.py diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..b58b603 --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,5 @@ +# Default ignored files +/shelf/ +/workspace.xml +# Editor-based HTTP Client requests +/httpRequests/ diff --git a/.idea/inspectionProfiles/profiles_settings.xml b/.idea/inspectionProfiles/profiles_settings.xml new file mode 100644 index 0000000..105ce2d --- /dev/null +++ b/.idea/inspectionProfiles/profiles_settings.xml @@ -0,0 +1,6 @@ + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 0000000..590a59e --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,7 @@ + + + + + + \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml new file mode 100644 index 0000000..4e61346 --- /dev/null +++ b/.idea/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/praktika.iml b/.idea/praktika.iml new file mode 100644 index 0000000..c03f621 --- /dev/null +++ b/.idea/praktika.iml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..35eb1dd --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/data/comments.txt b/data/comments.txt new file mode 100644 index 0000000..82cd6f5 --- /dev/null +++ b/data/comments.txt @@ -0,0 +1,10 @@ +alice|Это плохое кино, ужас просто! Актеры играют отвратительно +bob|Отлично! Фильм супер, мне очень понравилось. Напишите мне на test@example.com +charlie|Нормально, но могло быть и лучше. Спецэффекты слабоваты +alice|Фигня полная, bad фильм. Зря потратил время и деньги +bob|Хорошо, спасибо за рекомендацию. Буду ждать продолжение +david|Коротко и неинформативно +alice|Отстой! Ужасная игра актеров и сценарий ни о чем +bob|Прекрасный фильм, спасибо огромное! Обратная связь: feedback@site.ru +eve|Неплохо, но есть к чему стремиться. Сценарий слабоват +alice|Это плохое кино, ужас просто! \ No newline at end of file diff --git a/data/filtered_comments.txt b/data/filtered_comments.txt new file mode 100644 index 0000000..278c15a --- /dev/null +++ b/data/filtered_comments.txt @@ -0,0 +1,10 @@ +это *** кино *** просто! актеры играют отвратительно +отлично! фильм супер мне очень понравилось. напишите мне на test@example.com +нормально но могло быть и лучше. спецэффекты слабоваты +*** полная *** фильм. зря потратил время и деньги +хорошо спасибо за рекомендацию. буду ждать продолжение +коротко и неинформативно +отстой! ***ная игра актеров и сценарий ни о чем +прекрасный фильм спасибо огромное! обратная связь feedback@site.ru +неплохо но есть к чему стремиться. сценарий слабоват +это *** кино *** просто! diff --git a/src/__init__.py b/src/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/comment_processor.py b/src/comment_processor.py new file mode 100644 index 0000000..ad13d7c --- /dev/null +++ b/src/comment_processor.py @@ -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}") \ No newline at end of file diff --git a/src/main.py b/src/main.py new file mode 100644 index 0000000..7763e74 --- /dev/null +++ b/src/main.py @@ -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() \ No newline at end of file