File size: 13,970 Bytes
68545bc
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
"""
Модуль для предобработки текстовых данных для задач классификации.
Включает очистку, токенизацию, лемматизацию, векторизацию и извлечение мета-признаков.
"""

from __future__ import annotations

import re
from typing import List, Dict, Any, Optional, Tuple
from dataclasses import dataclass

import numpy as np
from bs4 import BeautifulSoup
import spacy
from sklearn.feature_extraction.text import TfidfVectorizer, CountVectorizer
from gensim.models import Word2Vec, FastText, Doc2Vec
from gensim.utils import simple_preprocess

from src.text_cleaner import clean_text, remove_html, normalize_whitespace
from src.classical_vectorizers import ClassicalVectorizers, VectorizationConfig


@dataclass
class PreprocessingConfig:
    """Конфигурация предобработки текста."""
    lowercase: bool = True
    remove_html: bool = True
    remove_urls: bool = True
    remove_emails: bool = True
    remove_numbers: bool = False
    lemmatize: bool = True
    remove_stopwords: bool = False
    min_token_length: int = 2
    emoji_to_text: bool = True


class TextPreprocessor:
    """Класс для предобработки текстов для классификации."""
    
    def __init__(self, config: Optional[PreprocessingConfig] = None):
        self.config = config or PreprocessingConfig()
        self.nlp = None
        if self.config.lemmatize:
            try:
                self.nlp = spacy.load("ru_core_news_sm")
            except OSError:
                try:
                    self.nlp = spacy.load("ru_core_news_md")
                except OSError:
                    print("⚠️ spaCy русская модель не найдена. Лемматизация отключена.")
                    self.config.lemmatize = False
    
    def _remove_urls(self, text: str) -> str:
        """Удаляет URL из текста."""
        url_pattern = r'http[s]?://(?:[a-zA-Z]|[0-9]|[$-_@.&+]|[!*\\(\\),]|(?:%[0-9a-fA-F][0-9a-fA-F]))+'
        return re.sub(url_pattern, '', text)
    
    def _remove_emails(self, text: str) -> str:
        """Удаляет email адреса из текста."""
        email_pattern = r'\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b'
        return re.sub(email_pattern, '', text)
    
    def _emoji_to_text(self, text: str) -> str:
        """Заменяет эмодзи на текстовое описание (упрощенная версия)."""
        # Базовые замены для русскоязычного контекста
        emoji_map = {
            '😀': ' улыбка ',
            '😃': ' радость ',
            '😄': ' смех ',
            '😁': ' веселье ',
            '😆': ' хохот ',
            '😅': ' пот ',
            '😂': ' слезы радости ',
            '🤣': ' хохот ',
            '😊': ' улыбка ',
            '😇': ' ангел ',
            '🙂': ' улыбка ',
            '🙃': ' перевернутое лицо ',
            '😉': ' подмигивание ',
            '😌': ' облегчение ',
            '😍': ' любовь ',
            '🥰': ' любовь ',
            '😘': ' поцелуй ',
            '😗': ' поцелуй ',
            '😙': ' поцелуй ',
            '😚': ' поцелуй ',
            '😋': ' вкусно ',
            '😛': ' язык ',
            '😜': ' подмигивание ',
            '😝': ' язык ',
            '😞': ' грусть ',
            '😟': ' беспокойство ',
            '😠': ' злость ',
            '😡': ' ярость ',
            '😢': ' плач ',
            '😣': ' страдание ',
            '😤': ' упрямство ',
            '😥': ' разочарование ',
            '😦': ' удивление ',
            '😧': ' шок ',
            '😨': ' страх ',
            '😩': ' усталость ',
            '😪': ' сонливость ',
            '😫': ' усталость ',
            '😬': ' напряжение ',
            '😭': ' плач ',
            '😮': ' удивление ',
            '😯': ' удивление ',
            '😰': ' тревога ',
            '😱': ' ужас ',
            '😲': ' шок ',
            '😳': ' смущение ',
            '😴': ' сон ',
            '😵': ' головокружение ',
            '😶': ' без слов ',
            '😷': ' маска ',
            '🤐': ' молчание ',
            '🤒': ' болезнь ',
            '🤕': ' травма ',
            '🤢': ' тошнота ',
            '🤣': ' хохот ',
            '🤤': ' слюни ',
            '🤥': ' ложь ',
            '🤧': ' чихание ',
            '🤨': ' подозрение ',
            '🤩': ' звезды ',
            '🤪': ' безумие ',
            '🤫': ' тишина ',
            '🤬': ' ругательство ',
            '🤭': ' секрет ',
            '🤮': ' рвота ',
            '🤯': ' взрыв мозга ',
        }
        for emoji, replacement in emoji_map.items():
            text = text.replace(emoji, replacement)
        return text
    
    def preprocess(self, text: str) -> str:
        """Основная функция предобработки текста."""
        if not text:
            return ""
        
        # Удаление HTML
        if self.config.remove_html:
            text = remove_html(text)
        
        # Удаление URL
        if self.config.remove_urls:
            text = self._remove_urls(text)
        
        # Удаление email
        if self.config.remove_emails:
            text = self._remove_emails(text)
        
        # Замена эмодзи
        if self.config.emoji_to_text:
            text = self._emoji_to_text(text)
        
        # Нормализация пробелов
        text = normalize_whitespace(text)
        
        # Приведение к нижнему регистру
        if self.config.lowercase:
            text = text.lower()
        
        # Удаление чисел (опционально)
        if self.config.remove_numbers:
            text = re.sub(r'\d+', '', text)
        
        # Лемматизация
        if self.config.lemmatize and self.nlp:
            doc = self.nlp(text)
            tokens = [token.lemma_ for token in doc if not token.is_punct and not token.is_space]
            text = ' '.join(tokens)
        else:
            # Простая токенизация
            tokens = simple_preprocess(text, deacc=False, min_len=self.config.min_token_length)
            text = ' '.join(tokens)
        
        # Удаление стоп-слов (если не использовалась лемматизация со spaCy)
        if self.config.remove_stopwords and not (self.config.lemmatize and self.nlp):
            from src.text_cleaner import remove_stopwords_tokens
            tokens = text.split()
            tokens = remove_stopwords_tokens(tokens)
            text = ' '.join(tokens)
        
        # Финальная нормализация
        text = normalize_whitespace(text)
        
        return text
    
    def preprocess_batch(self, texts: List[str]) -> List[str]:
        """Предобработка списка текстов."""
        return [self.preprocess(text) for text in texts]


