import re # --- Ресурсы для алгоритма --- STOP_WORDS = {'пр.', 'лек.', 'лаб.', 'семинар'} TITLES = ('доц.', 'проф.', 'асс.', 'ст.пр.') SURNAME_ENDINGS = ('ов', 'ев', 'ин', 'ский', 'цкой', 'их', 'ых', 'ова', 'ева', 'ина', 'ская', 'ян', 'ко', "ня", "ин") def is_surname_string(s: str) -> bool: """ Классифицирует строку, определяя, содержит ли она фамилию. """ if not isinstance(s, str) or not s: return False # Шаг 1: Очистка s_clean = s.strip() # Шаг 2: Жесткие правила "НЕ ФАМИЛИЯ" if s_clean.lower() in STOP_WORDS: return False if s_clean.isupper() and len(s_clean) > 3: return False if re.search(r'[\(\)/«»%]', s_clean): return False words = s_clean.split() if len(words) > 3: return False # Шаг 3: Жесткие правила "ТОЧНО ФАМИЛИЯ" if re.search(r'\b[А-Я]\.?', s_clean): # Ищет "И.А." или "И.А" return True if s_clean.lower().startswith(TITLES): return True # Шаг 4: Эвристический анализ для оставшихся случаев score = 0 # Правило на капитализацию if words and words[0][0].isupper(): score += 5 else: # Если слово не с большой буквы, это почти точно не фамилия return False # Правило на окончания last_word = words[-1].lower() if last_word.endswith(SURNAME_ENDINGS): score += 6 # Правило на количество слов if len(words) in [1, 2]: score += 2 # Пороговое значение THRESHOLD = 8 return score >= THRESHOLD def extract_last_name(name_str: str) -> str or None: """ Извлекает из строки только фамилию. Справляется с приклеенными званиями (типа "ст.пр.Дмитриев") и отбрасывает инициалы. Ищет первое слово, написанное с заглавной буквы. Args: name_str: Исходная "грязная" строка с именем. Returns: Чистая фамилия в виде строки, или None, если фамилия не найдена. """ # Проверка, что на вход подана строка if not isinstance(name_str, str): return None # Паттерн для поиска: # [А-ЯЁ] - одна заглавная русская буква в начале # [а-яё]+ - одна или более строчных русских букв после неё # (?:-[А-ЯЁ][а-яё]+)? - опциональная часть для двойных фамилий (например, -Петров) pattern = r'[А-ЯЁ][а-яё]+(?:-[А-ЯЁ][а-яё]+)?' match = re.search(pattern, name_str) if match: return match.group(0) # Возвращаем найденное совпадение else: return None # Если ничего не найдено # --- Шаг 0: Константы --- POSITIVE_KEYWORDS = ['зал', 'ауд', 'каб', 'корп', 'кор.'] NEGATIVE_KEYWORDS = ['доц', 'проф', 'асс', 'лек', 'пр'] # Транслитерация для унификации TRANS_TABLE = str.maketrans('TCBAHIMK', 'ТКВАНІМК') # Латиница -> Кириллица def is_room_number(s: str) -> bool: """ Проверяет, является ли строка номером кабинета, по многоуровневому алгоритму. """ # --- Шаг 1: Быстрые негативные фильтры --- if not isinstance(s, str) or not s.strip(): return False # 1. Проверка на пустоту if ',' in s: return False # 2. Проверка на запятые (даты) # 3. Проверка на "очевидный мусор" first_word = s.strip().lower().split()[0] if first_word in NEGATIVE_KEYWORDS: return False # 4. Проверка на инициалы (напр. А.Е.) if re.search(r'\b[А-Я]\.[А-Я]\.?\b', s): return False # 5. Проверка на длинное слово без цифр if not any(char.isdigit() for char in s) and len(s.split()) == 1 and len(s) > 10: return False # --- Шаг 2: Быстрые позитивные фильтры --- s_lower = s.lower() if any(keyword in s_lower for keyword in POSITIVE_KEYWORDS): return True # 1. Проверка по ключевым словам # --- Шаг 3: Основной анализ --- if not any(char.isdigit() for char in s): return False # 1. Требуется наличие цифр # 2. Создание "чистой" версии clean_s = s.upper().translate(TRANS_TABLE).replace(' ', '') # 3. Комплексный паттерн для проверки # Пояснение: # ^...$ - шаблон должен соответствовать всей строке # [А-ЯЁ]?-? - необязательная буква и необязательный дефис в начале # \d+ - одна или более цифр (ядро номера) # (?:[.-]\d+)* - необязательные группы ".число" или "-число" # [А-ЯЁ]?$ - необязательная буква в конце room_pattern = re.compile(r'^[А-ЯЁ]?-?\d+(?:[.-]\d+)*[А-ЯЁ]?$') if room_pattern.fullmatch(clean_s): return True # --- Шаг 4: Финальное решение --- return False