Update app.py
Browse files
app.py
CHANGED
@@ -1,7 +1,261 @@
|
|
1 |
-
import
|
|
|
|
|
|
|
|
|
|
|
|
|
2 |
|
3 |
-
|
4 |
-
|
|
|
|
|
|
|
|
|
5 |
|
6 |
-
|
7 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from dataclasses import dataclass, field
|
2 |
+
from typing import List, Optional, Dict, Any
|
3 |
+
import re
|
4 |
+
from datetime import datetime
|
5 |
+
import logging
|
6 |
+
import html
|
7 |
+
from uuid import uuid4
|
8 |
|
9 |
+
# Настройка логирования
|
10 |
+
logging.basicConfig(
|
11 |
+
level=logging.INFO,
|
12 |
+
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
|
13 |
+
)
|
14 |
+
logger = logging.getLogger(__name__)
|
15 |
|
16 |
+
@dataclass
|
17 |
+
class Comment:
|
18 |
+
"""
|
19 |
+
Представляет комментарий Instagram со всеми метаданными и вложенной структурой.
|
20 |
+
|
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 = ""
|
38 |
+
content: str = ""
|
39 |
+
likes: int = 0
|
40 |
+
level: int = 0
|
41 |
+
parent_id: Optional[str] = None
|
42 |
+
replies: List['Comment'] = field(default_factory=list)
|
43 |
+
is_verified: bool = False
|
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+нед\.)
|
64 |
+
(?P<content>.*?)
|
65 |
+
(?:Отметки\s*"Нравится":\s*(?P<likes>\d+))?
|
66 |
+
(?:Ответить)?(?:Показать\sперевод)?(?:Нравится)?
|
67 |
+
'''
|
68 |
+
|
69 |
+
def __init__(self, max_depth: int = 10, max_comment_length: int = 2200):
|
70 |
+
"""
|
71 |
+
Инициализация анализатора с настраиваемыми параметрами.
|
72 |
+
|
73 |
+
Args:
|
74 |
+
max_depth: Максимальная глубина вложенности комментариев
|
75 |
+
max_comment_length: Максимальная длина комментария
|
76 |
+
"""
|
77 |
+
self.max_depth = max_depth
|
78 |
+
self.max_comment_length = max_comment_length
|
79 |
+
self.pattern = re.compile(self.COMMENT_PATTERN, re.VERBOSE | re.DOTALL)
|
80 |
+
self.comments: List[Comment] = []
|
81 |
+
self.stats: Dict[str, int] = {
|
82 |
+
'total_comments': 0,
|
83 |
+
'deleted_comments': 0,
|
84 |
+
'empty_comments': 0,
|
85 |
+
'max_depth_reached': 0,
|
86 |
+
'truncated_comments': 0,
|
87 |
+
'processed_mentions': 0,
|
88 |
+
'processed_hashtags': 0
|
89 |
+
}
|
90 |
+
|
91 |
+
def normalize_text(self, text: str) -> str:
|
92 |
+
"""
|
93 |
+
Нормализация входного текста.
|
94 |
+
|
95 |
+
Args:
|
96 |
+
text: Исходный текст
|
97 |
+
|
98 |
+
Returns:
|
99 |
+
Нормализованный текст
|
100 |
+
"""
|
101 |
+
# Декодирование HTML-сущностей
|
102 |
+
text = html.unescape(text)
|
103 |
+
# Нормализация пробелов
|
104 |
+
text = ' '.join(text.split())
|
105 |
+
# Удаление невидимых символов
|
106 |
+
text = re.sub(r'[\u200b\ufeff\u200c]', '', text)
|
107 |
+
return text
|
108 |
+
|
109 |
+
def extract_metadata(self, comment: Comment) -> None:
|
110 |
+
"""
|
111 |
+
Извлечение метаданных из комментария.
|
112 |
+
|
113 |
+
Args:
|
114 |
+
comment: Объект комментария
|
115 |
+
"""
|
116 |
+
# Извлечение @упоминаний
|
117 |
+
comment.mentions = re.findall(r'@(\w+)', comment.content)
|
118 |
+
self.stats['processed_mentions'] += len(comment.mentions)
|
119 |
+
|
120 |
+
# Извлечение #хэштегов
|
121 |
+
comment.hashtags = re.findall(r'#(\w+)', comment.content)
|
122 |
+
self.stats['processed_hashtags'] += len(comment.hashtags)
|
123 |
+
|
124 |
+
# Проверка верификации
|
125 |
+
comment.is_verified = bool(re.search(r'✓|Подтвержденный', comment.username))
|
126 |
+
|
127 |
+
def process_comment(self, text: str, parent_id: Optional[str] = None, level: int = 0) -> Optional[Comment]:
|
128 |
+
"""
|
129 |
+
Обработка отдельного комментария.
|
130 |
+
|
131 |
+
Args:
|
132 |
+
text: Текст комментария
|
133 |
+
parent_id: ID родительского комментария
|
134 |
+
level: Уровень вложенности
|
135 |
+
|
136 |
+
Returns:
|
137 |
+
Обработанный объект Comment или None
|
138 |
+
"""
|
139 |
+
if level > self.max_depth:
|
140 |
+
logger.warning(f"Maximum depth {self.max_depth} exceeded")
|
141 |
+
self.stats['max_depth_reached'] += 1
|
142 |
+
return None
|
143 |
+
|
144 |
+
if not text.strip():
|
145 |
+
self.stats['empty_comments'] += 1
|
146 |
+
return None
|
147 |
+
|
148 |
+
try:
|
149 |
+
match = self.pattern.match(text)
|
150 |
+
if not match:
|
151 |
+
raise ValueError(f"Could not parse comment: {text[:100]}...")
|
152 |
+
|
153 |
+
data = match.groupdict()
|
154 |
+
comment = Comment(
|
155 |
+
username=data['username'],
|
156 |
+
time=data['time'],
|
157 |
+
content=data['content'].strip(),
|
158 |
+
likes=int(data['likes'] or 0),
|
159 |
+
level=level,
|
160 |
+
parent_id=parent_id
|
161 |
+
)
|
162 |
+
|
163 |
+
if len(comment.content) > self.max_comment_length:
|
164 |
+
self.stats['truncated_comments'] += 1
|
165 |
+
comment.content = comment.content[:self.max_comment_length] + "..."
|
166 |
+
|
167 |
+
self.extract_metadata(comment)
|
168 |
+
self.stats['total_comments'] += 1
|
169 |
+
return comment
|
170 |
+
|
171 |
+
except Exception as e:
|
172 |
+
logger.error(f"Error processing comment: {str(e)}")
|
173 |
+
comment = Comment(
|
174 |
+
username="[damaged]",
|
175 |
+
time="",
|
176 |
+
content="[Поврежденные данные]",
|
177 |
+
is_deleted=True
|
178 |
+
)
|
179 |
+
self.stats['deleted_comments'] += 1
|
180 |
+
return comment
|
181 |
+
|
182 |
+
def format_comment(self, comment: Comment, index: int) -> str:
|
183 |
+
"""
|
184 |
+
Форматирование комментария для вывода.
|
185 |
+
|
186 |
+
Args:
|
187 |
+
comment: Объект комментария
|
188 |
+
index: Номер комментария
|
189 |
+
|
190 |
+
Returns:
|
191 |
+
Отформатированная строка комментария
|
192 |
+
"""
|
193 |
+
if comment.is_deleted:
|
194 |
+
return f'{index}. "[УДАЛЕНО]" "" "" "Нравится 0"'
|
195 |
+
|
196 |
+
return (
|
197 |
+
f'{index}. "{comment.username}" "{comment.time}" '
|
198 |
+
f'"{comment.content}" "Нравится {comment.likes}"'
|
199 |
+
)
|
200 |
+
|
201 |
+
def process_comments(self, text: str) -> List[str]:
|
202 |
+
"""
|
203 |
+
Обработка всех комментариев в тексте.
|
204 |
+
|
205 |
+
Args:
|
206 |
+
text: Исходный текст с комментариями
|
207 |
+
|
208 |
+
Returns:
|
209 |
+
Список отформатированных комментариев
|
210 |
+
"""
|
211 |
+
# Сброс статистики
|
212 |
+
self.stats = {key: 0 for key in self.stats}
|
213 |
+
|
214 |
+
# Нормализация текста
|
215 |
+
text = self.normalize_text(text)
|
216 |
+
|
217 |
+
# Разделение на отдельные комментарии
|
218 |
+
raw_comments = text.split('ОтветитьНравится')
|
219 |
+
|
220 |
+
# Обработка комментариев
|
221 |
+
formatted_comments = []
|
222 |
+
for i, raw_comment in enumerate(raw_comments, 1):
|
223 |
+
if not raw_comment.strip():
|
224 |
+
continue
|
225 |
+
|
226 |
+
comment = self.process_comment(raw_comment)
|
227 |
+
if comment:
|
228 |
+
formatted_comments.append(self.format_comment(comment, i))
|
229 |
+
|
230 |
+
return formatted_comments
|
231 |
+
|
232 |
+
def main():
|
233 |
+
"""
|
234 |
+
Пример использования анализатора.
|
235 |
+
"""
|
236 |
+
# Пример входного текста
|
237 |
+
input_text = """
|
238 |
+
0001.minakov 53 нед.А что такое для,Вас Василина,волшебство?Отметки "Нравится": 3ОтветитьНравится
|
239 |
+
aliyllmn 54 нед.seni seviyorum:)Отметки "Нравится": 2ОтветитьПоказать переводНравится
|
240 |
+
"""
|
241 |
+
|
242 |
+
# Создание экземпляра анализатора
|
243 |
+
analyzer = InstagramCommentAnalyzer(max_depth=10)
|
244 |
+
|
245 |
+
# Обработка комментариев
|
246 |
+
results = analyzer.process_comments(input_text)
|
247 |
+
|
248 |
+
# Вывод результатов
|
249 |
+
print("\nОбработанные комментарии:")
|
250 |
+
print("-" * 50)
|
251 |
+
for result in results:
|
252 |
+
print(result)
|
253 |
+
|
254 |
+
# Вывод статистики
|
255 |
+
print("\nСтатистика обработки:")
|
256 |
+
print("-" * 50)
|
257 |
+
for key, value in analyzer.stats.items():
|
258 |
+
print(f"{key}: {value}")
|
259 |
+
|
260 |
+
if __name__ == "__main__":
|
261 |
+
main()
|