def extract_meta_features(texts: List[str]) -> np.ndarray:
    """
    Извлекает мета-признаки из текстов.
    
    Возвращает:
        Массив формы (n_texts, n_features) с признаками:
        - длина текста (символы)
        - средняя длина слова
        - количество уникальных слов
        - доля знаков препинания
        - доля заглавных букв
        - доля цифр
    """
    features = []
    
    for text in texts:
        if not text:
            features.append([0, 0, 0, 0, 0, 0])
            continue
        
        # Длина текста
        text_length = len(text)
        
        # Токены
        tokens = text.split()
        if not tokens:
            features.append([text_length, 0, 0, 0, 0, 0])
            continue
        
        # Средняя длина слова
        avg_word_length = np.mean([len(token) for token in tokens])
        
        # Количество уникальных слов
        unique_words = len(set(tokens))
        
        # Доля знаков препинания
        punct_count = sum(1 for c in text if c in '.,;:!?()[]{}"\'-')
        punct_ratio = punct_count / text_length if text_length > 0 else 0
        
        # Доля заглавных букв
        upper_count = sum(1 for c in text if c.isupper())
        upper_ratio = upper_count / text_length if text_length > 0 else 0
        
        # Доля цифр
        digit_count = sum(1 for c in text if c.isdigit())
        digit_ratio = digit_count / text_length if text_length > 0 else 0
        
        features.append([
            text_length,
            avg_word_length,
            unique_words,
            punct_ratio,
            upper_ratio,
            digit_ratio
        ])
    
    return np.array(features)


