import gspread import requests from datetime import datetime, timedelta import json import random import time import re from functools import lru_cache @lru_cache(None) def partialize(s): s = s.lower().replace("ё", "е") p = re.split("[. ]+", s) return list(filter(lambda a: a, p)) def test_short(short: str, full: str) -> bool: short_parts = partialize(short) full_parts = partialize(full) for i in range(min(len(short_parts), len(full_parts))): if not full_parts[i].startswith(short_parts[i]): return False return True def find_in_list(short: str, full_list: list) -> str|list: result = [] for item in full_list: if test_short(short, item): result.append(item) if len(result) == 1: return result[0] elif short in result: return short else: return result def find_teacher_by_date(input_string: str, target_date: str) -> str: """ Ищет преподавателя по целевой дате в строке с датами и ФИО. :param input_string: Строка с датами и ФИО преподавателей. :param target_date: Целевая дата в формате "%d.%m". :return: ФИО преподавателя, связанного с целевой датой, или None, если дата не найдена. """ # Преобразуем целевую дату в формат "%d.%m" target_date_str = target_date#.strftime("%d.%m") # Регулярное выражение для поиска дат в формате "%d.%m" date_pattern = re.compile(r"\d{2}\.\d{2}") # Разделяем строку на строки по переносам lines = re.split(" {5,}", input_string) # Проходим по каждой строке for i in range(len(lines)): # Разделяем строку на даты и ФИО parts = lines[i].strip().split() # Извлекаем даты и ФИО dates = [] teacher = "" for part in parts: if date_pattern.fullmatch(part.replace(",", "")): # Убираем запятые, если они есть dates.append(part.replace(",", "")) # Убираем запятые, если они есть else: teacher += part + " " # Собираем ФИО # Проверяем, есть ли целевая дата в списке дат if target_date_str in dates: return teacher.strip(), i # Возвращаем ФИО преподавателя # Если дата не найдена return input_string, 1 def shorten_name(name: str) -> str: parts = name.split() result = "" for i in range(len(parts)): if i == 0: result = parts[0] + " " else: result += parts[i][0] + "." return result class Singleton(type): _instances = {} def __call__(cls, *args, **kwargs): if cls not in cls._instances: cls._instances[cls] = super(Singleton, cls).__call__(*args, **kwargs) return cls._instances[cls] class Lecture: def __init__(self, auditorium: str, day_of_week: str, pair_number: str, lecture: str, teacher: str): self.struct = Structure() self.auditorium = auditorium self.day_of_week = day_of_week self.day_of_week_i = self.struct.settings["week"].index(day_of_week) self.pair_number = pair_number self.input_teacher = teacher self.input_lecture = lecture print("[Пара] ", self.auditorium, "/", self.day_of_week, "/", self.pair_number) self.process_time() self.teacher = self.process_teacher() if self.teacher == False: self.complete = False print() return self.lecture = self.process_lecture() if self.lecture == False: self.complete = False print() return print("[Препод.] ", self.input_teacher, "->", self.teacher) print("[Предмет] ", self.input_lecture, "->", self.lecture) self.name = self.format_name() self.complete = True print() def process_time(self): target_date = self.struct.monday + timedelta(days=self.day_of_week_i) self.date = target_date.strftime("%d.%m") for call in self.struct.calls: if call["lesson"] == self.pair_number: start_time = datetime.fromisoformat("2025-01-01T" + call["start_time"]) end_time = datetime.fromisoformat("2025-01-01T" + call["end_time"]) # Объединяем дату и время self.start = target_date.replace( hour=start_time.hour, minute=start_time.minute, second=start_time.second, microsecond=0, tzinfo=start_time.tzinfo ).isoformat(timespec="milliseconds") self.end = target_date.replace( hour=end_time.hour, minute=end_time.minute, second=end_time.second, microsecond=0, tzinfo=end_time.tzinfo ).isoformat(timespec="milliseconds") def process_teacher(self) -> str: teacher, self.teacher_index = find_teacher_by_date(self.input_teacher, self.date) if teacher in self.struct.settings["aliases"].keys(): teacher = self.struct.settings["aliases"][teacher] variants = find_in_list(teacher, self.struct.teachers) return self.select_variant(teacher, variants, True) def process_lecture(self) -> str: parts = re.split(" {5,}", self.input_lecture) if self.teacher_index < len(parts): lecture = parts[self.teacher_index] else: lecture = parts[0] if lecture in self.struct.settings["aliases"].keys(): lecture = self.struct.settings["aliases"][lecture] variants = find_in_list(lecture, self.struct.disciplines) return self.select_variant(lecture, variants, False) def format_name(self) -> str: formatted_date = datetime.fromisoformat(self.start).strftime("%H:%M %d.%m.%Y") return f"{self.lecture} [{shorten_name(self.teacher)}] ({formatted_date})" def select_variant(self, search: str, variants: str|list, is_teacher: bool) -> bool|str: if isinstance(variants, str): return variants print("Невозможно выбрать", "преподавателя" if is_teacher else "лекцию", "для значения", f'"{search}".', "Выберите вариант:" if len(variants) > 0 else "") if len(variants) == 0: return False for i in range(len(variants)): print(str(i + 1).rjust(3) + ".", variants[i]) print(" 0. [Пропустить]") choice = int(input("Введите значение: ")) - 1 if choice < 0: return False return variants[choice] def json(self) -> dict|None: return { "auditorium": self.auditorium, "day_of_week": self.day_of_week, "pair_number": self.pair_number, "input_lecture": self.input_lecture, "input_teacher": self.input_teacher, "teacher": self.teacher, "lecture": self.lecture, "start": self.start, "end": self.end, "name": self.name } class Structure(metaclass=Singleton): def __init__(self): print("Подключение Google Sheets...", end=" ") gc = gspread.oauth() self.document = gc.open_by_key("1srtrHkwbTCfvxyeDtb_ca4RMx8W1kxgUQ-MAIumamVA") print("Готово.") print("Загрузка настроек...", end=" ") with open("settings.json", 'r', encoding='utf-8') as file: self.settings = json.load(file) print("Готово.") print("Загрузка списка преподавателей...", end=" ") self.teachers = list(map(lambda a: a["name"], self.get_json("teachers"))) print("Готово.") print("Загрузка списка дисциплин...", end=" ") self.disciplines = list(map(lambda a: a["name"], self.get_json("disciplines"))) print("Готово.") print("Загрузка расписания...", end=" ") self.calls = self.get_json("calls", "schedule") print("Готово.") self.monday = self.get_monday() self.week = self.get_week_number() print("Данные загружены!") def get_json(self, type, sub_type=None) -> dict: resp = requests.get(url="https://ulsu.ru/oto/api/production/internal/getEnterpriseData.php?type=" + type, params={}) return resp.json()[type if sub_type is None else sub_type] def get_monday(self): today = datetime.today() days_until_next_monday = (7 - today.weekday()) % 7 # Если сегодня понедельник, days_until_next_monday будет 0, поэтому добавляем 7 дней # if days_until_next_monday == 0: # days_until_next_monday = 7 return today + timedelta(days=days_until_next_monday) def get_week_number(self, target_date:datetime|None=None): if target_date is None: target_date = self.monday first_week_start_date = datetime.strptime(self.settings["first_week"], "%Y-%m-%d") return ((target_date - first_week_start_date).days // 7) % 2 + 1 def get_worksheets(self): return self.document.worksheets() def get_document_data(self, worksheet, range_name=None, max_retries=5, initial_backoff=1, maximum_backoff=32): retries = 0 while retries < max_retries: try: if range_name is None: return worksheet.get_all_values() else: return worksheet.get(range_name) except gspread.exceptions.APIError as e: retries += 1 if retries == max_retries: raise e backoff = min(initial_backoff * (2 ** (retries - 1)) + random.random(), maximum_backoff) time.sleep(backoff) def find_range(self, worksheet, first_week=True): def check(cell): if first_week: return "1 неделя" in cell or "I неделя" in cell else: return "2 неделя" in cell or "II неделя" in cell all_values = self.get_document_data(worksheet) week_row = None for i, row in enumerate(all_values): for j, cell in enumerate(row): if check(cell): week_row = i + 1 break if week_row is not None: break for row in range(week_row, worksheet.row_count + 1): cell_value = all_values[row][0] if cell_value == "1": return "C%d:H%d" % (row+1, row+16) def get_lectures_from_range(self, worksheet, range_name) -> list: # Получаем название аудитории из названия листа auditorium = worksheet.title # Получаем данные из указанного диапазона data = self.get_document_data(worksheet, range_name) if len(data) < 2: return [] # Дни недели days_of_week = self.settings["week"] day_order = {day: idx for idx, day in enumerate(days_of_week)} # Словарь для порядка дней # Список для хранения лекций lectures = [] # Проходим по данным for row_index in range(0, len(data), 2): # Шаг 2, так как каждая пара строк — это одна пара в расписании lecture_row = data[row_index] # Строка с названием лекции teacher_row = data[row_index + 1] # Строка с преподавателем # Номер пары pair_number = (row_index // 2) + 1 # Проходим по дням недели (столбцам) for col_index, (lecture, teacher) in enumerate(zip(lecture_row, teacher_row)): if lecture.strip() and teacher.strip(): # Если есть и название лекции, и преподаватель day_of_week = days_of_week[col_index] # День недели lectures.append(Lecture(auditorium, day_of_week, pair_number, lecture, teacher)) lectures = sorted(lectures, key=lambda x: (x.day_of_week_i, x.pair_number)) return lectures if __name__ == "__main__": struct = Structure() week = struct.get_week_number() lectures = [] for worksheet in struct.get_worksheets(): print(worksheet.title + "...") range_name = struct.find_range(worksheet, week == 1) part = struct.get_lectures_from_range(worksheet, range_name) lectures.extend(part) lectures = list(filter(lambda l: l.complete, lectures)) with open("lecture.json", "w", encoding="utf-8") as f: json.dump(list(map(lambda l: l.json(), lectures)), f, ensure_ascii=False, indent=4)