Реализована система анализа комментариев
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