Spaces:
Sleeping
Sleeping
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 | |
] | |