Update app.py
Browse files
app.py
CHANGED
@@ -16,22 +16,6 @@ logger = logging.getLogger(__name__)
|
|
16 |
|
17 |
@dataclass
|
18 |
class Comment:
|
19 |
-
"""
|
20 |
-
Представляет комментарий Instagram со всеми метаданными и вложенной структурой.
|
21 |
-
Attributes:
|
22 |
-
id: Уникальный идентификатор комментария
|
23 |
-
username: Имя пользователя
|
24 |
-
time: Временная метка
|
25 |
-
content: Текст комментария
|
26 |
-
likes: Количество лайков
|
27 |
-
level: Уровень вложенности
|
28 |
-
parent_id: ID родительского комментария
|
29 |
-
replies: Список ответов
|
30 |
-
is_verified: Верифицированный аккаунт
|
31 |
-
mentions: Упоминания пользователей
|
32 |
-
hashtags: Хэштеги
|
33 |
-
is_deleted: Флаг удаленного комментария
|
34 |
-
"""
|
35 |
id: str = field(default_factory=lambda: str(uuid4()))
|
36 |
username: str = ""
|
37 |
time: str = ""
|
@@ -44,20 +28,14 @@ class Comment:
|
|
44 |
mentions: List[str] = field(default_factory=list)
|
45 |
hashtags: List[str] = field(default_factory=list)
|
46 |
is_deleted: bool = False
|
|
|
47 |
|
48 |
def __post_init__(self):
|
49 |
-
"""Валидация после инициализации"""
|
50 |
if len(self.content) > 2200:
|
51 |
logger.warning(f"Comment content exceeds 2200 characters for user {self.username}")
|
52 |
self.content = self.content[:2200] + "..."
|
53 |
|
54 |
class InstagramCommentAnalyzer:
|
55 |
-
"""
|
56 |
-
Основной класс для обработки и анализа комментариев Instagram.
|
57 |
-
Обрабатывает парсинг комментариев, вложенную структуру и особые случаи.
|
58 |
-
"""
|
59 |
-
|
60 |
-
# Регулярное выражение для извлечения комментариев
|
61 |
COMMENT_PATTERN = r'''
|
62 |
(?P<username>[\w.-]+)\s+
|
63 |
(?P<time>\d+\s+нед\.)
|
@@ -67,12 +45,6 @@ class InstagramCommentAnalyzer:
|
|
67 |
'''
|
68 |
|
69 |
def __init__(self, max_depth: int = 10, max_comment_length: int = 2200):
|
70 |
-
"""
|
71 |
-
Инициализация анализатора с настраиваемыми параметрами.
|
72 |
-
Args:
|
73 |
-
max_depth: Максимальная глубина вложенности комментариев
|
74 |
-
max_comment_length: Максимальная длина комментария
|
75 |
-
"""
|
76 |
self.max_depth = max_depth
|
77 |
self.max_comment_length = max_comment_length
|
78 |
self.pattern = re.compile(self.COMMENT_PATTERN, re.VERBOSE | re.DOTALL)
|
@@ -87,63 +59,30 @@ class InstagramCommentAnalyzer:
|
|
87 |
'processed_hashtags': 0
|
88 |
}
|
89 |
|
90 |
-
#
|
91 |
-
self.sentiment_analyzer = pipeline(
|
|
|
|
|
|
|
92 |
|
93 |
def analyze_sentiment(self, text: str) -> str:
|
94 |
-
"""
|
95 |
-
Анализ настроений в комментарии с использованием модели Hugging Face.
|
96 |
-
Args:
|
97 |
-
text: Текст комментария
|
98 |
-
Returns:
|
99 |
-
Строка с меткой настроения ('POSITIVE' или 'NEGATIVE')
|
100 |
-
"""
|
101 |
result = self.sentiment_analyzer(text)
|
102 |
return result[0]['label']
|
103 |
|
104 |
def normalize_text(self, text: str) -> str:
|
105 |
-
"""
|
106 |
-
Нормализация входного текста.
|
107 |
-
Args:
|
108 |
-
text: Исходный текст
|
109 |
-
Returns:
|
110 |
-
Нормализованный текст
|
111 |
-
"""
|
112 |
-
# Декодирование HTML-сущностей
|
113 |
text = html.unescape(text)
|
114 |
-
# Нормализация пробелов
|
115 |
text = ' '.join(text.split())
|
116 |
-
# Удаление невидимых символов
|
117 |
text = re.sub(r'[\u200b\ufeff\u200c]', '', text)
|
118 |
return text
|
119 |
|
120 |
def extract_metadata(self, comment: Comment) -> None:
|
121 |
-
"""
|
122 |
-
Извлечение метаданных из комментария.
|
123 |
-
Args:
|
124 |
-
comment: Объект комментария
|
125 |
-
"""
|
126 |
-
# Извлечение @упоминаний
|
127 |
comment.mentions = re.findall(r'@(\w+)', comment.content)
|
128 |
self.stats['processed_mentions'] += len(comment.mentions)
|
129 |
-
|
130 |
-
# Извлечение #хэштегов
|
131 |
comment.hashtags = re.findall(r'#(\w+)', comment.content)
|
132 |
self.stats['processed_hashtags'] += len(comment.hashtags)
|
133 |
-
|
134 |
-
# Проверка верификации
|
135 |
comment.is_verified = bool(re.search(r'✓|Подтвержденный', comment.username))
|
136 |
|
137 |
def process_comment(self, text: str, parent_id: Optional[str] = None, level: int = 0) -> Optional[Comment]:
|
138 |
-
"""
|
139 |
-
Обработка отдельного комментария.
|
140 |
-
Args:
|
141 |
-
text: Текст комментария
|
142 |
-
parent_id: ID родительского комментария
|
143 |
-
level: Уровень вложенности
|
144 |
-
Returns:
|
145 |
-
Обработанный объект Comment или None
|
146 |
-
"""
|
147 |
if level > self.max_depth:
|
148 |
logger.warning(f"Maximum depth {self.max_depth} exceeded")
|
149 |
self.stats['max_depth_reached'] += 1
|
@@ -172,7 +111,6 @@ class InstagramCommentAnalyzer:
|
|
172 |
self.stats['truncated_comments'] += 1
|
173 |
comment.content = comment.content[:self.max_comment_length] + "..."
|
174 |
|
175 |
-
# Добавление анализа настроений
|
176 |
comment.sentiment = self.analyze_sentiment(comment.content)
|
177 |
|
178 |
self.extract_metadata(comment)
|
@@ -191,14 +129,6 @@ class InstagramCommentAnalyzer:
|
|
191 |
return comment
|
192 |
|
193 |
def format_comment(self, comment: Comment, index: int) -> str:
|
194 |
-
"""
|
195 |
-
Форматирование комментария для вывода.
|
196 |
-
Args:
|
197 |
-
comment: Объект комментария
|
198 |
-
index: Номер комментария
|
199 |
-
Returns:
|
200 |
-
Отформатированная строка комментария
|
201 |
-
"""
|
202 |
if comment.is_deleted:
|
203 |
return f'{index}. "[УДАЛЕНО]" "" "" "Нравится 0"'
|
204 |
|
@@ -208,23 +138,9 @@ class InstagramCommentAnalyzer:
|
|
208 |
)
|
209 |
|
210 |
def process_comments(self, text: str) -> List[str]:
|
211 |
-
"""
|
212 |
-
Обработка всех комментариев в тексте.
|
213 |
-
Args:
|
214 |
-
text: Исходный текст с комментариями
|
215 |
-
Returns:
|
216 |
-
Список отформатированных комментариев
|
217 |
-
"""
|
218 |
-
# Сброс статистики
|
219 |
self.stats = {key: 0 for key in self.stats}
|
220 |
-
|
221 |
-
# Нормализация текста
|
222 |
text = self.normalize_text(text)
|
223 |
-
|
224 |
-
# Разделение на отдельные комментарии
|
225 |
raw_comments = text.split('ОтветитьНравится')
|
226 |
-
|
227 |
-
# Обработка комментариев
|
228 |
formatted_comments = []
|
229 |
for i, raw_comment in enumerate(raw_comments, 1):
|
230 |
if not raw_comment.strip():
|
@@ -237,10 +153,6 @@ class InstagramCommentAnalyzer:
|
|
237 |
return formatted_comments
|
238 |
|
239 |
def main():
|
240 |
-
"""
|
241 |
-
Пример использования анализатора.
|
242 |
-
"""
|
243 |
-
# Пример входного текста
|
244 |
example_text = """
|
245 |
user1 2 нед. This is a positive comment! Отметки "Нравится": 25
|
246 |
user2 3 нед. This is a negative comment! Отметки "Нравится": 5
|
|
|
16 |
|
17 |
@dataclass
|
18 |
class Comment:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
19 |
id: str = field(default_factory=lambda: str(uuid4()))
|
20 |
username: str = ""
|
21 |
time: str = ""
|
|
|
28 |
mentions: List[str] = field(default_factory=list)
|
29 |
hashtags: List[str] = field(default_factory=list)
|
30 |
is_deleted: bool = False
|
31 |
+
sentiment: Optional[str] = None
|
32 |
|
33 |
def __post_init__(self):
|
|
|
34 |
if len(self.content) > 2200:
|
35 |
logger.warning(f"Comment content exceeds 2200 characters for user {self.username}")
|
36 |
self.content = self.content[:2200] + "..."
|
37 |
|
38 |
class InstagramCommentAnalyzer:
|
|
|
|
|
|
|
|
|
|
|
|
|
39 |
COMMENT_PATTERN = r'''
|
40 |
(?P<username>[\w.-]+)\s+
|
41 |
(?P<time>\d+\s+нед\.)
|
|
|
45 |
'''
|
46 |
|
47 |
def __init__(self, max_depth: int = 10, max_comment_length: int = 2200):
|
|
|
|
|
|
|
|
|
|
|
|
|
48 |
self.max_depth = max_depth
|
49 |
self.max_comment_length = max_comment_length
|
50 |
self.pattern = re.compile(self.COMMENT_PATTERN, re.VERBOSE | re.DOTALL)
|
|
|
59 |
'processed_hashtags': 0
|
60 |
}
|
61 |
|
62 |
+
# Явное указание модели для анализа настроений
|
63 |
+
self.sentiment_analyzer = pipeline(
|
64 |
+
"sentiment-analysis",
|
65 |
+
model="distilbert-base-uncased-finetuned-sst-2-english" # Выбор модели
|
66 |
+
)
|
67 |
|
68 |
def analyze_sentiment(self, text: str) -> str:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
69 |
result = self.sentiment_analyzer(text)
|
70 |
return result[0]['label']
|
71 |
|
72 |
def normalize_text(self, text: str) -> str:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
73 |
text = html.unescape(text)
|
|
|
74 |
text = ' '.join(text.split())
|
|
|
75 |
text = re.sub(r'[\u200b\ufeff\u200c]', '', text)
|
76 |
return text
|
77 |
|
78 |
def extract_metadata(self, comment: Comment) -> None:
|
|
|
|
|
|
|
|
|
|
|
|
|
79 |
comment.mentions = re.findall(r'@(\w+)', comment.content)
|
80 |
self.stats['processed_mentions'] += len(comment.mentions)
|
|
|
|
|
81 |
comment.hashtags = re.findall(r'#(\w+)', comment.content)
|
82 |
self.stats['processed_hashtags'] += len(comment.hashtags)
|
|
|
|
|
83 |
comment.is_verified = bool(re.search(r'✓|Подтвержденный', comment.username))
|
84 |
|
85 |
def process_comment(self, text: str, parent_id: Optional[str] = None, level: int = 0) -> Optional[Comment]:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
86 |
if level > self.max_depth:
|
87 |
logger.warning(f"Maximum depth {self.max_depth} exceeded")
|
88 |
self.stats['max_depth_reached'] += 1
|
|
|
111 |
self.stats['truncated_comments'] += 1
|
112 |
comment.content = comment.content[:self.max_comment_length] + "..."
|
113 |
|
|
|
114 |
comment.sentiment = self.analyze_sentiment(comment.content)
|
115 |
|
116 |
self.extract_metadata(comment)
|
|
|
129 |
return comment
|
130 |
|
131 |
def format_comment(self, comment: Comment, index: int) -> str:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
132 |
if comment.is_deleted:
|
133 |
return f'{index}. "[УДАЛЕНО]" "" "" "Нравится 0"'
|
134 |
|
|
|
138 |
)
|
139 |
|
140 |
def process_comments(self, text: str) -> List[str]:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
141 |
self.stats = {key: 0 for key in self.stats}
|
|
|
|
|
142 |
text = self.normalize_text(text)
|
|
|
|
|
143 |
raw_comments = text.split('ОтветитьНравится')
|
|
|
|
|
144 |
formatted_comments = []
|
145 |
for i, raw_comment in enumerate(raw_comments, 1):
|
146 |
if not raw_comment.strip():
|
|
|
153 |
return formatted_comments
|
154 |
|
155 |
def main():
|
|
|
|
|
|
|
|
|
156 |
example_text = """
|
157 |
user1 2 нед. This is a positive comment! Отметки "Нравится": 25
|
158 |
user2 3 нед. This is a negative comment! Отметки "Нравится": 5
|