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 ]