def vectorize_with_classical(texts: List[str], method: str = "tfidf", 
                            ngram_range: Tuple[int, int] = (1, 2),
                            max_features: Optional[int] = None) -> Tuple[np.ndarray, Any]:
    """
    Векторизация текстов классическими методами.
    
    Args:
        texts: Список текстов
        method: Метод векторизации (tfidf, bow)
        ngram_range: Диапазон n-грамм
        max_features: Максимальное количество признаков
    
    Returns:
        Матрица признаков и векторизатор
    """
    config = VectorizationConfig(
        method=method,
        ngram_range=ngram_range,
        max_features=max_features
    )
    vectorizer = ClassicalVectorizers(config)
    X, _ = vectorizer.fit_transform(texts)
    return X.toarray() if hasattr(X, 'toarray') else X, vectorizer


def vectorize_with_embeddings(texts: List[str], 
                              model: Any,
                              aggregation: str = "mean") -> np.ndarray:
    """
    Векторизация текстов с использованием обученных эмбеддингов.
    
    Args:
        texts: Список текстов (уже токенизированных)
        model: Обученная модель (Word2Vec, FastText, Doc2Vec)
        aggregation: Метод агрегации (mean, max, sum)
    
    Returns:
        Матрица эмбеддингов документов
    """
    if isinstance(model, Doc2Vec):
        # Doc2Vec имеет встроенный метод для документов
        vectors = []
        for text in texts:
            tokens = simple_preprocess(text, deacc=False, min_len=1)
            if tokens:
                vec = model.infer_vector(tokens)
            else:
                vec = np.zeros(model.vector_size)
            vectors.append(vec)
        return np.array(vectors)
    
    # Word2Vec / FastText
    kv = model.wv if hasattr(model, 'wv') else model
    vector_size = kv.vector_size if hasattr(kv, 'vector_size') else model.vector_size
    
    vectors = []
    for text in texts:
        tokens = simple_preprocess(text, deacc=False, min_len=1)
        word_vectors = []
        for token in tokens:
            if token in kv:
                word_vectors.append(kv[token])
        
        if not word_vectors:
            vectors.append(np.zeros(vector_size))
            continue
        
        word_vectors = np.array(word_vectors)
        
        if aggregation == "mean":
            doc_vector = np.mean(word_vectors, axis=0)
        elif aggregation == "max":
            doc_vector = np.max(word_vectors, axis=0)
        elif aggregation == "sum":
            doc_vector = np.sum(word_vectors, axis=0)
        else:
            doc_vector = np.mean(word_vectors, axis=0)
        
        vectors.append(doc_vector)
    
    return np.array(vectors)


if __name__ == "__main__":
    # Тестирование
    sample_texts = [
        "Это тестовый текст для проверки предобработки. https://example.com test@email.ru",
        "Второй текст с эмодзи 😀 и HTML <p>тегами</p>.",
        "Третий текст 123 с числами и ПРОПИСНЫМИ буквами!"
    ]
    
    config = PreprocessingConfig(
        lowercase=True,
        remove_html=True,
        remove_urls=True,
        remove_emails=True,
        lemmatize=False,  # Отключаем для теста
        remove_stopwords=False
    )
    
    preprocessor = TextPreprocessor(config)
    processed = preprocessor.preprocess_batch(sample_texts)
    
    print("Обработанные тексты:")
    for i, (orig, proc) in enumerate(zip(sample_texts, processed)):
        print(f"\n{i+1}. Исходный: {orig[:50]}...")
        print(f"   Обработанный: {proc[:50]}...")
    
    # Мета-признаки
    meta_features = extract_meta_features(processed)
    print(f"\nМета-признаки (форма: {meta_features.shape}):")
    print(meta_features)