Реализована система анализа комментариев
This commit is contained in:
parent
31f845f17b
commit
35800b825d
5
.idea/.gitignore
generated
vendored
Normal file
5
.idea/.gitignore
generated
vendored
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
# Default ignored files
|
||||||
|
/shelf/
|
||||||
|
/workspace.xml
|
||||||
|
# Editor-based HTTP Client requests
|
||||||
|
/httpRequests/
|
||||||
6
.idea/inspectionProfiles/profiles_settings.xml
generated
Normal file
6
.idea/inspectionProfiles/profiles_settings.xml
generated
Normal 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
7
.idea/misc.xml
generated
Normal 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
8
.idea/modules.xml
generated
Normal 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
8
.idea/praktika.iml
generated
Normal 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
6
.idea/vcs.xml
generated
Normal 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
10
data/comments.txt
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
alice|Это плохое кино, ужас просто! Актеры играют отвратительно
|
||||||
|
bob|Отлично! Фильм супер, мне очень понравилось. Напишите мне на test@example.com
|
||||||
|
charlie|Нормально, но могло быть и лучше. Спецэффекты слабоваты
|
||||||
|
alice|Фигня полная, bad фильм. Зря потратил время и деньги
|
||||||
|
bob|Хорошо, спасибо за рекомендацию. Буду ждать продолжение
|
||||||
|
david|Коротко и неинформативно
|
||||||
|
alice|Отстой! Ужасная игра актеров и сценарий ни о чем
|
||||||
|
bob|Прекрасный фильм, спасибо огромное! Обратная связь: feedback@site.ru
|
||||||
|
eve|Неплохо, но есть к чему стремиться. Сценарий слабоват
|
||||||
|
alice|Это плохое кино, ужас просто!
|
||||||
10
data/filtered_comments.txt
Normal file
10
data/filtered_comments.txt
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
это *** кино *** просто! актеры играют отвратительно
|
||||||
|
отлично! фильм супер мне очень понравилось. напишите мне на test@example.com
|
||||||
|
нормально но могло быть и лучше. спецэффекты слабоваты
|
||||||
|
*** полная *** фильм. зря потратил время и деньги
|
||||||
|
хорошо спасибо за рекомендацию. буду ждать продолжение
|
||||||
|
коротко и неинформативно
|
||||||
|
отстой! ***ная игра актеров и сценарий ни о чем
|
||||||
|
прекрасный фильм спасибо огромное! обратная связь feedback@site.ru
|
||||||
|
неплохо но есть к чему стремиться. сценарий слабоват
|
||||||
|
это *** кино *** просто!
|
||||||
0
src/__init__.py
Normal file
0
src/__init__.py
Normal file
169
src/comment_processor.py
Normal file
169
src/comment_processor.py
Normal 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
152
src/main.py
Normal 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()
|
||||||
Loading…
Reference in New Issue
Block a user