199 lines
8.3 KiB
Python
199 lines
8.3 KiB
Python
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
|
||
|
||
|
||
import requests
|
||
from urllib.parse import urlsplit, urlunsplit, quote
|
||
|
||
def download_file_from_url(url, output_filename):
|
||
"""
|
||
Скачивает файл по URL со спецсимволами и пробелами, сохраняя его под указанным именем.
|
||
|
||
Args:
|
||
url (str): Исходный URL, который может содержать пробелы и кириллицу.
|
||
output_filename (str): Имя файла для сохранения (например, 'calc.xls').
|
||
"""
|
||
try:
|
||
# --- Шаг 1: Правильное кодирование URL ---
|
||
# Разбираем URL на части: ('https', 'www.vstu.ru', '/path/to file.xls', '', '')
|
||
parts = urlsplit(url)
|
||
|
||
# Кодируем только путь, оставляя слэши '/' безопасными
|
||
# Это превратит ' ' в '%20', 'В' в '%D0%92' и т.д.
|
||
encoded_path = quote(parts.path, safe='/-_')
|
||
|
||
# Собираем URL обратно из частей с уже закодированным путем
|
||
encoded_url = urlunsplit((parts.scheme, parts.netloc, encoded_path, parts.query, parts.fragment))
|
||
|
||
|
||
# --- Шаг 2: Скачивание файла ---
|
||
response = requests.get(encoded_url, stream=True)
|
||
|
||
# Проверяем, успешен ли запрос (код 200 OK)
|
||
# Если сервер вернет ошибку (404, 500 и т.д.), здесь возникнет исключение
|
||
response.raise_for_status()
|
||
|
||
# --- Шаг 3: Сохранение файла ---
|
||
# Открываем файл для записи в бинарном режиме ('wb')
|
||
# Использование 'with' гарантирует, что файл будет закрыт автоматически
|
||
with open(output_filename, 'wb') as f:
|
||
for chunk in response.iter_content(chunk_size=8192):
|
||
f.write(chunk)
|
||
|
||
print(f"✅ Файл успешно скачан и сохранен как '{output_filename}'")
|
||
|
||
except requests.exceptions.RequestException as e:
|
||
print(f"❌ Ошибка скачивания: {e}")
|
||
|
||
except Exception as e:
|
||
print(f"❌ Произошла непредвиденная ошибка: {e}")
|
||
|
||
|
||
|