File size: 11,852 Bytes
57cf043
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
import re

from natasha import Doc, MorphVocab, NewsEmbedding, NewsMorphTagger, Segmenter


from .constants import (
    ABBREVIATION_RE,
    CLOSE_BRACKET_RE,
    FIRST_CHARS_SET,
    NEXT_MARKER_RE,
    NON_SENTENCE_ENDINGS,
    SECOND_CHARS_SET,
    UPPERCASE_LETTER_RE,
)
from .structures import Abbreviation


class AbbreviationExtractor:
    def __init__(self):
        """
        Инициализация экстрактора сокращений.

        Создает необходимые компоненты для лемматизации и компилирует регулярные выражения.
        """
        # Инициализация компонентов Natasha для лемматизации
        self.segmenter = Segmenter()
        self.morph_tagger = NewsMorphTagger(NewsEmbedding())
        self.morph_vocab = MorphVocab()

        # Компиляция регулярных выражений
        self.next_re = re.compile(NEXT_MARKER_RE, re.IGNORECASE)
        self.abbreviation_re = re.compile(ABBREVIATION_RE)
        self.uppercase_letter_re = re.compile(UPPERCASE_LETTER_RE)
        self.close_bracket_re = re.compile(CLOSE_BRACKET_RE)

        self.delimiters = [
            f'{char1} {char2} '.format(char1, char2)
            for char1 in FIRST_CHARS_SET
            for char2 in SECOND_CHARS_SET
        ]

    def extract_abbreviations_from_text(
        self,
        text: str,
    ) -> list[Abbreviation]:
        """
        Извлечение всех сокращений из текста.

        Args:
            text: Текст для обработки

        Returns:
            list[Abbreviation]: Список найденных сокращений
        """
        sentences = self._extract_sentences_with_abbreviations(text)

        abbreviations = [self._process_one_sentence(sentence) for sentence in sentences]
        abbreviations = sum(abbreviations, [])  # делаем список одномерным
        abbreviations = [abbreviation.process() for abbreviation in abbreviations]

        return abbreviations

    def _process_one_sentence(self, sentence: str) -> list[Abbreviation]:
        """
        Обработка одного предложения для извлечения сокращений.

        Args:
            sentence: Текст для обработки

        Returns:
            list[Abbreviation]: Список найденных сокращений
        """
        search_iter = self.next_re.finditer(sentence)
        prev_index = 0

        abbreviations = []

        for match in search_iter:
            abbreviation, prev_index = self._process_match(sentence, match, prev_index)
            if abbreviation is not None:
                abbreviations.append(abbreviation)

        return abbreviations

    def _process_match(
        self,
        sentence: str,
        match: re.Match,
        prev_index: int,
    ) -> tuple[Abbreviation | None, int]:
        """
        Обработка одного совпадения с конструкцией "далее - {short_form}" для извлечения сокращений.

        Args:
            sentence: Текст для обработки
            match: Совпадение для обработки
            prev_index: Предыдущий индекс

        Returns:
            tuple[Abbreviation | None, int]: Найденное сокращение (None, если нет сокращения) и следующий индекс
        """
        start, end = match.start(), match.end()
        text = sentence[start:]

        index_close_parenthesis = self._get_close_parenthesis_index(text)
        index_point = self._get_point_index(text, start)

        prev_index += index_point
        short_word = text[end : start + index_close_parenthesis].strip()

        if len(short_word.split()) < 2:
            abbreviation = self._process_match_for_word(
                short_word, text, start, end, prev_index
            )

        else:
            abbreviation = self._process_match_for_phrase(
                short_word, text, start, end, prev_index
            )

        prev_index = start + index_close_parenthesis + 1

        return abbreviation, prev_index

    def _get_close_parenthesis_index(self, text: str) -> int:
        """
        Получение индекса закрывающей скобки в тексте.

        Args:
            text: Текст для обработки

        Returns:
            int: Индекс закрывающей скобки или 0, если не найдено
        """
        result = self.close_bracket_re.search(text)
        if result is None:
            return 0
        return result.start()

    def _get_point_index(self, text: str, start_index: int) -> int:
        """
        Получение индекса точки в тексте.

        Args:
            text: Текст для обработки
            start_index: Индекс начала поиска

        Returns:
            int: Индекс точки или 0, если не найдено
        """
        result = text.rfind('.', 0, start_index - 1)
        if result == -1:
            return 0
        return result

    def _process_match_for_word(
        self,
        short_word: str,
        text: str,
        start_next_re_index: int,
        end_next_re_index: int,
        prev_index: int,
    ) -> Abbreviation | None:
        """
        Обработка сокращения, состоящего из одного слова.

        Args:
            short_word: Сокращение
            text: Текст для обработки
            start_next_re_index: Индекс начала следующего совпадения
            end_next_re_index: Индекс конца следующего совпадения
            prev_index: Предыдущий индекс

        Returns:
            Abbreviation | None: Найденное сокращение или None, если нет сокращения
        """
        if self.abbreviation_re.findall(text) or (short_word == 'ПДн'):
            return None

        lemm_text = self._lemmatize_text(text[prev_index:start_next_re_index])
        lemm_short_word = self._lemmatize_text(short_word)

        search_word = re.search(lemm_short_word, lemm_text)

        if not search_word:
            start_text_index = self._get_start_text_index(
                text,
                start_next_re_index,
                prev_index,
            )

            if start_text_index is None:
                return None

            full_text = text[prev_index + start_text_index : end_next_re_index]

        else:
            index_word = search_word.span()[1]
            space_index = text[prev_index:start_next_re_index].rfind(' ', 0, index_word)
            if space_index == -1:
                space_index = 0
            text = text[prev_index + space_index : start_next_re_index]

        full_text = text.replace(')', '').replace('(', '').replace('', '- ')

        return Abbreviation(
            short_form=short_word,
            full_form=full_text,
        )

    def _process_match_for_phrase(
        self,
        short_word: str,
        text: str,
        start_next_re_index: int,
        end_next_re_index: int,
        prev_index: int,
    ) -> list[Abbreviation] | None:
        """
        Обработка сокращения, состоящего из нескольких слов.
        В действительности производится обработка первого слова сокращения, а затем вместо него подставляется полное сокращение.

        Args:
            short_word: Сокращение
            text: Текст для обработки
            start_next_re_index: Индекс начала следующего совпадения
            end_next_re_index: Индекс конца следующего совпадения
            prev_index: Предыдущий индекс

        Returns:
            list[Abbreviation] | None: Найденные сокращения или None, если нет сокращений
        """
        first_short_word = short_word.split()[0]
        result = self._process_match_for_word(
            first_short_word, text, start_next_re_index, end_next_re_index, prev_index
        )
        if result is None:
            return None
        return Abbreviation(
            short_form=short_word,
            full_form=result.full_form,
        )

    def _get_start_text_index(
        self,
        text: str,
        start_next_re_index: int,
        prev_index: int,
    ) -> int | None:
        """
        Получение индекса начала текста для поиска сокращения с учётом разделителей типа
        "; - "
        ": - "
        ";  "
        ": ‒ " и т.п.

        Args:
            text: Текст для обработки
            start_next_re_index: Индекс начала следующего совпадения
            prev_index: Предыдущий индекс

        Returns:
            int | None: Индекс начала текста или None, если не найдено
        """
        if prev_index == 0:
            return 0

        for delimiter in self.delimiters:
            result = re.search(delimiter, text[prev_index:start_next_re_index])
            if result is not None:
                return result.span()[1]

        return None

    def _lemmatize_text(self, text: str) -> str:
        """
        Лемматизация текста.

        Args:
            text: Текст для лемматизации

        Returns:
            str: Лемматизированный текст
        """
        doc = Doc(text)
        doc.segment(self.segmenter)
        doc.tag_morph(self.morph_tagger)

        for token in doc.tokens:
            token.lemmatize(self.morph_vocab)

        return ' '.join([token.lemma for token in doc.tokens])

    def _extract_sentences_with_abbreviations(self, text: str) -> list[str]:
        """
        Разбивает текст на предложения с учетом специальных сокращений.

        Точка после сокращений из NON_SENTENCE_ENDINGS не считается концом предложения.

        Args:
            text: Текст для разбиения

        Returns:
            list[str]: Список предложений
        """
        text = text.replace('\n', ' ')
        sentence_endings = re.finditer(r'\.\s+[А-Я]', text)

        sentences = []
        start = 0

        for match in sentence_endings:
            end = match.start() + 1

            # Проверяем, не заканчивается ли предложение на специальное сокращение
            preceding_text = text[start:end]
            words = preceding_text.split()

            if words and any(
                words[-1].rstrip('.').startswith(abbr) for abbr in NON_SENTENCE_ENDINGS
            ):
                continue

            sentence = text[start:end].strip()
            sentences.append(sentence)
            start = end + 1

        # Добавляем последнее предложение
        if start < len(text):
            sentences.append(text[start:].strip())

        return [
            sentence
            for sentence in sentences
            if self.next_re.search(sentence) is not None
        ]